From ea9853660c1fafbcd42f4d5f0118ed82be6f8eac Mon Sep 17 00:00:00 2001 From: yao-msft <50888816+yao-msft@users.noreply.github.com> Date: Tue, 30 Apr 2024 12:16:19 -0700 Subject: [PATCH 1/8] Update instructions to build repo locally with vcpkg enabled (#4426) --- .vsconfig | 1 + README.md | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.vsconfig b/.vsconfig index 101e664b78..453964198e 100644 --- a/.vsconfig +++ b/.vsconfig @@ -49,6 +49,7 @@ "Microsoft.VisualStudio.Component.VC.TestAdapterForBoostTest", "Microsoft.VisualStudio.Component.VC.TestAdapterForGoogleTest", "Microsoft.VisualStudio.Component.VC.ASAN", + "Microsoft.VisualStudio.Component.Vcpkg", "Microsoft.VisualStudio.Component.Windows10SDK.19041", "Microsoft.VisualStudio.Component.Windows11SDK.22000", "Microsoft.VisualStudio.Workload.NativeDesktop", diff --git a/README.md b/README.md index 390dc15ffb..ae2fe25c2d 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,8 @@ The client is built around the concept of sources; a set of packages effectively 1. Clone the repository 2. Configure your system, please use the [configuration file](.configurations/configuration.dsc.yaml). This can be applied by either: * [Dev Home](https://github.com/microsoft/devhome)'s machine configuration tool - * WinGet configuration. If you have the experimental feature enabled, run `winget configure .configurations/configuration.dsc.yaml` from the project root so relative paths resolve correctly + * WinGet configuration. Run `winget configure .configurations/configuration.dsc.yaml` from the project root so relative paths resolve correctly +3. Run `vcpkg integrate install` from Developer Command Prompt for VS 2022. This is one time setup step until configuration file in step 2 is updated to work with vcpkg setup. ### Prerequisites @@ -96,6 +97,7 @@ The client is built around the concept of sources; a set of packages effectively * .NET Desktop Development * Desktop Development with C++ * Universal Windows Platform Development + * Check [.vsconfig file](.vsconfig) for full components list * [Windows SDK for Windows 11 (10.0.22000.194)](https://developer.microsoft.com/en-us/windows/downloads/sdk-archive/) > **Note**: You can also get it through `winget install Microsoft.WindowsSDK --version 10.0.22000.832` (use --force if you have a newer version installed) or via Visual Studio > Get Tools and Features > Individual Components > Windows 10 SDK (10.0.22000.0) * The following extensions: From c2781b03d820e2c84e6d489468e6a9886ada42fd Mon Sep 17 00:00:00 2001 From: yao-msft <50888816+yao-msft@users.noreply.github.com> Date: Tue, 30 Apr 2024 18:39:58 -0700 Subject: [PATCH 2/8] Make SecurityContext field name camelCase (#4433) To be consistent with other field names in configuration file. We may want to improve to be case insensitive of all field names in the future. --- .../ConfigurationDynamicRuntimeFactory.cpp | 2 +- .../Tests/OpenConfigurationSetTests.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/AppInstallerCLICore/ConfigurationDynamicRuntimeFactory.cpp b/src/AppInstallerCLICore/ConfigurationDynamicRuntimeFactory.cpp index f3ae3e1ce0..5bd590c9a8 100644 --- a/src/AppInstallerCLICore/ConfigurationDynamicRuntimeFactory.cpp +++ b/src/AppInstallerCLICore/ConfigurationDynamicRuntimeFactory.cpp @@ -99,7 +99,7 @@ namespace AppInstaller::CLI::ConfigurationRemoting // Support for 0.2 schema via metadata value // TODO: Support case insensitive lookup by iteration auto unitMetadata = unit.Metadata(); - auto securityContext = unitMetadata.TryLookup(L"SecurityContext"); + auto securityContext = unitMetadata.TryLookup(L"securityContext"); if (securityContext) { auto securityContextProperty = securityContext.try_as(); diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/OpenConfigurationSetTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/OpenConfigurationSetTests.cs index db34cb2f05..04c9316f93 100644 --- a/src/Microsoft.Management.Configuration.UnitTests/Tests/OpenConfigurationSetTests.cs +++ b/src/Microsoft.Management.Configuration.UnitTests/Tests/OpenConfigurationSetTests.cs @@ -487,7 +487,7 @@ public void TestSet_Serialize_0_2() directives: description: FakeDescription allowPrerelease: true - SecurityContext: elevated + securityContext: elevated settings: TestString: Hello TestBool: false @@ -501,7 +501,7 @@ public void TestSet_Serialize_0_2() - dependency3 directives: description: FakeDescription2 - SecurityContext: elevated + securityContext: elevated settings: TestString: Bye TestBool: true @@ -529,14 +529,14 @@ public void TestSet_Serialize_0_2() Assert.Equal("FakeModule", set.Units[0].Type); Assert.Equal(ConfigurationUnitIntent.Assert, set.Units[0].Intent); Assert.Equal("TestId", set.Units[0].Identifier); - this.VerifyValueSet(set.Units[0].Metadata, new ("description", "FakeDescription"), new ("allowPrerelease", true), new ("SecurityContext", "elevated")); + this.VerifyValueSet(set.Units[0].Metadata, new ("description", "FakeDescription"), new ("allowPrerelease", true), new ("securityContext", "elevated")); this.VerifyValueSet(set.Units[0].Settings, new ("TestString", "Hello"), new ("TestBool", false), new ("TestInt", 1234)); Assert.Equal("FakeModule2", set.Units[1].Type); Assert.Equal(ConfigurationUnitIntent.Apply, set.Units[1].Intent); Assert.Equal("TestId2", set.Units[1].Identifier); this.VerifyStringArray(set.Units[1].Dependencies, "TestId", "dependency2", "dependency3"); - this.VerifyValueSet(set.Units[1].Metadata, new ("description", "FakeDescription2"), new ("SecurityContext", "elevated")); + this.VerifyValueSet(set.Units[1].Metadata, new ("description", "FakeDescription2"), new ("securityContext", "elevated")); ValueSet mapping = new ValueSet(); mapping.Add("Key", "TestValue"); From 2b185be5192229b17e4fa8fd32c39d3c606a8d7d Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Wed, 1 May 2024 09:16:23 -0700 Subject: [PATCH 3/8] Add Workflow logs and fix installed version workflow bug (#4432) Fixes #4425 ## Change This adds a `Workflow` channel (disabled by default) that has the entry to tasks and the `Get`/`Add`/`Contains` calls logged to it. This enables a view into a flow that can be collected by a third party. It also fixes a behavior change with the side-by-side code path for selecting the installed version that would lead to no data item being inserted, when the previous behavior was that a `nullptr` would be inserted when no version is installed. --- .github/actions/spelling/allow.txt | 1 + .github/actions/spelling/expect.txt | 2 +- src/AppInstallerCLICore/ExecutionContext.cpp | 18 +++++++++ src/AppInstallerCLICore/ExecutionContext.h | 5 ++- .../Workflows/WorkflowBase.cpp | 24 +++++++++++- .../Workflows/WorkflowBase.h | 1 + .../CompositeSource.cpp | 4 +- .../AppInstallerLogging.cpp | 5 +++ .../Public/AppInstallerLanguageUtilities.h | 39 ++++++++++++++++++- .../Public/AppInstallerLogging.h | 3 +- 10 files changed, 94 insertions(+), 8 deletions(-) diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 4bd68dfff2..c85d0047d1 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -375,6 +375,7 @@ wcsicmp webpage WHOLECHAIN wil +windbg wincrypt WINEVENT winget diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 43c5df655e..10806a348f 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -295,7 +295,7 @@ NESTEDINSTALLER netfx netlify NETSDK -Newtonsoft +Newtonsoft nlohmann NNS NOAGGREGATION diff --git a/src/AppInstallerCLICore/ExecutionContext.cpp b/src/AppInstallerCLICore/ExecutionContext.cpp index 12feadf263..97d82f63ca 100644 --- a/src/AppInstallerCLICore/ExecutionContext.cpp +++ b/src/AppInstallerCLICore/ExecutionContext.cpp @@ -487,6 +487,24 @@ namespace AppInstaller::CLI::Execution } #endif + void ContextEnumBasedVariantMapActionCallback(const void* map, Data data, EnumBasedVariantMapAction action) + { + switch (action) + { + case EnumBasedVariantMapAction::Add: + AICLI_LOG(Workflow, Info, << "Setting data item: " << data); + break; + case EnumBasedVariantMapAction::Contains: + AICLI_LOG(Workflow, Info, << "Checking data item: " << data); + break; + case EnumBasedVariantMapAction::Get: + AICLI_LOG(Workflow, Info, << "Getting data item: " << data); + break; + } + + UNREFERENCED_PARAMETER(map); + } + std::string Context::GetResumeId() { return m_checkpointManager->GetResumeId(); diff --git a/src/AppInstallerCLICore/ExecutionContext.h b/src/AppInstallerCLICore/ExecutionContext.h index b686e48242..a5fa557570 100644 --- a/src/AppInstallerCLICore/ExecutionContext.h +++ b/src/AppInstallerCLICore/ExecutionContext.h @@ -85,10 +85,13 @@ namespace AppInstaller::CLI::Execution bool WaitForAppShutdownEvent(); #endif + // Callback to log data actions. + void ContextEnumBasedVariantMapActionCallback(const void* map, Data data, EnumBasedVariantMapAction action); + // The context within which all commands execute. // Contains input/output via Execution::Reporter and // arguments via Execution::Args. - struct Context : EnumBasedVariantMap + struct Context : EnumBasedVariantMap { Context(std::ostream& out, std::istream& in) : Reporter(out, in) {} diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index 752455b7d8..dbf3e754bf 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -13,6 +13,8 @@ #include #include +EXTERN_C IMAGE_DOS_HEADER __ImageBase; + using namespace std::string_literals; using namespace AppInstaller::Utility::literals; using namespace AppInstaller::Pinning; @@ -259,6 +261,19 @@ namespace AppInstaller::CLI::Workflow m_func(context); } + void WorkflowTask::Log() const + { + if (m_isFunc) + { + // Using `00000001`80000000` as base address default when loading dll into windbg as dump file. + AICLI_LOG(Workflow, Info, << "Running task: 0x" << m_func << " [ln 00000001`80000000+" << std::hex << (reinterpret_cast(m_func) - reinterpret_cast(&__ImageBase)) << "]"); + } + else + { + AICLI_LOG(Workflow, Info, << "Running task: " << m_name); + } + } + Repository::PredefinedSource DetermineInstalledSource(const Execution::Context& context) { Repository::PredefinedSource installedSource = Repository::PredefinedSource::Installed; @@ -1368,10 +1383,10 @@ namespace AppInstaller::CLI::Workflow void GetInstalledPackageVersion(Execution::Context& context) { - std::shared_ptr installed = context.Get()->GetInstalled(); - if (ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::SideBySide)) { + std::shared_ptr installed = context.Get()->GetInstalled(); + if (installed) { // TODO: This may need to be expanded dramatically to enable targeting across a variety of dimensions (architecture, etc.) @@ -1395,6 +1410,10 @@ namespace AppInstaller::CLI::Workflow context.Add(installed->GetLatestVersion()); } } + else + { + context.Add(nullptr); + } } else { @@ -1433,6 +1452,7 @@ AppInstaller::CLI::Execution::Context& operator<<(AppInstaller::CLI::Execution:: if (context.ShouldExecuteWorkflowTask(task)) #endif { + task.Log(); task(context); } } diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h index c8cdce1f2c..a923c8dd26 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.h +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -69,6 +69,7 @@ namespace AppInstaller::CLI::Workflow bool IsFunction() const { return m_isFunc; } Func Function() const { return m_func; } bool ExecuteAlways() const { return m_executeAlways; } + void Log() const; private: bool m_isFunc = false; diff --git a/src/AppInstallerRepositoryCore/CompositeSource.cpp b/src/AppInstallerRepositoryCore/CompositeSource.cpp index 11ae60ecff..a34ddab666 100644 --- a/src/AppInstallerRepositoryCore/CompositeSource.cpp +++ b/src/AppInstallerRepositoryCore/CompositeSource.cpp @@ -10,7 +10,7 @@ namespace AppInstaller::Repository { using namespace std::string_view_literals; - namespace + namespace anon { Utility::VersionAndChannel GetVACFromVersion(IPackageVersion* packageVersion) { @@ -1365,6 +1365,8 @@ namespace AppInstaller::Repository } } + using namespace anon; + CompositeSource::CompositeSource(std::string identifier) { m_details.Identifier = std::move(identifier); diff --git a/src/AppInstallerSharedLib/AppInstallerLogging.cpp b/src/AppInstallerSharedLib/AppInstallerLogging.cpp index 4e2054ae71..295304febf 100644 --- a/src/AppInstallerSharedLib/AppInstallerLogging.cpp +++ b/src/AppInstallerSharedLib/AppInstallerLogging.cpp @@ -20,6 +20,7 @@ namespace AppInstaller::Logging case Channel::Core: return "CORE"; case Channel::Test: return "TEST"; case Channel::Config: return "CONF"; + case Channel::Workflow: return "WORK"; default: return "NONE"; } } @@ -60,6 +61,10 @@ namespace AppInstaller::Logging { return Channel::Config; } + else if (lowerChannel == "workflow") + { + return Channel::Workflow; + } else if (lowerChannel == "default" || lowerChannel == "defaults") { return Channel::Defaults; diff --git a/src/AppInstallerSharedLib/Public/AppInstallerLanguageUtilities.h b/src/AppInstallerSharedLib/Public/AppInstallerLanguageUtilities.h index 11cfefbab0..9b958342c1 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerLanguageUtilities.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerLanguageUtilities.h @@ -89,8 +89,20 @@ namespace AppInstaller static constexpr inline size_t Index(Enum e) { return static_cast(e) + 1; } }; + // An action that can be taken on an EnumBasedVariantMap. + enum class EnumBasedVariantMapAction + { + Add, + Contains, + Get, + }; + + // A callback function that can be used for logging map actions. + template + using EnumBasedVariantMapActionCallback = void (*)(const void* map, Enum value, EnumBasedVariantMapAction action); + // Provides a map of the Enum to the mapped types. - template typename Mapping> + template typename Mapping, EnumBasedVariantMapActionCallback Callback = nullptr> struct EnumBasedVariantMap { using Variant = EnumBasedVariant; @@ -103,28 +115,51 @@ namespace AppInstaller template void Add(mapping_t&& v) { + if constexpr (Callback) + { + Callback(this, E, EnumBasedVariantMapAction::Add); + } m_data[E].emplace(std::move(std::forward>(v))); } template void Add(const mapping_t& v) { + if constexpr (Callback) + { + Callback(this, E, EnumBasedVariantMapAction::Add); + } m_data[E].emplace(v); } // Return a value indicating whether the given enum is stored in the map. - bool Contains(Enum e) const { return (m_data.find(e) != m_data.end()); } + bool Contains(Enum e) const + { + if constexpr (Callback) + { + Callback(this, e, EnumBasedVariantMapAction::Contains); + } + return (m_data.find(e) != m_data.end()); + } // Gets the value. template mapping_t& Get() { + if constexpr (Callback) + { + Callback(this, E, EnumBasedVariantMapAction::Get); + } return std::get(GetVariant(E)); } template const mapping_t& Get() const { + if constexpr (Callback) + { + Callback(this, E, EnumBasedVariantMapAction::Get); + } return std::get(GetVariant(E)); } diff --git a/src/AppInstallerSharedLib/Public/AppInstallerLogging.h b/src/AppInstallerSharedLib/Public/AppInstallerLogging.h index 22aa749cec..a7ac612abb 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerLogging.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerLogging.h @@ -56,9 +56,10 @@ namespace AppInstaller::Logging Core = 0x20, Test = 0x40, Config = 0x80, + Workflow = 0x100, None = 0, All = 0xFFFFFFFF, - Defaults = All & ~SQL, + Defaults = All & ~(SQL | Workflow), }; DEFINE_ENUM_FLAG_OPERATORS(Channel); From 457f84bf5ac7947a12c8b65879001cce1cdbfc43 Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Wed, 1 May 2024 14:47:40 -0700 Subject: [PATCH 4/8] Block control codes and truncate longer configuration text blocks (#4436) ## Change All control codes in the range [0x0, 0x20) and the DELETE control code 0x7F (with the exceptions of the tab, line feed, and carriage return characters) will result in an error from the YAML parser. An alternative solution is to convert them to their control pictures, but it was decided that it was best to fail at this time. The configuration output for each unit during the "show" portion (used by almost all of the commands to display details about the file) will limit the amount of output allowed for any field that comes from an external source. Data from the file will present a warning that it was truncated just below its output. If anything is truncated, an overall "error" will be output as well. --- .github/actions/spelling/allow.txt | 1 + .github/actions/spelling/expect.txt | 1 + src/AppInstallerCLICore/Resources.h | 2 + .../Workflows/ConfigurationFlow.cpp | 505 ++++++++++-------- .../ConfigureShowCommand.cs | 13 + .../Configuration/LargeContentStrings.yml | 22 + .../Modules/xE2EMalicious/xE2EMalicious.psd1 | 33 ++ .../Modules/xE2EMalicious/xE2EMalicious.psm1 | 86 +++ .../Shared/Strings/en-us/winget.resw | 7 + .../AppInstallerCLITests.vcxproj | 31 +- .../AppInstallerCLITests.vcxproj.filters | 6 + src/AppInstallerCLITests/Strings.cpp | 36 +- .../TestData/ContainsEscapeControlCode.yaml | 1 + src/AppInstallerCLITests/Yaml.cpp | 175 ++++++ src/AppInstallerCLITests/YamlManifest.cpp | 159 ------ .../AppInstallerStrings.cpp | 91 +++- .../Public/AppInstallerStrings.h | 11 + .../Public/winget/Yaml.h | 3 +- src/AppInstallerSharedLib/Yaml.cpp | 2 + src/AppInstallerSharedLib/YamlWrapper.cpp | 40 +- 20 files changed, 815 insertions(+), 410 deletions(-) create mode 100644 src/AppInstallerCLIE2ETests/TestData/Configuration/LargeContentStrings.yml create mode 100644 src/AppInstallerCLIE2ETests/TestData/Configuration/Modules/xE2EMalicious/xE2EMalicious.psd1 create mode 100644 src/AppInstallerCLIE2ETests/TestData/Configuration/Modules/xE2EMalicious/xE2EMalicious.psm1 create mode 100644 src/AppInstallerCLITests/TestData/ContainsEscapeControlCode.yaml create mode 100644 src/AppInstallerCLITests/Yaml.cpp diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index c85d0047d1..abd2303149 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -349,6 +349,7 @@ und UNICODESTRING uninstalling Unmarshal +unskipped untimes updatefile updatemanifest diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 10806a348f..0a84af9093 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -116,6 +116,7 @@ ECustom EFGH EFile efileresource +EMalicious endregion ENDSESSION EPester diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index 443df0163d..0b554603ea 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -128,6 +128,8 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationWarning); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationWarningPromptApply); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationWarningPromptTest); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationWarningSetViewTruncated); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationWarningValueTruncated); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureShowCommandLongDescription); diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp index 58736ea4fb..d5f6d8db02 100644 --- a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp @@ -106,7 +106,7 @@ namespace AppInstaller::CLI::Workflow return factory; } - std::optional GetValueSetString(const ValueSet& valueSet, std::wstring_view value) + winrt::hstring GetValueSetString(const ValueSet& valueSet, std::wstring_view value) { if (valueSet.HasKey(value)) { @@ -114,7 +114,7 @@ namespace AppInstaller::CLI::Workflow IPropertyValue property = object.try_as(); if (property && property.Type() == PropertyType::String) { - return Utility::LocIndString{ Utility::ConvertToUTF8(property.GetString()) }; + return property.GetString(); } } @@ -136,259 +136,344 @@ namespace AppInstaller::CLI::Workflow return {}; } - void OutputPropertyValue(OutputStream& out, const IPropertyValue property) + // Contains the output functions and tracks whether any fields needed to be truncated. + struct OutputHelper { - switch (property.Type()) + OutputHelper(Execution::Context& context) : m_context(context) {} + + size_t ValuesTruncated = 0; + + // Converts a string from the configuration API surface for output. + // All strings coming from the API are external data and not localizable by us. + Utility::LocIndString ConvertForOutput(const std::string& input, size_t maxLines) { - case PropertyType::String: - out << ' ' << Utility::ConvertToUTF8(property.GetString()) << '\n'; - break; - case PropertyType::Boolean: - out << ' ' << (property.GetBoolean() ? Utility::LocIndView("true") : Utility::LocIndView("false")) << '\n'; - break; - case PropertyType::Int64: - out << ' ' << property.GetInt64() << '\n'; - break; - default: - out << " [Debug:PropertyType="_liv << property.Type() << "]\n"_liv; - break; + bool truncated = false; + auto lines = Utility::SplitIntoLines(input); + + if (maxLines == 1 && lines.size() > 1) + { + // If the limit was one line, don't allow line breaks but do allow a second line of overflow + lines.resize(1); + maxLines = 2; + truncated = true; + } + + if (Utility::LimitOutputLines(lines, GetConsoleWidth(), maxLines)) + { + truncated = true; + } + + if (truncated) + { + ++ValuesTruncated; + } + + return Utility::LocIndString{ Utility::Join("\n", lines) }; } - } - void OutputValueSet(OutputStream& out, const ValueSet& valueSet, size_t indent); + Utility::LocIndString ConvertForOutput(const winrt::hstring& input, size_t maxLines) + { + return ConvertForOutput(Utility::ConvertToUTF8(input), maxLines); + } - void OutputValueSetAsArray(OutputStream& out, const ValueSet& valueSetArray, size_t indent) - { - Utility::LocIndString indentString{ std::string(indent, ' ') }; + Utility::LocIndString ConvertIdentifier(const winrt::hstring& input) + { + return ConvertForOutput(input, 1); + } + + Utility::LocIndString ConvertURI(const winrt::hstring& input) + { + return ConvertForOutput(input, 1); + } - std::vector> arrayValues; - for (const auto& arrayValue : valueSetArray) + Utility::LocIndString ConvertValue(const winrt::hstring& input) { - if (arrayValue.Key() != L"treatAsArray") + return ConvertForOutput(input, 5); + } + + Utility::LocIndString ConvertDetailsIdentifier(const winrt::hstring& input) + { + return ConvertForOutput(Utility::ConvertControlCodesToPictures(Utility::ConvertToUTF8(input)), 1); + } + + Utility::LocIndString ConvertDetailsURI(const winrt::hstring& input) + { + return ConvertForOutput(Utility::ConvertControlCodesToPictures(Utility::ConvertToUTF8(input)), 1); + } + + Utility::LocIndString ConvertDetailsValue(const winrt::hstring& input) + { + return ConvertForOutput(Utility::ConvertControlCodesToPictures(Utility::ConvertToUTF8(input)), 5); + } + + void OutputValueWithTruncationWarningIfNeeded(const winrt::hstring& input) + { + size_t truncatedBefore = ValuesTruncated; + m_context.Reporter.Info() << ConvertValue(input) << '\n'; + + if (ValuesTruncated > truncatedBefore) { - arrayValues.emplace_back(std::make_pair(std::stoi(arrayValue.Key().c_str()), arrayValue.Value())); + m_context.Reporter.Warn() << Resource::String::ConfigurationWarningValueTruncated << std::endl; } } - std::sort( - arrayValues.begin(), - arrayValues.end(), - [](const std::pair& a, const std::pair& b) + void OutputPropertyValue(const IPropertyValue property) + { + switch (property.Type()) { - return a.first < b.first; - }); + case PropertyType::String: + m_context.Reporter.Info() << ' '; + OutputValueWithTruncationWarningIfNeeded(property.GetString()); + break; + case PropertyType::Boolean: + m_context.Reporter.Info() << ' ' << (property.GetBoolean() ? Utility::LocIndView("true") : Utility::LocIndView("false")) << '\n'; + break; + case PropertyType::Int64: + m_context.Reporter.Info() << ' ' << property.GetInt64() << '\n'; + break; + default: + m_context.Reporter.Info() << " [Debug:PropertyType="_liv << property.Type() << "]\n"_liv; + break; + } + } - for (const auto& arrayValue : arrayValues) + void OutputValueSetAsArray(const ValueSet& valueSetArray, size_t indent) { - auto arrayObject = arrayValue.second; - IPropertyValue arrayProperty = arrayObject.try_as(); + Utility::LocIndString indentString{ std::string(indent, ' ') }; - out << indentString << "-"; - if (arrayProperty) + std::vector> arrayValues; + for (const auto& arrayValue : valueSetArray) { - OutputPropertyValue(out, arrayProperty); + if (arrayValue.Key() != L"treatAsArray") + { + arrayValues.emplace_back(std::make_pair(std::stoi(arrayValue.Key().c_str()), arrayValue.Value())); + } } - else - { - ValueSet arraySubset = arrayObject.as(); - auto size = arraySubset.Size(); - if (size > 0) + + std::sort( + arrayValues.begin(), + arrayValues.end(), + [](const std::pair& a, const std::pair& b) { - // First one is special. - auto first = arraySubset.First().Current(); - out << ' ' << Utility::ConvertToUTF8(first.Key()) << ':'; - - auto object = first.Value(); - IPropertyValue property = object.try_as(); - if (property) - { - OutputPropertyValue(out, property); - } - else - { - // If not an IPropertyValue, it must be a ValueSet - ValueSet subset = object.as(); - out << '\n'; - OutputValueSet(out, subset, indent + 4); - } + return a.first < b.first; + }); + + for (const auto& arrayValue : arrayValues) + { + auto arrayObject = arrayValue.second; + IPropertyValue arrayProperty = arrayObject.try_as(); - if (size > 1) + m_context.Reporter.Info() << indentString << "-"; + if (arrayProperty) + { + OutputPropertyValue(arrayProperty); + } + else + { + ValueSet arraySubset = arrayObject.as(); + auto size = arraySubset.Size(); + if (size > 0) { - arraySubset.Remove(first.Key()); - OutputValueSet(out, arraySubset, indent + 2); - arraySubset.Insert(first.Key(), first.Value()); + // First one is special. + auto first = arraySubset.First().Current(); + m_context.Reporter.Info() << ' ' << ConvertIdentifier(first.Key()) << ':'; + + auto object = first.Value(); + IPropertyValue property = object.try_as(); + if (property) + { + OutputPropertyValue(property); + } + else + { + // If not an IPropertyValue, it must be a ValueSet + ValueSet subset = object.as(); + m_context.Reporter.Info() << '\n'; + OutputValueSet(subset, indent + 4); + } + + if (size > 1) + { + arraySubset.Remove(first.Key()); + OutputValueSet(arraySubset, indent + 2); + arraySubset.Insert(first.Key(), first.Value()); + } } } } } - } - - void OutputValueSet(OutputStream& out, const ValueSet& valueSet, size_t indent) - { - Utility::LocIndString indentString{ std::string(indent, ' ') }; - for (const auto& value : valueSet) + void OutputValueSet(const ValueSet& valueSet, size_t indent) { - out << indentString << Utility::ConvertToUTF8(value.Key()) << ':'; + Utility::LocIndString indentString{ std::string(indent, ' ') }; - auto object = value.Value(); - - IPropertyValue property = object.try_as(); - if (property) + for (const auto& value : valueSet) { - OutputPropertyValue(out, property); - } - else - { - // If not an IPropertyValue, it must be a ValueSet - ValueSet subset = object.as(); - out << '\n'; - if (subset.HasKey(L"treatAsArray")) + m_context.Reporter.Info() << indentString << ConvertIdentifier(value.Key()) << ':'; + + auto object = value.Value(); + + IPropertyValue property = object.try_as(); + if (property) { - OutputValueSetAsArray(out, subset, indent + 2); + OutputPropertyValue(property); } else { - OutputValueSet(out, subset, indent + 2); + // If not an IPropertyValue, it must be a ValueSet + ValueSet subset = object.as(); + m_context.Reporter.Info() << '\n'; + if (subset.HasKey(L"treatAsArray")) + { + OutputValueSetAsArray(subset, indent + 2); + } + else + { + OutputValueSet(subset, indent + 2); + } } } } - } - - // Converts a string from the configuration API surface for output. - // All strings coming from the API are external data and not localizable by us. - Utility::LocIndString ConvertForOutput(const winrt::hstring& input) - { - return Utility::LocIndString{ Utility::ConvertToUTF8(input) }; - } - void OutputConfigurationUnitHeader(OutputStream& out, const ConfigurationUnit& unit, const winrt::hstring& name) - { - out << ConfigurationIntentEmphasis << ToResource(unit.Intent()) << " :: "_liv << ConfigurationUnitEmphasis << ConvertForOutput(name); - - winrt::hstring identifier = unit.Identifier(); - if (!identifier.empty()) + void OutputConfigurationUnitHeader(const ConfigurationUnit& unit, const winrt::hstring& name) { - out << " ["_liv << ConvertForOutput(identifier) << ']'; - } + m_context.Reporter.Info() << ConfigurationIntentEmphasis << ToResource(unit.Intent()) << " :: "_liv << ConfigurationUnitEmphasis << ConvertIdentifier(name); - out << '\n'; - } + winrt::hstring identifier = unit.Identifier(); + if (!identifier.empty()) + { + m_context.Reporter.Info() << " ["_liv << ConvertIdentifier(identifier) << ']'; + } - void OutputConfigurationUnitInformation(OutputStream& out, const ConfigurationUnit& unit) - { - IConfigurationUnitProcessorDetails details = unit.Details(); - ValueSet metadata = unit.Metadata(); + m_context.Reporter.Info() << '\n'; + } - if (details) + void OutputConfigurationUnitInformation(const ConfigurationUnit& unit) { - // -- Sample output when IConfigurationUnitProcessorDetails present -- - // Intent :: UnitType [Identifier] - // UnitDocumentationUri - // Description - // "Module": ModuleName "by" Author / Publisher (IsLocal / ModuleSource) - // "Signed by": SigningCertificateChain (leaf subject CN) - // PublishedModuleUri / ModuleDocumentationUri - // ModuleDescription - OutputConfigurationUnitHeader(out, unit, details.UnitType()); - - auto unitDocumentationUri = details.UnitDocumentationUri(); - if (unitDocumentationUri) - { - out << " "_liv << ConvertForOutput(unitDocumentationUri.DisplayUri()) << '\n'; - } + IConfigurationUnitProcessorDetails details = unit.Details(); + ValueSet metadata = unit.Metadata(); - winrt::hstring unitDescriptionFromDetails = details.UnitDescription(); - if (!unitDescriptionFromDetails.empty()) + if (details) { - out << " "_liv << ConvertForOutput(unitDescriptionFromDetails) << '\n'; - } - else - { - auto unitDescriptionFromDirectives = GetValueSetString(metadata, s_Directive_Description); - if (unitDescriptionFromDirectives && !unitDescriptionFromDirectives.value().empty()) + // -- Sample output when IConfigurationUnitProcessorDetails present -- + // Intent :: UnitType [Identifier] + // UnitDocumentationUri + // Description + // "Module": ModuleName "by" Author / Publisher (IsLocal / ModuleSource) + // "Signed by": SigningCertificateChain (leaf subject CN) + // PublishedModuleUri / ModuleDocumentationUri + // ModuleDescription + OutputConfigurationUnitHeader(unit, details.UnitType()); + + auto unitDocumentationUri = details.UnitDocumentationUri(); + if (unitDocumentationUri) { - out << " "_liv << unitDescriptionFromDirectives.value() << '\n'; + m_context.Reporter.Info() << " "_liv << ConvertDetailsURI(unitDocumentationUri.DisplayUri()) << '\n'; } - } - auto author = ConvertForOutput(details.Author()); - if (author.empty()) - { - author = ConvertForOutput(details.Publisher()); - } - if (details.IsLocal()) - { - out << " "_liv << Resource::String::ConfigurationModuleWithDetails(ConvertForOutput(details.ModuleName()), author, Resource::String::ConfigurationLocal) << '\n'; - } - else - { - out << " "_liv << Resource::String::ConfigurationModuleWithDetails(ConvertForOutput(details.ModuleName()), author, ConvertForOutput(details.ModuleSource())) << '\n'; - } + winrt::hstring unitDescriptionFromDetails = details.UnitDescription(); + if (!unitDescriptionFromDetails.empty()) + { + m_context.Reporter.Info() << " "_liv << ConvertDetailsValue(unitDescriptionFromDetails) << '\n'; + } + else + { + auto unitDescriptionFromDirectives = GetValueSetString(metadata, s_Directive_Description); + if (!unitDescriptionFromDirectives.empty()) + { + m_context.Reporter.Info() << " "_liv; + OutputValueWithTruncationWarningIfNeeded(unitDescriptionFromDirectives); + } + } + + auto author = ConvertDetailsIdentifier(details.Author()); + if (author.empty()) + { + author = ConvertDetailsIdentifier(details.Publisher()); + } + if (details.IsLocal()) + { + m_context.Reporter.Info() << " "_liv << Resource::String::ConfigurationModuleWithDetails(ConvertDetailsIdentifier(details.ModuleName()), author, Resource::String::ConfigurationLocal) << '\n'; + } + else + { + m_context.Reporter.Info() << " "_liv << Resource::String::ConfigurationModuleWithDetails(ConvertDetailsIdentifier(details.ModuleName()), author, ConvertDetailsIdentifier(details.ModuleSource())) << '\n'; + } - // TODO: Currently the signature information is only for the top files. Maybe each item should be tagged? - // TODO: Output signing information with additional details (like whether the certificate is trusted). Doing this with the validate command - // seems like a good time, as that will also need to do the check in order to inform the user on the validation. - // Just saying "Signed By: Foo" is going to lead to a false sense of trust if the signature is valid but not actually trusted. + // TODO: Currently the signature information is only for the top files. Maybe each item should be tagged? + // TODO: Output signing information with additional details (like whether the certificate is trusted). Doing this with the validate command + // seems like a good time, as that will also need to do the check in order to inform the user on the validation. + // Just saying "Signed By: Foo" is going to lead to a false sense of trust if the signature is valid but not actually trusted. - auto moduleUri = details.PublishedModuleUri(); - if (!moduleUri) - { - moduleUri = details.ModuleDocumentationUri(); - } - if (moduleUri) - { - out << " "_liv << ConvertForOutput(moduleUri.DisplayUri()) << '\n'; - } + auto moduleUri = details.PublishedModuleUri(); + if (!moduleUri) + { + moduleUri = details.ModuleDocumentationUri(); + } + if (moduleUri) + { + m_context.Reporter.Info() << " "_liv << ConvertDetailsURI(moduleUri.DisplayUri()) << '\n'; + } - winrt::hstring moduleDescription = details.ModuleDescription(); - if (!moduleDescription.empty()) - { - out << " "_liv << ConvertForOutput(moduleDescription) << '\n'; + winrt::hstring moduleDescription = details.ModuleDescription(); + if (!moduleDescription.empty()) + { + m_context.Reporter.Info() << " "_liv << ConvertDetailsValue(moduleDescription) << '\n'; + } } - } - else - { - // -- Sample output when no IConfigurationUnitProcessorDetails present -- - // Intent :: Type [identifier] - // Description (from directives) - // "Module": module - OutputConfigurationUnitHeader(out, unit, unit.Type()); - - auto description = GetValueSetString(metadata, s_Directive_Description); - if (description && !description.value().empty()) + else { - out << " "_liv << description.value() << '\n'; + // -- Sample output when no IConfigurationUnitProcessorDetails present -- + // Intent :: Type [identifier] + // Description (from directives) + // "Module": module + OutputConfigurationUnitHeader(unit, unit.Type()); + + auto description = GetValueSetString(metadata, s_Directive_Description); + if (!description.empty()) + { + m_context.Reporter.Info() << " "_liv; + OutputValueWithTruncationWarningIfNeeded(description); + } + + auto module = GetValueSetString(metadata, s_Directive_Module); + if (!module.empty()) + { + m_context.Reporter.Info() << " "_liv << Resource::String::ConfigurationModuleNameOnly(ConvertIdentifier(module)) << '\n'; + } } - auto module = GetValueSetString(metadata, s_Directive_Module); - if (module && !module.value().empty()) + // -- Sample output footer -- + // Dependencies: dep1, dep2, ... + // Settings: + // <... settings splat> + auto dependencies = unit.Dependencies(); + if (dependencies.Size() > 0) { - out << " "_liv << Resource::String::ConfigurationModuleNameOnly(module.value()) << '\n'; + std::ostringstream allDependencies; + for (const winrt::hstring& dependency : dependencies) + { + allDependencies << ' ' << ConvertIdentifier(dependency); + } + m_context.Reporter.Info() << " "_liv << Resource::String::ConfigurationDependencies(Utility::LocIndString{ std::move(allDependencies).str() }) << '\n'; } - } - // -- Sample output footer -- - // Dependencies: dep1, dep2, ... - // Settings: - // <... settings splat> - auto dependencies = unit.Dependencies(); - if (dependencies.Size() > 0) - { - std::ostringstream allDependencies; - for (const winrt::hstring& dependency : dependencies) + ValueSet settings = unit.Settings(); + if (settings.Size() > 0) { - allDependencies << ' ' << Utility::ConvertToUTF8(dependency); + m_context.Reporter.Info() << " "_liv << Resource::String::ConfigurationSettings << '\n'; + OutputValueSet(settings, 4); } - out << " "_liv << Resource::String::ConfigurationDependencies(Utility::LocIndString{ std::move(allDependencies).str() }) << '\n'; } - ValueSet settings = unit.Settings(); - if (settings.Size() > 0) - { - out << " "_liv << Resource::String::ConfigurationSettings << '\n'; - OutputValueSet(out, settings, 4); - } + private: + Execution::Context& m_context; + }; + + void OutputConfigurationUnitHeader(Execution::Context& context, const ConfigurationUnit& unit, const winrt::hstring& name) + { + OutputHelper helper{ context }; + helper.OutputConfigurationUnitHeader(unit, name); } void LogFailedGetConfigurationUnitDetails(const ConfigurationUnit& unit, const IConfigurationUnitResultInformation& resultInformation) @@ -701,8 +786,7 @@ namespace AppInstaller::CLI::Workflow { m_unitsSeen.insert(unitInstance); - OutputStream out = m_context.Reporter.Info(); - OutputConfigurationUnitHeader(out, unit, unit.Details() ? unit.Details().UnitType() : unit.Type()); + OutputConfigurationUnitHeader(m_context, unit, unit.Details() ? unit.Details().UnitType() : unit.Type()); } } @@ -758,10 +842,7 @@ namespace AppInstaller::CLI::Workflow EndProgress(); - { - OutputStream info = m_context.Reporter.Info(); - OutputConfigurationUnitHeader(info, unit, unit.Details() ? unit.Details().UnitType() : unit.Type()); - } + OutputConfigurationUnitHeader(m_context, unit, unit.Details() ? unit.Details().UnitType() : unit.Type()); switch (testResult) { @@ -988,7 +1069,7 @@ namespace AppInstaller::CLI::Workflow auto getDetailsOperation = configContext.Processor().GetSetDetailsAsync(configContext.Set(), ConfigurationUnitDetailFlags::ReadOnly); auto unification = CreateProgressCancellationUnification(std::move(progressScope), getDetailsOperation); - OutputStream out = context.Reporter.Info(); + OutputHelper outputHelper{ context }; uint32_t unitsShown = 0; getDetailsOperation.Progress([&](const IAsyncOperationWithProgress& operation, const GetConfigurationUnitDetailsResult&) @@ -1002,7 +1083,7 @@ namespace AppInstaller::CLI::Workflow { GetConfigurationUnitDetailsResult unitResult = unitResults.GetAt(unitsShown); LogFailedGetConfigurationUnitDetails(unitResult.Unit(), unitResult.ResultInformation()); - OutputConfigurationUnitInformation(out, unitResult.Unit()); + outputHelper.OutputConfigurationUnitInformation(unitResult.Unit()); } progressScope = context.Reporter.BeginAsyncProgress(true); @@ -1047,7 +1128,7 @@ namespace AppInstaller::CLI::Workflow { GetConfigurationUnitDetailsResult unitResult = unitResults.GetAt(unitsShown); LogFailedGetConfigurationUnitDetails(unitResult.Unit(), unitResult.ResultInformation()); - OutputConfigurationUnitInformation(out, unitResult.Unit()); + outputHelper.OutputConfigurationUnitInformation(unitResult.Unit()); } } } @@ -1057,7 +1138,13 @@ namespace AppInstaller::CLI::Workflow for (unitsShown; unitsShown < allUnits.Size(); ++unitsShown) { ConfigurationUnit unit = allUnits.GetAt(unitsShown); - OutputConfigurationUnitInformation(out, unit); + outputHelper.OutputConfigurationUnitInformation(unit); + } + + if (outputHelper.ValuesTruncated) + { + // Using error to make this stand out from other warnings + context.Reporter.Error() << Resource::String::ConfigurationWarningSetViewTruncated << std::endl; } } @@ -1183,8 +1270,7 @@ namespace AppInstaller::CLI::Workflow { ConfigurationUnit unit = unitResult.Unit(); - auto out = context.Reporter.Info(); - OutputConfigurationUnitHeader(out, unit, unit.Type()); + OutputConfigurationUnitHeader(context, unit, unit.Type()); switch (resultCode) { @@ -1314,14 +1400,13 @@ namespace AppInstaller::CLI::Workflow { if (needsHeader) { - auto out = context.Reporter.Info(); - OutputConfigurationUnitHeader(out, unit, unit.Type()); + OutputConfigurationUnitHeader(context, unit, unit.Type()); needsHeader = false; foundIssue = true; } }; - if (GetValueSetString(unit.Metadata(), s_Directive_Module).value_or(Utility::LocIndString{}).empty()) + if (GetValueSetString(unit.Metadata(), s_Directive_Module).empty()) { outputHeaderIfNeeded(); context.Reporter.Warn() << " "_liv << Resource::String::ConfigurationUnitModuleNotProvidedWarning << std::endl; diff --git a/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs index 115fa83d24..66fdc60571 100644 --- a/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs +++ b/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs @@ -120,5 +120,18 @@ public void ShowDetailsFromHttpsConfigurationFile() Assert.AreEqual(0, result.ExitCode); Assert.True(result.StdOut.Contains(Constants.TestRepoName)); } + + /// + /// This test ensures that there is not significant overflow from large strings in the configuration file. + /// + [Test] + public void ShowTruncatedDetailsAndFileContent() + { + var result = TestCommon.RunAICLICommand("configure show", $"{TestCommon.GetTestDataFile("Configuration\\LargeContentStrings.yml")} --verbose"); + Assert.AreEqual(0, result.ExitCode); + Assert.True(result.StdOut.Contains("")); + Assert.True(result.StdOut.Contains("Some of the data present in the configuration file was truncated for this output; inspect the file contents for the complete content.")); + Assert.False(result.StdOut.Contains("Line5")); + } } } diff --git a/src/AppInstallerCLIE2ETests/TestData/Configuration/LargeContentStrings.yml b/src/AppInstallerCLIE2ETests/TestData/Configuration/LargeContentStrings.yml new file mode 100644 index 0000000000..a5971d708d --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/Configuration/LargeContentStrings.yml @@ -0,0 +1,22 @@ +properties: + configurationVersion: 0.2 + resources: + - resource: xE2EMalicious/E2EMalicious + id: firstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirst + directives: + repository: AppInstallerCLIE2ETestsRepo + description: "Line1\nLine2\nLine3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3\nLine4\nLine5\nLine6" + settings: + key: Foo + description: "Line1\nLine2\nLine3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3\nLine4\nLine5\nLine6" + - resource: Unknown + dependsOn: + - firstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirst + directives: + module: UnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknownUnknown + repository: AppInstallerCLIE2ETestsRepo + description: "Line1\nLine2\nLine3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3\nLine4\nLine5\nLine6" + settings: + Path: ${WinGetConfigRoot}\ConfigServerUnexpectedExit.txt + Content: Contents! + description: "Line1\nLine2\nLine3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3Line3\nLine4\nLine5\nLine6" diff --git a/src/AppInstallerCLIE2ETests/TestData/Configuration/Modules/xE2EMalicious/xE2EMalicious.psd1 b/src/AppInstallerCLIE2ETests/TestData/Configuration/Modules/xE2EMalicious/xE2EMalicious.psd1 new file mode 100644 index 0000000000..fcf009be10 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/Configuration/Modules/xE2EMalicious/xE2EMalicious.psd1 @@ -0,0 +1,33 @@ +# +# Module manifest for module 'xE2ETestResource' +# + +@{ + +RootModule = 'xE2EMalicious.psm1' +ModuleVersion = '0.0.0.1' +GUID = 'a0be43e8-ac22-4244-8efc-7263dfa50b92' +CompatiblePSEditions = 'Core' +Author = "WinGet Dev Team" +CompanyName = 'Microsoft Corporation' +Copyright = '(c) Microsoft Corporation. All rights reserved.' +Description = "PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests | PowerShell module with DSC resources for unit tests" +PowerShellVersion = '7.2' +FunctionsToExport = @() +CmdletsToExport = @() +DscResourcesToExport = @( + 'E2EMalicious' +) +HelpInfoURI = 'https://www.contoso.com/help' + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + ProjectUri = 'https://github.com/microsoft/winget-cli' + IconUri = 'https://www.contoso.com/icons/icon.png' + } + +} + +} diff --git a/src/AppInstallerCLIE2ETests/TestData/Configuration/Modules/xE2EMalicious/xE2EMalicious.psm1 b/src/AppInstallerCLIE2ETests/TestData/Configuration/Modules/xE2EMalicious/xE2EMalicious.psm1 new file mode 100644 index 0000000000..603827fb55 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/Configuration/Modules/xE2EMalicious/xE2EMalicious.psm1 @@ -0,0 +1,86 @@ +# E2E module with resources. + +enum Ensure +{ + Absent + Present +} + +# This resource just checks if a file is there or not with and if its with the specified content. +[DscResource()] +class E2EMalicious +{ + [DscProperty(Key)] + [string] $Path + + [DscProperty()] + [Ensure] $Ensure = [Ensure]::Present + + [DscProperty()] + [string] $Content = $null + + [E2EFileResource] Get() + { + if ([string]::IsNullOrEmpty($this.Path)) + { + throw + } + + $fileContent = $null + if (Test-Path -Path $this.Path -PathType Leaf) + { + $fileContent = Get-Content $this.Path -Raw + } + + $result = @{ + Path = $this.Path + Content = $fileContent + } + + return $result + } + + [bool] Test() + { + $get = $this.Get() + + if (Test-Path -Path $this.Path -PathType Leaf) + { + if ($this.Ensure -eq [Ensure]::Present) + { + return $this.Content -eq $get.Content + } + } + elseif ($this.Ensure -eq [Ensure]::Absent) + { + return $true + } + + return $false + } + + [void] Set() + { + if (-not $this.Test()) + { + if (Test-Path -Path $this.Path -PathType Leaf) + { + if ($this.Ensure -eq [Ensure]::Present) + { + Set-Content $this.Path $this.Content -NoNewline + } + else + { + Remove-Item $this.Path + } + } + else + { + if ($this.Ensure -eq [Ensure]::Present) + { + Set-Content $this.Path $this.Content -NoNewline + } + } + } + } +} diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 694700499b..593c83143d 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -2889,4 +2889,11 @@ Please specify one of them using the --source option to proceed. The MSStore package could not be found. + + Some of the data present in the configuration file was truncated for this output; inspect the file contents for the complete content. + + + <this value has been truncated; inspect the file contents for the complete text> + Keep some form of separator like the "<>" around the text so that it stands out from the preceding text. + \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index a6cc60b68c..3318666700 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -325,6 +325,7 @@ + @@ -929,40 +930,34 @@ true - true - Document + true - true - Document + true - true - Document + true - true - Document + true - true - Document + true - true - Document + true - true - Document + true - true - Document + true - true - Document + true + + + true diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters index 02fd1e2f62..e63749ea8b 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -347,6 +347,9 @@ Source Files\Common + + Source Files\Common + @@ -978,5 +981,8 @@ TestData + + TestData + \ No newline at end of file diff --git a/src/AppInstallerCLITests/Strings.cpp b/src/AppInstallerCLITests/Strings.cpp index a28b45451a..68214f4370 100644 --- a/src/AppInstallerCLITests/Strings.cpp +++ b/src/AppInstallerCLITests/Strings.cpp @@ -249,7 +249,7 @@ TEST_CASE("Format", "[strings]") REQUIRE("First {1}" == Format("{0} {1}", "First")); } -TEST_CASE("SplitIntoLines", "[string]") +TEST_CASE("SplitIntoLines", "[strings]") { REQUIRE(SplitIntoLines("Boring test") == std::vector{ "Boring test" }); REQUIRE(SplitIntoLines( @@ -261,7 +261,7 @@ TEST_CASE("SplitIntoLines", "[string]") == std::vector{ "You want my treasure?", "You can have it!", "I left everything I gathered in one place!", "You just have to find it!" }); } -TEST_CASE("SplitWithSeparator", "[string]") +TEST_CASE("SplitWithSeparator", "[strings]") { std::vector test1 = Split("first;second;third", ';'); REQUIRE(test1.size() == 3); @@ -285,10 +285,40 @@ TEST_CASE("SplitWithSeparator", "[string]") REQUIRE(test4[1] == "spaces"); } -TEST_CASE("ConvertGuid", "[string]") +TEST_CASE("ConvertGuid", "[strings]") { std::string validGuidString = "{4d1e55b2-f16f-11cf-88cb-001111000030}"; GUID guid = { 0x4d1e55b2, 0xf16f, 0x11cf, 0x88, 0xcb, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 }; REQUIRE(CaseInsensitiveEquals(ConvertGuidToString(guid), validGuidString)); } + +TEST_CASE("FindControlCodeToConvert", "[strings]") +{ + REQUIRE(FindControlCodeToConvert("No codes") == std::string::npos); + REQUIRE(FindControlCodeToConvert("Allowed codes: \t\r\n") == std::string::npos); + REQUIRE(FindControlCodeToConvert("\x1bSkipped code", 1) == std::string::npos); + + REQUIRE(FindControlCodeToConvert("\x1bUnskipped code") == 0); + REQUIRE(FindControlCodeToConvert("Escape code: \x1b") == 13); + + std::string_view allCodes{ "\x0\x1\x2\x3\x4\x5\x6\x7\x8\xb\xc\xe\xf\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f"sv }; + for (size_t i = 0; i < allCodes.length(); ++i) + { + REQUIRE(FindControlCodeToConvert(allCodes, i) == i); + } +} + +TEST_CASE("ConvertControlCodesToPictures", "[strings]") +{ + REQUIRE(ConvertControlCodesToPictures("No codes") == "No codes"); + REQUIRE(ConvertControlCodesToPictures("Allowed codes: \t\r\n") == "Allowed codes: \t\r\n"); + + REQUIRE(ConvertControlCodesToPictures("\x1b Code First") == ConvertToUTF8(L"\x241b Code First")); + REQUIRE(ConvertControlCodesToPictures("Escape code: \x1b") == ConvertToUTF8(L"Escape code: \x241b")); + + std::string_view allCodes{ "\x0\x1\x2\x3\x4\x5\x6\x7\x8\xb\xc\xe\xf\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f"sv }; + std::wstring_view allPictures{ L"\x2400\x2401\x2402\x2403\x2404\x2405\x2406\x2407\x2408\x240b\x240c\x240e\x240f\x2410\x2411\x2412\x2413\x2414\x2415\x2416\x2417\x2418\x2419\x241a\x241b\x241c\x241d\x241e\x241f\x2421"sv }; + + REQUIRE(ConvertControlCodesToPictures(allCodes) == ConvertToUTF8(allPictures)); +} diff --git a/src/AppInstallerCLITests/TestData/ContainsEscapeControlCode.yaml b/src/AppInstallerCLITests/TestData/ContainsEscapeControlCode.yaml new file mode 100644 index 0000000000..7dee1859d0 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/ContainsEscapeControlCode.yaml @@ -0,0 +1 @@ +key: "This is the ESCAPE control code: \x1b" diff --git a/src/AppInstallerCLITests/Yaml.cpp b/src/AppInstallerCLITests/Yaml.cpp new file mode 100644 index 0000000000..47edbb8e16 --- /dev/null +++ b/src/AppInstallerCLITests/Yaml.cpp @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include +#include + +using namespace TestCommon; +using namespace AppInstaller::Utility; +using namespace AppInstaller::YAML; + + +TEST_CASE("YamlParserTypes", "[YAML]") +{ + auto document = AppInstaller::YAML::Load(TestDataFile("Node-Types.yaml")); + + auto intUnquoted = document["IntegerUnquoted"]; + CHECK(intUnquoted.GetTagType() == Node::TagType::Int); + + auto intSingleQuoted = document["IntegerSingleQuoted"]; + CHECK(intSingleQuoted.GetTagType() == Node::TagType::Str); + + auto intDoubleQuoted = document["IntegerDoubleQuoted"]; + CHECK(intDoubleQuoted.GetTagType() == Node::TagType::Str); + + auto boolTrue = document["BooleanTrue"]; + CHECK(boolTrue.GetTagType() == Node::TagType::Bool); + + auto strTrue = document["StringTrue"]; + CHECK(strTrue.GetTagType() == Node::TagType::Str); + + auto boolFalse = document["BooleanFalse"]; + CHECK(boolFalse.GetTagType() == Node::TagType::Bool); + + auto strFalse = document["StringFalse"]; + CHECK(strFalse.GetTagType() == Node::TagType::Str); + + auto localTag = document["LocalTag"]; + CHECK(localTag.GetTagType() == Node::TagType::Unknown); +} + +TEST_CASE("YamlMergeMappingNode", "[YAML]") +{ + auto document = Load(TestDataFile("Node-Mapping.yaml")); + + auto mergeNode = document["MergeNode"]; + auto mergeNode2 = document["MergeNode2"]; + + REQUIRE(3 == mergeNode.size()); + REQUIRE(2 == mergeNode2.size()); + + mergeNode.MergeMappingNode(mergeNode2); + + REQUIRE(5 == mergeNode.size()); +} + +TEST_CASE("YamlMergeMappingNode_CaseInsensitive", "[YAML]") +{ + auto document = Load(TestDataFile("Node-Mapping.yaml")); + + auto mergeNode = document["MergeNode"]; + auto mergeNode2 = document["MergeNode2"]; + + REQUIRE(3 == mergeNode.size()); + REQUIRE(2 == mergeNode2.size()); + + mergeNode.MergeMappingNode(mergeNode2, true); + + REQUIRE(4 == mergeNode.size()); +} + +TEST_CASE("YamlMergeSequenceNode", "[YAML]") +{ + auto document = Load(TestDataFile("Node-Merge.yaml")); + auto document2 = Load(TestDataFile("Node-Merge2.yaml")); + + REQUIRE(3 == document["StrawHats"].size()); + REQUIRE(2 == document2["StrawHats"].size()); + + // Internally will call MergeMappingNode. + document["StrawHats"].MergeSequenceNode(document2["StrawHats"], "Bounty"); + REQUIRE(5 == document["StrawHats"].size()); +} + +TEST_CASE("YamlMergeSequenceNode_CaseInsensitive", "[YAML]") +{ + auto document = Load(TestDataFile("Node-Merge.yaml")); + auto document2 = Load(TestDataFile("Node-Merge2.yaml")); + + REQUIRE(3 == document["StrawHats"].size()); + REQUIRE(2 == document2["StrawHats"].size()); + + // Internally will call MergeMappingNode. + document["StrawHats"].MergeSequenceNode(document2["StrawHats"], "Name", true); + REQUIRE(4 == document["StrawHats"].size()); + + auto luffy = std::find_if( + document["StrawHats"].Sequence().begin(), + document["StrawHats"].Sequence().end(), + [](auto const& n) { return n["Name"].as() == "Monkey D Luffy"; }); + REQUIRE(luffy != document["StrawHats"].Sequence().end()); + + // From original node + REQUIRE((*luffy)["Bounty"].as() == "3,000,000,000"); + + // From merged node + REQUIRE((*luffy)["Fruit"].as() == "Gomu Gomu no Mi"); +} + +TEST_CASE("YamlMergeNode_MergeSequenceNoKey", "[YAML]") +{ + auto document = Load(TestDataFile("Node-Merge.yaml")); + auto document2 = Load(TestDataFile("Node-Merge2.yaml")); + + REQUIRE_THROWS_HR(document["StrawHats"].MergeSequenceNode(document2["StrawHats"], "Power"), APPINSTALLER_CLI_ERROR_YAML_INVALID_DATA); +} + +TEST_CASE("YamlMappingNode", "[YAML]") +{ + auto document = Load(TestDataFile("Node-Mapping.yaml")); + + auto node = document["key"]; + REQUIRE(node.as() == "value"); + + auto node2 = document.GetChildNode("KEY"); + REQUIRE(node2.as() == "value"); + + auto node3 = document.GetChildNode("key"); + REQUIRE(node3.as() == "value"); + + auto node4 = document.GetChildNode("kEy"); + REQUIRE(node4.as() == "value"); + + auto node5 = document.GetChildNode("fake"); + REQUIRE(node5.IsNull()); + + auto node6 = document["repeatedkey"]; + REQUIRE(node6.as() == "repeated value"); + REQUIRE_THROWS_HR(document.GetChildNode("repeatedkey"), APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); + + REQUIRE_THROWS_HR(document.GetChildNode("RepeatedKey"), APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); + REQUIRE_THROWS_HR(document["RepeatedKey"], APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); +} + +TEST_CASE("YamlMappingNode_const", "[YAML]") +{ + const auto document = Load(TestDataFile("Node-Mapping.yaml")); + + auto node = document["key"]; + REQUIRE(node.as() == "value"); + + auto node2 = document.GetChildNode("KEY"); + REQUIRE(node2.as() == "value"); + + auto node3 = document.GetChildNode("key"); + REQUIRE(node3.as() == "value"); + + auto node4 = document.GetChildNode("kEy"); + REQUIRE(node4.as() == "value"); + + auto node5 = document.GetChildNode("fake"); + REQUIRE(node5.IsNull()); + + auto node6 = document["repeatedkey"]; + REQUIRE(node6.as() == "repeated value"); + REQUIRE_THROWS_HR(document.GetChildNode("repeatedkey"), APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); + + REQUIRE_THROWS_HR(document.GetChildNode("RepeatedKey"), APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); + REQUIRE_THROWS_HR(document["RepeatedKey"], APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); +} + +TEST_CASE("YamlContainsEscapeControlCode", "[YAML]") +{ + REQUIRE_THROWS_HR(Load(TestDataFile("ContainsEscapeControlCode.yaml")), APPINSTALLER_CLI_ERROR_LIBYAML_ERROR); +} diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp index 4efc06cf7a..3938d20724 100644 --- a/src/AppInstallerCLITests/YamlManifest.cpp +++ b/src/AppInstallerCLITests/YamlManifest.cpp @@ -1789,162 +1789,3 @@ TEST_CASE("ShadowManifest_NotVerifiedPublisher", "[ShadowManifest]") TempFile mergedManifestFile{ "merged.yaml" }; REQUIRE_THROWS_MATCHES(YamlParser::CreateFromPath(multiFileDirectory, validateOption, mergedManifestFile), ManifestException, ManifestExceptionMatcher("Field usage requires verified publishers. [Icons]")); } - -TEST_CASE("YamlParserTypes", "[YAML]") -{ - auto document = AppInstaller::YAML::Load(TestDataFile("Node-Types.yaml")); - - auto intUnquoted = document["IntegerUnquoted"]; - CHECK(intUnquoted.GetTagType() == Node::TagType::Int); - - auto intSingleQuoted = document["IntegerSingleQuoted"]; - CHECK(intSingleQuoted.GetTagType() == Node::TagType::Str); - - auto intDoubleQuoted = document["IntegerDoubleQuoted"]; - CHECK(intDoubleQuoted.GetTagType() == Node::TagType::Str); - - auto boolTrue = document["BooleanTrue"]; - CHECK(boolTrue.GetTagType() == Node::TagType::Bool); - - auto strTrue = document["StringTrue"]; - CHECK(strTrue.GetTagType() == Node::TagType::Str); - - auto boolFalse = document["BooleanFalse"]; - CHECK(boolFalse.GetTagType() == Node::TagType::Bool); - - auto strFalse = document["StringFalse"]; - CHECK(strFalse.GetTagType() == Node::TagType::Str); - - auto localTag = document["LocalTag"]; - CHECK(localTag.GetTagType() == Node::TagType::Unknown); -} - -TEST_CASE("YamlMergeMappingNode", "[YAML]") -{ - auto document = Load(TestDataFile("Node-Mapping.yaml")); - - auto mergeNode = document["MergeNode"]; - auto mergeNode2 = document["MergeNode2"]; - - REQUIRE(3 == mergeNode.size()); - REQUIRE(2 == mergeNode2.size()); - - mergeNode.MergeMappingNode(mergeNode2); - - REQUIRE(5 == mergeNode.size()); -} - -TEST_CASE("YamlMergeMappingNode_CaseInsensitive", "[YAML]") -{ - auto document = Load(TestDataFile("Node-Mapping.yaml")); - - auto mergeNode = document["MergeNode"]; - auto mergeNode2 = document["MergeNode2"]; - - REQUIRE(3 == mergeNode.size()); - REQUIRE(2 == mergeNode2.size()); - - mergeNode.MergeMappingNode(mergeNode2, true); - - REQUIRE(4 == mergeNode.size()); -} - -TEST_CASE("YamlMergeSequenceNode", "[YAML]") -{ - auto document = Load(TestDataFile("Node-Merge.yaml")); - auto document2 = Load(TestDataFile("Node-Merge2.yaml")); - - REQUIRE(3 == document["StrawHats"].size()); - REQUIRE(2 == document2["StrawHats"].size()); - - // Internally will call MergeMappingNode. - document["StrawHats"].MergeSequenceNode(document2["StrawHats"], "Bounty"); - REQUIRE(5 == document["StrawHats"].size()); -} - -TEST_CASE("YamlMergeSequenceNode_CaseInsensitive", "[YAML]") -{ - auto document = Load(TestDataFile("Node-Merge.yaml")); - auto document2 = Load(TestDataFile("Node-Merge2.yaml")); - - REQUIRE(3 == document["StrawHats"].size()); - REQUIRE(2 == document2["StrawHats"].size()); - - // Internally will call MergeMappingNode. - document["StrawHats"].MergeSequenceNode(document2["StrawHats"], "Name", true); - REQUIRE(4 == document["StrawHats"].size()); - - auto luffy = std::find_if( - document["StrawHats"].Sequence().begin(), - document["StrawHats"].Sequence().end(), - [](auto const& n) { return n["Name"].as() == "Monkey D Luffy"; }); - REQUIRE(luffy != document["StrawHats"].Sequence().end()); - - // From original node - REQUIRE((*luffy)["Bounty"].as() == "3,000,000,000"); - - // From merged node - REQUIRE((*luffy)["Fruit"].as() == "Gomu Gomu no Mi"); -} - -TEST_CASE("YamlMergeNode_MergeSequenceNoKey", "[YAML]") -{ - auto document = Load(TestDataFile("Node-Merge.yaml")); - auto document2 = Load(TestDataFile("Node-Merge2.yaml")); - - REQUIRE_THROWS_HR(document["StrawHats"].MergeSequenceNode(document2["StrawHats"], "Power"), APPINSTALLER_CLI_ERROR_YAML_INVALID_DATA); -} - -TEST_CASE("YamlMappingNode", "[YAML]") -{ - auto document = Load(TestDataFile("Node-Mapping.yaml")); - - auto node = document["key"]; - REQUIRE(node.as() == "value"); - - auto node2 = document.GetChildNode("KEY"); - REQUIRE(node2.as() == "value"); - - auto node3 = document.GetChildNode("key"); - REQUIRE(node3.as() == "value"); - - auto node4 = document.GetChildNode("kEy"); - REQUIRE(node4.as() == "value"); - - auto node5 = document.GetChildNode("fake"); - REQUIRE(node5.IsNull()); - - auto node6 = document["repeatedkey"]; - REQUIRE(node6.as() == "repeated value"); - REQUIRE_THROWS_HR(document.GetChildNode("repeatedkey"), APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); - - REQUIRE_THROWS_HR(document.GetChildNode("RepeatedKey"), APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); - REQUIRE_THROWS_HR(document["RepeatedKey"], APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); -} - -TEST_CASE("YamlMappingNode_const", "[YAML]") -{ - const auto document = Load(TestDataFile("Node-Mapping.yaml")); - - auto node = document["key"]; - REQUIRE(node.as() == "value"); - - auto node2 = document.GetChildNode("KEY"); - REQUIRE(node2.as() == "value"); - - auto node3 = document.GetChildNode("key"); - REQUIRE(node3.as() == "value"); - - auto node4 = document.GetChildNode("kEy"); - REQUIRE(node4.as() == "value"); - - auto node5 = document.GetChildNode("fake"); - REQUIRE(node5.IsNull()); - - auto node6 = document["repeatedkey"]; - REQUIRE(node6.as() == "repeated value"); - REQUIRE_THROWS_HR(document.GetChildNode("repeatedkey"), APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); - - REQUIRE_THROWS_HR(document.GetChildNode("RepeatedKey"), APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); - REQUIRE_THROWS_HR(document["RepeatedKey"], APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); -} diff --git a/src/AppInstallerSharedLib/AppInstallerStrings.cpp b/src/AppInstallerSharedLib/AppInstallerStrings.cpp index bf6e7075be..df1cf35496 100644 --- a/src/AppInstallerSharedLib/AppInstallerStrings.cpp +++ b/src/AppInstallerSharedLib/AppInstallerStrings.cpp @@ -762,9 +762,11 @@ namespace AppInstaller::Utility // If not, round up to the next line count (by rounding down through integer division after subtracting 1 + 1). size_t currentLineActualLineCount = (currentLineWidth ? (currentLineWidth - 1) / lineWidth : 0) + 1; - // The current line may be too big to be the last line. + // The current line may be too big to be the last line, or it may be just the right size but we will end up trimming + // additional lines. In either case, append an ellipsis to indicate that we trimmed the value. size_t availableLines = maximum - totalLines; - if (currentLineActualLineCount > availableLines) + if (currentLineActualLineCount > availableLines || + (currentLineActualLineCount == availableLines && currentLine != lines.size() - 1)) { size_t actualWidth = 0; std::string trimmedLine = UTF8TrimRightToColumnWidth(lines[currentLine], (availableLines * lineWidth) - 1, actualWidth); @@ -827,7 +829,8 @@ namespace AppInstaller::Utility return result; } - LocIndString Join(LocIndView separator, const std::vector& vector) + template + static std::string JoinInternal(std::string_view separator, const std::vector& vector) { auto vectorSize = vector.size(); if (vectorSize == 0) @@ -841,7 +844,17 @@ namespace AppInstaller::Utility { ssJoin << separator << vector[i]; } - return LocIndString{ ssJoin.str() }; + return ssJoin.str(); + } + + LocIndString Join(LocIndView separator, const std::vector& vector) + { + return LocIndString{ JoinInternal(separator, vector) }; + } + + std::string Join(std::string_view separator, const std::vector& vector) + { + return JoinInternal(separator, vector); } std::vector Split(const std::string& input, char separator, bool trim) @@ -909,4 +922,74 @@ namespace AppInstaller::Utility return false; } } + + size_t FindControlCodeToConvert(std::string_view input, size_t offset) + { + size_t nextControl = offset; + while (nextControl < input.size()) + { + char currentChar = input[nextControl]; + + // Convert all low controls except tab, line feed and carriage return + if (currentChar >= 0 && currentChar < 0x20 && + currentChar != '\t' && + currentChar != '\n' && + currentChar != '\r') + { + break; + } + + // Convert the Delete control + if (currentChar == 0x7F) + { + break; + } + + ++nextControl; + } + + return nextControl < input.size() ? nextControl : std::string::npos; + } + + std::string ConvertControlCodesToPictures(std::string_view input) + { + std::string result; + size_t pos = 0; + + while (pos < input.size()) + { + size_t nextControl = FindControlCodeToConvert(input, pos); + + if (nextControl == std::string::npos) + { + // No more control codes found + result += input.substr(pos); + break; + } + else + { + result += input.substr(pos, nextControl - pos); + + char currentChar = input[nextControl]; + + if (currentChar >= 0 && currentChar < 0x20) + { + // ASCII 0x00 - 0x1F => UTF-8 0x2400 - 0x241F + // Then manually converted to UTF-8 since only the last character is affected + result += '\xE2'; + result += '\x90'; + result += ('\x80' + currentChar); + } + else if (currentChar == 0x7F) + { + // UTF-8 for control picture of DELETE + result += "\xE2\x90\xA1"; + } + + pos = nextControl + 1; + } + } + + return result; + } } diff --git a/src/AppInstallerSharedLib/Public/AppInstallerStrings.h b/src/AppInstallerSharedLib/Public/AppInstallerStrings.h index 83d78477e0..b2386801fb 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerStrings.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerStrings.h @@ -254,6 +254,9 @@ namespace AppInstaller::Utility // Join a string vector using the provided separator. LocIndString Join(LocIndView separator, const std::vector& vector); + // Join a string vector using the provided separator. + std::string Join(std::string_view separator, const std::vector& vector); + // Splits the string using the provided separator. Entries can also be trimmed. std::vector Split(const std::string& input, char separator, bool trim = false); @@ -278,4 +281,12 @@ namespace AppInstaller::Utility // Converts the input string to a DWORD value using std::stoul and returns a boolean value based on the resulting DWORD value. bool IsDwordFlagSet(const std::string& value); + + // Finds the next control code index that would be replaced. + // Returns std::string::npos if not found. + size_t FindControlCodeToConvert(std::string_view input, size_t offset = 0); + + // Converts most control codes in the input to their corresponding control picture in the output. + // Exempts tab, line feed, and carriage return from being replaced. + std::string ConvertControlCodesToPictures(std::string_view input); } diff --git a/src/AppInstallerSharedLib/Public/winget/Yaml.h b/src/AppInstallerSharedLib/Public/winget/Yaml.h index 83836a342b..3774941df5 100644 --- a/src/AppInstallerSharedLib/Public/winget/Yaml.h +++ b/src/AppInstallerSharedLib/Public/winget/Yaml.h @@ -38,7 +38,8 @@ namespace AppInstaller::YAML Parser, Composer, Writer, - Emitter + Emitter, + Policy, }; // Should only be used for Memory. diff --git a/src/AppInstallerSharedLib/Yaml.cpp b/src/AppInstallerSharedLib/Yaml.cpp index e2596629db..4125c31059 100644 --- a/src/AppInstallerSharedLib/Yaml.cpp +++ b/src/AppInstallerSharedLib/Yaml.cpp @@ -45,6 +45,8 @@ namespace AppInstaller::YAML return "Writer"sv; case Exception::Type::Emitter: return "Emitter"sv; + case Exception::Type::Policy: + return "Policy"sv; } return "Unknown"sv; diff --git a/src/AppInstallerSharedLib/YamlWrapper.cpp b/src/AppInstallerSharedLib/YamlWrapper.cpp index a367ab942c..812082e3f0 100644 --- a/src/AppInstallerSharedLib/YamlWrapper.cpp +++ b/src/AppInstallerSharedLib/YamlWrapper.cpp @@ -53,26 +53,36 @@ namespace AppInstaller::YAML::Wrapper THROW_HR(E_UNEXPECTED); } - std::string ConvertYamlString(yaml_char_t* string, size_t length = std::string::npos) + Mark ConvertMark(const yaml_mark_t& mark) { + return { mark.line + 1, mark.column + 1 }; + } + + std::string ConvertYamlString(yaml_char_t* string, const yaml_mark_t& mark, size_t length = std::string::npos) + { + std::string_view resultView; + if (length == std::string::npos) { - return { reinterpret_cast(string) }; + resultView = { reinterpret_cast(string) }; } else { - return { reinterpret_cast(string), length }; + resultView = { reinterpret_cast(string), length }; } - } - std::string ConvertScalarToString(yaml_node_t* node) - { - return ConvertYamlString(node->data.scalar.value, node->data.scalar.length); + size_t invalidCharacter = Utility::FindControlCodeToConvert(resultView); + if (invalidCharacter != std::string::npos) + { + THROW_EXCEPTION(Exception(Exception::Type::Policy, "unsupported control character", ConvertMark(mark))); + } + + return std::string{ resultView }; } - Mark ConvertMark(const yaml_mark_t& mark) + std::string ConvertScalarToString(yaml_node_t* node, const yaml_mark_t& mark) { - return { mark.line + 1, mark.column + 1 }; + return ConvertYamlString(node->data.scalar.value, mark, node->data.scalar.length); } } @@ -115,7 +125,7 @@ namespace AppInstaller::YAML::Wrapper return {}; } - Node result(ConvertNodeType(root->type), ConvertYamlString(root->tag), ConvertMark(root->start_mark)); + Node result(ConvertNodeType(root->type), ConvertYamlString(root->tag, root->start_mark), ConvertMark(root->start_mark)); struct StackItem { @@ -142,7 +152,7 @@ namespace AppInstaller::YAML::Wrapper break; case YAML_SCALAR_NODE: stackItem.node->SetScalar( - ConvertScalarToString(stackItem.yamlNode), + ConvertScalarToString(stackItem.yamlNode, stackItem.yamlNode->start_mark), stackItem.yamlNode->data.scalar.style == YAML_SINGLE_QUOTED_SCALAR_STYLE || stackItem.yamlNode->data.scalar.style == YAML_DOUBLE_QUOTED_SCALAR_STYLE); pop = true; @@ -153,7 +163,7 @@ namespace AppInstaller::YAML::Wrapper if (child < stackItem.yamlNode->data.sequence.items.top) { yaml_node_t* childYamlNode = GetNode(*child); - Node& childNode = stackItem.node->AddSequenceNode(ConvertNodeType(childYamlNode->type), ConvertYamlString(childYamlNode->tag), ConvertMark(childYamlNode->start_mark)); + Node& childNode = stackItem.node->AddSequenceNode(ConvertNodeType(childYamlNode->type), ConvertYamlString(childYamlNode->tag, childYamlNode->start_mark), ConvertMark(childYamlNode->start_mark)); resultStack.emplace(childYamlNode, &childNode); } else @@ -171,12 +181,12 @@ namespace AppInstaller::YAML::Wrapper yaml_node_t* keyYamlNode = GetNode(child->key); THROW_HR_IF(APPINSTALLER_CLI_ERROR_YAML_INVALID_MAPPING_KEY, keyYamlNode->type != YAML_SCALAR_NODE); - Node keyNode(ConvertNodeType(keyYamlNode->type), ConvertYamlString(keyYamlNode->tag), ConvertMark(keyYamlNode->start_mark)); - keyNode.SetScalar(ConvertScalarToString(keyYamlNode)); + Node keyNode(ConvertNodeType(keyYamlNode->type), ConvertYamlString(keyYamlNode->tag, keyYamlNode->start_mark), ConvertMark(keyYamlNode->start_mark)); + keyNode.SetScalar(ConvertScalarToString(keyYamlNode, keyYamlNode->start_mark)); yaml_node_t* valueYamlNode = GetNode(child->value); - Node& childNode = stackItem.node->AddMappingNode(std::move(keyNode), ConvertNodeType(valueYamlNode->type), ConvertYamlString(valueYamlNode->tag), ConvertMark(valueYamlNode->start_mark)); + Node& childNode = stackItem.node->AddMappingNode(std::move(keyNode), ConvertNodeType(valueYamlNode->type), ConvertYamlString(valueYamlNode->tag, valueYamlNode->start_mark), ConvertMark(valueYamlNode->start_mark)); resultStack.emplace(valueYamlNode, &childNode); } else From 5a1631facc9b419d5ae96bd00dfafccc62f4bdeb Mon Sep 17 00:00:00 2001 From: Ruben Guerrero Date: Thu, 2 May 2024 13:36:13 -0700 Subject: [PATCH 5/8] Configure export command (#4434) This PR introduces the `configure export` command as an exprimental feature. This is mostly a proof of concept and should not be considered a full feature. #### Scenario A - Create a configuration to install a winget package. Use `--pacakgeId` with the package identifier of an application in the winget source to produce a configuration file that uses the `Microsoft.WinGet.DSC/WinGetPackage` resource to install the package via winget. Right now, we don't validate if the package id exists and will just copy what the user sets into the settings of the resource. #### Scenario B - 'Export' the configuration from the specified resource. Use both `--module` and `--resource` to get the configuration of a resource and add it to the configuration file. Internally, configuration will install the module (if not installed already) and call Get on the resource. We will then try to serialize its property into the configuration file. Current limitation is that if a resource has a required setting (like WinGetPackage required Id) it won't work. If the resource is not found in the gallery a retry will be performed allowing prereleased modules in the case it exists. #### Scenario C - Mix of A and B If `--packageId`, `--module` and `--resource` are used, configure export will produce two resources. The first one is the `WinGetPackage` for the specified package. The second one is the same as in B, with the difference that it includes a dependency of the previously created `WinGetPackage` resource. #### Scenario D - Configuration file already exists. If the file passed to the `--output` parameters already exists and is a valid configuration file, the resources will be appended. There is currently not validation into the correctness of this, so it can result in a configuration with resources with the same id. For example `winget configure export --packageId Microsoft.AppInstaller --module Microsoft.WinGet.DSC --resource WinGetUserSettings -o test_export.yml` would produce the following file ``` # Created using winget configure export 1.8.0-preview # yaml-language-server: $schema=https://aka.ms/configuration-dsc-schema/0.2 properties: configurationVersion: 0.2 resources: - resource: Microsoft.WinGet.DSC/WinGetPackage id: Microsoft.AppInstaller directives: description: Install Microsoft.AppInstaller allowPrerelease: true settings: id: Microsoft.AppInstaller source: winget - resource: Microsoft.WinGet.DSC/WinGetUserSettings dependsOn: - Microsoft.AppInstaller directives: description: Configure Microsoft.AppInstaller settings: Settings: experimentalFeatures: configureSelfElevate: true installBehavior: preferences: locale: - en-US - fr-FR $schema: https://aka.ms/winget-settings.schema.json Action: Full ``` ###### Microsoft Reviewers: [Open in CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/microsoft/winget-cli/pull/4434) --- .github/actions/spelling/expect.txt | 2 + doc/Settings.md | 17 +- .../package-manager/winget/returnCodes.md | 4 + .../JSON/settings/settings.schema.0.2.json | 5 + .../AppInstallerCLICore.vcxproj | 2 + .../AppInstallerCLICore.vcxproj.filters | 6 + src/AppInstallerCLICore/Argument.cpp | 10 +- src/AppInstallerCLICore/Command.h | 2 + .../Commands/ConfigureCommand.cpp | 3 +- .../Commands/ConfigureShowCommand.cpp | 2 +- .../Commands/ConfigureTestCommand.cpp | 1 - .../Commands/ConfigureValidateCommand.cpp | 1 - .../ConfigureExportCommand.cpp | 64 +++ .../ConfigureExportCommand.h | 23 + src/AppInstallerCLICore/ExecutionArgs.h | 11 +- src/AppInstallerCLICore/Resources.h | 16 +- .../Workflows/ConfigurationFlow.cpp | 458 +++++++++++++----- .../Workflows/ConfigurationFlow.h | 18 + src/AppInstallerCLIE2ETests/Constants.cs | 1 + .../Shared/Strings/en-us/winget.resw | 44 ++ .../ExperimentalFeature.cpp | 8 +- .../Public/winget/ExperimentalFeature.h | 3 +- .../Public/winget/UserSettings.h | 2 + src/AppInstallerCommonCore/UserSettings.cpp | 1 + src/AppInstallerSharedLib/Errors.cpp | 1 + .../Public/AppInstallerErrors.h | 1 + .../Exceptions/ErrorCodes.cs | 7 +- .../UnitPropertyUnsupportedException.cs | 51 ++ .../Extensions/HashtableExtensions.cs | 54 +++ .../Helpers/TypeHelpers.cs | 76 ++- .../Helpers/Errors.cs | 1 + .../Tests/HashtableExtensionsTests.cs | 134 +++++ .../Tests/OpenConfigurationSetTests.cs | 14 +- .../Tests/TypeHelpersTests.cs | 73 ++- .../ConfigurationSetSerializer.cpp | 95 +++- .../ConfigurationSetSerializer.h | 7 +- .../ConfigurationSetSerializer_0_2.cpp | 34 +- .../ConfigurationSetSerializer_0_2.h | 4 + ...t.Management.Configuration.vcxproj.filters | 8 +- 39 files changed, 1106 insertions(+), 158 deletions(-) create mode 100644 src/AppInstallerCLICore/ConfigureExportCommand.cpp create mode 100644 src/AppInstallerCLICore/ConfigureExportCommand.h create mode 100644 src/Microsoft.Management.Configuration.Processor/Exceptions/UnitPropertyUnsupportedException.cs create mode 100644 src/Microsoft.Management.Configuration.Processor/Extensions/HashtableExtensions.cs create mode 100644 src/Microsoft.Management.Configuration.UnitTests/Tests/HashtableExtensionsTests.cs diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 0a84af9093..a14f858ea1 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -161,6 +161,7 @@ GRPICONDIR GRPICONDIRENTRY guiddef Hackathon +hashtables helplib helplibrary hhx @@ -217,6 +218,7 @@ JToken JValue Kaido KNOWNFOLDERID +kool ktf ldcase learnxinyminutes diff --git a/doc/Settings.md b/doc/Settings.md index f4dbe70bb0..f58469e0bc 100644 --- a/doc/Settings.md +++ b/doc/Settings.md @@ -332,11 +332,11 @@ Currently, this means that properly attributed configuration units (and only tho "experimentalFeatures": { "configureSelfElevate": true }, -``` - +``` + ### storeDownload -This feature enables packages to be downloaded from the Microsoft Store. +This feature enables packages to be downloaded from the Microsoft Store. You can enable the feature as shown below. ```json @@ -344,3 +344,14 @@ You can enable the feature as shown below. "storeDownload": true }, ``` + +### configureExport + +This feature enables exporting a configuration file. +You can enable the feature as shown below. + +```json + "experimentalFeatures": { + "configureExport": true + }, +``` diff --git a/doc/windows/package-manager/winget/returnCodes.md b/doc/windows/package-manager/winget/returnCodes.md index 64ac247ed7..aee9dd17fb 100644 --- a/doc/windows/package-manager/winget/returnCodes.md +++ b/doc/windows/package-manager/winget/returnCodes.md @@ -195,6 +195,9 @@ Installation failed. Restart your PC then try again. | | 0x8A15C00C | -1978286068 | WINGET_CONFIG_ERROR_SET_DEPENDENCY_CYCLE | The dependency graph contains a cycle which cannot be resolved. | | 0x8A15C00D | -1978286067 | WINGET_CONFIG_ERROR_INVALID_FIELD_VALUE | The configuration has an invalid field value. | | 0x8A15C00E | -1978286066 | WINGET_CONFIG_ERROR_MISSING_FIELD | The configuration is missing a field. | +| 0x8A15C00F | -1978286065 | WINGET_CONFIG_ERROR_TEST_FAILED | Some of the configuration units failed while testing their state. | +| 0x8A15C010 | -1978286064 | WINGET_CONFIG_ERROR_TEST_NOT_RUN | Configuration state was not tested. | +| 0x8A15C011 | -1978286063 | WINGET_CONFIG_ERROR_GET_FAILED | The configuration unit failed getting its properties. | ## Configuration Processor Errors @@ -211,3 +214,4 @@ Installation failed. Restart your PC then try again. | | 0x8A15C109 | -1978285815 | WINGET_CONFIG_ERROR_UNIT_INVOKE_INVALID_RESULT | The configuration unit returned an unexpected result during execution. | | 0x8A15C110 | -1978285814 | WINGET_CONFIG_ERROR_UNIT_SETTING_CONFIG_ROOT | A unit contains a setting that requires the config root. | | 0x8A15C111 | -1978285813 | WINGET_CONFIG_ERROR_UNIT_IMPORT_MODULE_ADMIN | Loading the module for the configuration unit failed because it requires administrator privileges to run. | +| 0x8A15C112 | -1978285812 | WINGET_CONFIG_ERROR_NOT_SUPPORTED_BY_PROCESSOR | Operation is not supported by the configuration processor. | diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json index 0f800e9e73..8f8ce786da 100644 --- a/schemas/JSON/settings/settings.schema.0.2.json +++ b/schemas/JSON/settings/settings.schema.0.2.json @@ -290,6 +290,11 @@ "description": "Enable support for downloading packages from the Microsoft Store", "type": "boolean", "default": false + }, + "configureExport": { + "description": "Enable support for the configure export command", + "type": "boolean", + "default": false } } } diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj index 2f4ab13de0..c7e62a40c4 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -384,6 +384,7 @@ + @@ -446,6 +447,7 @@ + diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters index dde62a5482..a6a7ab1e4b 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -251,6 +251,9 @@ Header Files + + Commands + @@ -472,6 +475,9 @@ Source Files + + Commands + diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp index 33bead9d3e..2a194edee2 100644 --- a/src/AppInstallerCLICore/Argument.cpp +++ b/src/AppInstallerCLICore/Argument.cpp @@ -141,8 +141,6 @@ namespace AppInstaller::CLI return { type, "position"_liv }; // Export Command - case Execution::Args::Type::OutputFile: - return { type, "output"_liv, 'o' }; case Execution::Args::Type::IncludeVersions: return { type, "include-versions"_liv }; @@ -211,6 +209,12 @@ namespace AppInstaller::CLI return { type, "disable"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::StubType }; case Execution::Args::Type::ConfigurationModulePath: return { type, "module-path"_liv }; + case Execution::Args::Type::ConfigurationExportPackageId: + return { type, "package-id"_liv }; + case Execution::Args::Type::ConfigurationExportModule: + return { type, "module"_liv }; + case Execution::Args::Type::ConfigurationExportResource: + return { type, "resource"_liv }; // Download command case Execution::Args::Type::DownloadDirectory: @@ -237,6 +241,8 @@ namespace AppInstaller::CLI return { type, "open-logs"_liv, "logs"_liv }; case Execution::Args::Type::Force: return { type, "force"_liv, ArgTypeCategory::CopyFlagToSubContext }; + case Execution::Args::Type::OutputFile: + return { type, "output"_liv, 'o' }; case Execution::Args::Type::DependencySource: return { type, "dependency-source"_liv, ArgTypeCategory::ExtendedSource }; diff --git a/src/AppInstallerCLICore/Command.h b/src/AppInstallerCLICore/Command.h index 15804ab86f..d1260967b7 100644 --- a/src/AppInstallerCLICore/Command.h +++ b/src/AppInstallerCLICore/Command.h @@ -57,6 +57,8 @@ namespace AppInstaller::CLI Command(name, {}, parent, Command::Visibility::Show, Settings::ExperimentalFeature::Feature::None, Settings::TogglePolicy::Policy::None, outputFlags) {} Command(std::string_view name, std::vector aliases, std::string_view parent, Command::Visibility visibility) : Command(name, aliases, parent, visibility, Settings::ExperimentalFeature::Feature::None) {} + Command(std::string_view name, std::string_view parent, Settings::ExperimentalFeature::Feature feature) : + Command(name, {}, parent, Command::Visibility::Show, feature) {} Command(std::string_view name, std::vector aliases, std::string_view parent, Settings::ExperimentalFeature::Feature feature) : Command(name, aliases, parent, Command::Visibility::Show, feature) {} Command(std::string_view name, std::vector aliases, std::string_view parent, Settings::TogglePolicy::Policy groupPolicy) : diff --git a/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp index 3b8f6daed6..93b83cbc8e 100644 --- a/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp @@ -5,6 +5,7 @@ #include "ConfigureShowCommand.h" #include "ConfigureTestCommand.h" #include "ConfigureValidateCommand.h" +#include "ConfigureExportCommand.h" #include "Workflows/ConfigurationFlow.h" #include "Workflows/MSStoreInstallerHandler.h" #include "ConfigurationCommon.h" @@ -25,6 +26,7 @@ namespace AppInstaller::CLI std::make_unique(FullName()), std::make_unique(FullName()), std::make_unique(FullName()), + std::make_unique(FullName()), }); } @@ -51,7 +53,6 @@ namespace AppInstaller::CLI Utility::LocIndView ConfigureCommand::HelpLink() const { - // TODO: Make this exist return "https://aka.ms/winget-command-configure"_liv; } diff --git a/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp index c575cc349a..3c25c0f4da 100644 --- a/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp @@ -30,13 +30,13 @@ namespace AppInstaller::CLI Utility::LocIndView ConfigureShowCommand::HelpLink() const { - // TODO: Make this exist return "https://aka.ms/winget-command-configure#show"_liv; } void ConfigureShowCommand::ExecuteInternal(Execution::Context& context) const { context << + VerifyIsFullPackage << VerifyFileOrUri(Execution::Args::Type::ConfigurationFile) << CreateConfigurationProcessor << OpenConfigurationSet << diff --git a/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp index bfc53f78a9..8ced8e83e9 100644 --- a/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp @@ -30,7 +30,6 @@ namespace AppInstaller::CLI Utility::LocIndView ConfigureTestCommand::HelpLink() const { - // TODO: Make this exist return "https://aka.ms/winget-command-configure#test"_liv; } diff --git a/src/AppInstallerCLICore/Commands/ConfigureValidateCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureValidateCommand.cpp index 024b450e7a..9adb7dd82a 100644 --- a/src/AppInstallerCLICore/Commands/ConfigureValidateCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ConfigureValidateCommand.cpp @@ -29,7 +29,6 @@ namespace AppInstaller::CLI Utility::LocIndView ConfigureValidateCommand::HelpLink() const { - // TODO: Make this exist return "https://aka.ms/winget-command-configure#validate"_liv; } diff --git a/src/AppInstallerCLICore/ConfigureExportCommand.cpp b/src/AppInstallerCLICore/ConfigureExportCommand.cpp new file mode 100644 index 0000000000..2a9fff3555 --- /dev/null +++ b/src/AppInstallerCLICore/ConfigureExportCommand.cpp @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "ConfigureExportCommand.h" +#include "Workflows/ConfigurationFlow.h" +#include "ConfigurationCommon.h" + +using namespace AppInstaller::CLI::Workflow; + +namespace AppInstaller::CLI +{ + std::vector ConfigureExportCommand::GetArguments() const + { + return { + Argument{ Execution::Args::Type::OutputFile, Resource::String::OutputFileArgumentDescription, true }, + Argument{ Execution::Args::Type::ConfigurationExportPackageId, Resource::String::ConfigureExportPackageId }, + Argument{ Execution::Args::Type::ConfigurationExportModule, Resource::String::ConfigureExportModule }, + Argument{ Execution::Args::Type::ConfigurationExportResource, Resource::String::ConfigureExportResource }, + Argument{ Execution::Args::Type::ConfigurationModulePath, Resource::String::ConfigurationModulePath }, + }; + } + + Resource::LocString ConfigureExportCommand::ShortDescription() const + { + return { Resource::String::ConfigureExportCommandShortDescription }; + } + + Resource::LocString ConfigureExportCommand::LongDescription() const + { + return { Resource::String::ConfigureExportCommandLongDescription }; + } + + Utility::LocIndView ConfigureExportCommand::HelpLink() const + { + return "https://aka.ms/winget-command-configure#export"_liv; + } + + void ConfigureExportCommand::ExecuteInternal(Execution::Context& context) const + { + context << + VerifyIsFullPackage << + CreateConfigurationProcessor << + CreateOrOpenConfigurationSet << + AddWinGetPackageAndResource << + WriteConfigFile; + } + + void ConfigureExportCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const + { + Configuration::ValidateCommonArguments(execArgs); + + bool validInputArgs = false; + if (execArgs.Contains(Execution::Args::Type::ConfigurationExportModule, Execution::Args::Type::ConfigurationExportResource) || + execArgs.Contains(Execution::Args::Type::ConfigurationExportPackageId)) + { + validInputArgs = true; + } + + if (!validInputArgs) + { + throw CommandException(Resource::String::ConfigureExportArgumentError); + } + } +} diff --git a/src/AppInstallerCLICore/ConfigureExportCommand.h b/src/AppInstallerCLICore/ConfigureExportCommand.h new file mode 100644 index 0000000000..a8a7895790 --- /dev/null +++ b/src/AppInstallerCLICore/ConfigureExportCommand.h @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Command.h" + +namespace AppInstaller::CLI +{ + struct ConfigureExportCommand final : public Command + { + ConfigureExportCommand(std::string_view parent) : Command("export", parent, Settings::ExperimentalFeature::Feature::ConfigureExport) {} + + std::vector GetArguments() const override; + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + Utility::LocIndView HelpLink() const override; + + protected: + void ExecuteInternal(Execution::Context& context) const override; + void ValidateArgumentsInternal(Execution::Args& execArgs) const override; + }; +} diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h index 430bebf92f..f7933fb62b 100644 --- a/src/AppInstallerCLICore/ExecutionArgs.h +++ b/src/AppInstallerCLICore/ExecutionArgs.h @@ -79,7 +79,6 @@ namespace AppInstaller::CLI::Execution Position, // Export Command - OutputFile, IncludeVersions, // Import Command @@ -126,6 +125,9 @@ namespace AppInstaller::CLI::Execution ConfigurationEnable, ConfigurationDisable, ConfigurationModulePath, + ConfigurationExportPackageId, + ConfigurationExportModule, + ConfigurationExportResource, // Common arguments NoVT, // Disable VirtualTerminal outputs @@ -138,6 +140,7 @@ namespace AppInstaller::CLI::Execution Wait, // Prompts the user to press any key before exiting OpenLogs, // Opens the default logs directory after executing the command Force, // Forces the execution of the workflow with non security related issues + OutputFile, DependencySource, // Index source to be queried against for finding dependencies CustomHeader, // Optional Rest source header @@ -159,7 +162,11 @@ namespace AppInstaller::CLI::Execution Max }; - bool Contains(Type arg) const { return (m_parsedArgs.count(arg) != 0); } + template), bool> = true> + bool Contains(T... arg) const + { + return (... && (m_parsedArgs.count(arg) != 0)); + } const std::vector* GetArgs(Type arg) const { diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index 0b554603ea..0d1090f897 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -67,6 +67,9 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationEnabledMessage); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationEnableMessage); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationEnablingMessage); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationExportAddingToFile); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationExportFailed); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationExportSuccessful); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFailedToApply); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFailedToGetDetails); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFailedToTest); @@ -79,6 +82,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFileInvalidYAML); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFileVersionUnknown); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationGettingDetails); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationGettingResourceSettings); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationInDesiredState); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationInform); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationInitializing); @@ -132,6 +136,14 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationWarningValueTruncated); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportArgumentError); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportModule); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportPackageId); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportResource); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportUnitDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportUnitInstallDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureShowCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureShowCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureTestCommandLongDescription); @@ -535,6 +547,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SourceArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(SourceExplicitArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceExportCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceExportCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceListAdditionalSource); @@ -543,12 +556,12 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SourceListCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceListCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceListData); + WINGET_DEFINE_RESOURCE_STRINGID(SourceListExplicit); WINGET_DEFINE_RESOURCE_STRINGID(SourceListField); WINGET_DEFINE_RESOURCE_STRINGID(SourceListIdentifier); WINGET_DEFINE_RESOURCE_STRINGID(SourceListName); WINGET_DEFINE_RESOURCE_STRINGID(SourceListNoneFound); WINGET_DEFINE_RESOURCE_STRINGID(SourceListNoSources); - WINGET_DEFINE_RESOURCE_STRINGID(SourceListExplicit); WINGET_DEFINE_RESOURCE_STRINGID(SourceListTrustLevel); WINGET_DEFINE_RESOURCE_STRINGID(SourceListType); WINGET_DEFINE_RESOURCE_STRINGID(SourceListUpdated); @@ -563,7 +576,6 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SourceRemoveCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceRemoveOne); WINGET_DEFINE_RESOURCE_STRINGID(SourceRequiresAuthentication); - WINGET_DEFINE_RESOURCE_STRINGID(SourceExplicitArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceResetAll); WINGET_DEFINE_RESOURCE_STRINGID(SourceResetCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceResetCommandShortDescription); diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp index d5f6d8db02..b46c8429ad 100644 --- a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp @@ -33,12 +33,21 @@ namespace AppInstaller::CLI::Workflow } #endif - namespace + namespace anon { constexpr std::wstring_view s_Directive_Description = L"description"; constexpr std::wstring_view s_Directive_Module = L"module"; constexpr std::wstring_view s_Directive_AllowPrerelease = L"allowPrerelease"; + constexpr std::wstring_view s_Unit_WinGetPackage = L"WinGetPackage"; + + constexpr std::wstring_view s_Module_WinGetClient = L"Microsoft.WinGet.DSC"; + + constexpr std::wstring_view s_Setting_Id = L"id"; + constexpr std::wstring_view s_Setting_Source = L"source"; + + constexpr std::wstring_view s_WinGetSource = L"winget"; + Logging::Level ConvertLevel(DiagnosticLevel level) { switch (level) @@ -918,6 +927,254 @@ namespace AppInstaller::CLI::Workflow return validationOrder; } + + void SetNameAndOrigin(ConfigurationSet& set, std::filesystem::path& absolutePath) + { + // TODO: Consider how to properly determine a good value for name and origin. + set.Name(absolutePath.filename().wstring()); + set.Origin(absolutePath.parent_path().wstring()); + set.Path(absolutePath.wstring()); + } + + void OpenConfigurationSet(Execution::Context& context, const std::string& argPath, bool allowRemote) + { + auto progressScope = context.Reporter.BeginAsyncProgress(true); + progressScope->Callback().SetProgressMessage(Resource::String::ConfigurationReadingConfigFile()); + + std::wstring argPathWide = Utility::ConvertToUTF16(argPath); + bool isRemote = Utility::IsUrlRemote(argPath); + std::filesystem::path absolutePath; + Streams::IInputStream inputStream = nullptr; + + if (isRemote) + { + if (!allowRemote) + { + AICLI_LOG(Config, Error, << "Remote files are not supported"); + AICLI_TERMINATE_CONTEXT(ERROR_NOT_SUPPORTED); + } + + std::ostringstream stringStream; + ProgressCallback emptyCallback; + Utility::DownloadToStream(argPath, stringStream, Utility::DownloadType::ConfigurationFile, emptyCallback); + + auto strContent = stringStream.str(); + std::vector byteContent{ strContent.begin(), strContent.end() }; + + Streams::InMemoryRandomAccessStream memoryStream; + Streams::DataWriter streamWriter{ memoryStream }; + streamWriter.WriteBytes(byteContent); + streamWriter.StoreAsync().get(); + streamWriter.DetachStream(); + memoryStream.Seek(0); + inputStream = memoryStream; + } + else + { + absolutePath = std::filesystem::weakly_canonical(std::filesystem::path{ argPathWide }); + auto openAction = Streams::FileRandomAccessStream::OpenAsync(absolutePath.wstring(), FileAccessMode::Read); + auto cancellationScope = progressScope->Callback().SetCancellationFunction([&]() { openAction.Cancel(); }); + inputStream = openAction.get(); + } + + OpenConfigurationSetResult openResult = nullptr; + { + auto openAction = context.Get().Processor().OpenConfigurationSetAsync(inputStream); + auto cancellationScope = progressScope->Callback().SetCancellationFunction([&]() { openAction.Cancel(); }); + openResult = openAction.get(); + } + + progressScope.reset(); + + if (FAILED_LOG(static_cast(openResult.ResultCode().value))) + { + AICLI_LOG(Config, Error, << "Failed to open configuration set at " << (isRemote ? argPath : absolutePath.u8string()) << " with error 0x" << Logging::SetHRFormat << static_cast(openResult.ResultCode().value)); + + switch (openResult.ResultCode()) + { + case WINGET_CONFIG_ERROR_INVALID_FIELD_TYPE: + context.Reporter.Error() << Resource::String::ConfigurationFieldInvalidType(Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Field()) }) << std::endl; + break; + case WINGET_CONFIG_ERROR_INVALID_FIELD_VALUE: + context.Reporter.Error() << Resource::String::ConfigurationFieldInvalidValue(Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Field()) }, Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Value()) }) << std::endl; + break; + case WINGET_CONFIG_ERROR_MISSING_FIELD: + context.Reporter.Error() << Resource::String::ConfigurationFieldMissing(Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Field()) }) << std::endl; + break; + case WINGET_CONFIG_ERROR_UNKNOWN_CONFIGURATION_FILE_VERSION: + context.Reporter.Error() << Resource::String::ConfigurationFileVersionUnknown(Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Value()) }) << std::endl; + break; + case WINGET_CONFIG_ERROR_INVALID_CONFIGURATION_FILE: + case WINGET_CONFIG_ERROR_INVALID_YAML: + default: + context.Reporter.Error() << Resource::String::ConfigurationFileInvalidYAML << std::endl; + break; + } + + if (openResult.Line() != 0) + { + context.Reporter.Error() << Resource::String::SeeLineAndColumn(openResult.Line(), openResult.Column()) << std::endl; + } + + AICLI_TERMINATE_CONTEXT(openResult.ResultCode()); + } + + ConfigurationSet result = openResult.Set(); + + // Temporary block on using schema 0.3 while experimental + if (result.SchemaVersion() == L"0.3") + { + AICLI_RETURN_IF_TERMINATED(context << EnsureFeatureEnabled(Settings::ExperimentalFeature::Feature::Configuration03)); + } + + // Fill out the information about the set based on it coming from a file. + if (isRemote) + { + result.Name(Utility::GetFileNameFromURI(argPath).wstring()); + result.Origin(argPathWide); + // Do not set path. This means ${WinGetConfigRoot} not supported in remote configs. + } + else + { + SetNameAndOrigin(result, absolutePath); + } + + context.Get().Set(result); + } + + std::optional CreateWinGetUnit(const Execution::Context& context) + { + if (context.Args.Contains(Execution::Args::Type::ConfigurationExportPackageId)) + { + // Maybe we can add some checks to validate the package id exists. + std::string packageId{ context.Args.GetArg(Args::Type::ConfigurationExportPackageId) }; + std::wstring packageIdWide = Utility::ConvertToUTF16(packageId); + + ConfigurationUnit unit; + unit.Type(s_Unit_WinGetPackage); + unit.Identifier(packageIdWide); + unit.Intent(ConfigurationUnitIntent::Apply); + + auto description = Resource::String::ConfigureExportUnitInstallDescription(Utility::LocIndView{ packageId }); + + ValueSet directives; + directives.Insert(s_Directive_Module, PropertyValue::CreateString(s_Module_WinGetClient)); + directives.Insert(s_Directive_Description, PropertyValue::CreateString(winrt::to_hstring(description.get()))); + directives.Insert(s_Directive_AllowPrerelease, PropertyValue::CreateBoolean(true)); + unit.Metadata(directives); + + ValueSet settings; + settings.Insert(s_Setting_Id, PropertyValue::CreateString(packageIdWide)); + settings.Insert(s_Setting_Source, PropertyValue::CreateString(s_WinGetSource)); + unit.Settings(settings); + + return unit; + } + + return {}; + } + + GetConfigurationUnitSettingsResult GetUnitSettings(Execution::Context& context, ConfigurationUnit& unit) + { + // This assumes there are no required properties for Get, but for example WinGetPackage requires the Id. + // It is obviously wrong and will be wrong until Export is implemented for DSC v2 and a proper way to inform + // about input to winget configure export is implemented. Drink the kool-aid and transcend. + unit.Intent(ConfigurationUnitIntent::Inform); + + auto progressScope = context.Reporter.BeginAsyncProgress(true); + + progressScope->Callback().SetProgressMessage(Resource::String::ConfigurationGettingResourceSettings()); + + GetConfigurationUnitSettingsResult getResult = nullptr; + { + auto getAction = context.Get().Processor().GetUnitSettingsAsync(unit); + auto cancellationScope = progressScope->Callback().SetCancellationFunction([&]() { getAction.Cancel(); }); + getResult = getAction.get(); + } + + progressScope.reset(); + return getResult; + } + + std::optional CreateConfigurationUnit(Execution::Context& context, const std::optional dependantUnit) + { + if (context.Args.Contains(Execution::Args::Type::ConfigurationExportModule, Execution::Args::Type::ConfigurationExportResource)) + { + std::string moduleName{ context.Args.GetArg(Args::Type::ConfigurationExportModule) }; + std::wstring moduleNameWide = Utility::ConvertToUTF16(moduleName); + + std::string resourceName{ context.Args.GetArg(Args::Type::ConfigurationExportResource) }; + std::wstring resourceNameWide = Utility::ConvertToUTF16(resourceName); + + ConfigurationUnit unit; + unit.Type(resourceNameWide); + + ValueSet directives; + directives.Insert(s_Directive_Module, PropertyValue::CreateString(moduleNameWide)); + + Utility::LocIndString description; + if (dependantUnit.has_value()) + { + description = Resource::String::ConfigureExportUnitDescription(Utility::LocIndView{ Utility::ConvertToUTF8(dependantUnit.value().Identifier()) }); + } + else + { + description = Resource::String::ConfigureExportUnitDescription(Utility::LocIndView{ resourceName }); + } + + directives.Insert(s_Directive_Description, PropertyValue::CreateString(winrt::to_hstring(description.get()))); + unit.Metadata(directives); + + // Call processor to get settings for the unit. + auto getResult = GetUnitSettings(context, unit); + winrt::hresult resultCode = getResult.ResultInformation().ResultCode(); + if (FAILED(resultCode)) + { + // Retry if it fails with not found in the case the module is a pre-released one. + bool isPreRelease = false; + if (resultCode == WINGET_CONFIG_ERROR_UNIT_NOT_FOUND_REPOSITORY) + { + directives.Insert(s_Directive_AllowPrerelease, PropertyValue::CreateBoolean(true)); + unit.Metadata(directives); + + auto preReleaseResult = GetUnitSettings(context, unit); + if (SUCCEEDED(preReleaseResult.ResultInformation().ResultCode())) + { + isPreRelease = true; + getResult = preReleaseResult; + } + else + { + AICLI_LOG(Config, Error, << "Failed Get allowing prerelease modules"); + LogFailedGetConfigurationUnitDetails(unit, preReleaseResult.ResultInformation()); + } + } + + if (!isPreRelease) + { + OutputUnitRunFailure(context, unit, getResult.ResultInformation()); + THROW_HR(WINGET_CONFIG_ERROR_GET_FAILED); + } + } + + unit.Settings(getResult.Settings()); + + // GetUnitSettings will set it to Inform. + unit.Intent(ConfigurationUnitIntent::Apply); + + // Add dependency if needed. + if (dependantUnit.has_value()) + { + auto dependencies = winrt::single_threaded_vector(); + dependencies.Append(dependantUnit.value().Identifier()); + unit.Dependencies(std::move(dependencies)); + } + + return unit; + } + + return {}; + } } void CreateConfigurationProcessor(Context& context) @@ -925,10 +1182,10 @@ namespace AppInstaller::CLI::Workflow auto progressScope = context.Reporter.BeginAsyncProgress(true); progressScope->Callback().SetProgressMessage(Resource::String::ConfigurationInitializing()); - ConfigurationProcessor processor{ CreateConfigurationSetProcessorFactory(context)}; + ConfigurationProcessor processor{ anon::CreateConfigurationSetProcessorFactory(context)}; // Set the processor to the current level of the logging. - processor.MinimumLevel(ConvertLevel(Logging::Log().GetLevel())); + processor.MinimumLevel(anon::ConvertLevel(Logging::Log().GetLevel())); processor.Caller(L"winget"); // Use same activity as the overall winget command processor.ActivityIdentifier(*Logging::Telemetry().GetActivityId()); @@ -938,7 +1195,7 @@ namespace AppInstaller::CLI::Workflow // Route the configuration diagnostics into the context's diagnostics logging processor.Diagnostics([&context](const winrt::Windows::Foundation::IInspectable&, const IDiagnosticInformation& diagnostics) { - context.GetThreadGlobals().GetDiagnosticLogger().Write(Logging::Channel::Config, ConvertLevel(diagnostics.Level()), Utility::ConvertToUTF8(diagnostics.Message())); + context.GetThreadGlobals().GetDiagnosticLogger().Write(Logging::Channel::Config, anon::ConvertLevel(diagnostics.Level()), Utility::ConvertToUTF8(diagnostics.Message())); }); ConfigurationContext configurationContext; @@ -949,106 +1206,30 @@ namespace AppInstaller::CLI::Workflow void OpenConfigurationSet(Context& context) { - auto progressScope = context.Reporter.BeginAsyncProgress(true); - progressScope->Callback().SetProgressMessage(Resource::String::ConfigurationReadingConfigFile()); - std::string argPath{ context.Args.GetArg(Args::Type::ConfigurationFile) }; - std::wstring argPathWide = Utility::ConvertToUTF16(argPath); - bool isRemote = Utility::IsUrlRemote(argPath); - std::filesystem::path absolutePath; - Streams::IInputStream inputStream = nullptr; + anon::OpenConfigurationSet(context, argPath, true); + } - if (isRemote) - { - std::ostringstream stringStream; - ProgressCallback emptyCallback; - Utility::DownloadToStream(argPath, stringStream, Utility::DownloadType::ConfigurationFile, emptyCallback); - - auto strContent = stringStream.str(); - std::vector byteContent{ strContent.begin(), strContent.end() }; - - Streams::InMemoryRandomAccessStream memoryStream; - Streams::DataWriter streamWriter{ memoryStream }; - streamWriter.WriteBytes(byteContent); - streamWriter.StoreAsync().get(); - streamWriter.DetachStream(); - memoryStream.Seek(0); - inputStream = memoryStream; - } - else - { - absolutePath = std::filesystem::weakly_canonical(std::filesystem::path{ argPathWide }); - auto openAction = Streams::FileRandomAccessStream::OpenAsync(absolutePath.wstring(), FileAccessMode::Read); - auto cancellationScope = progressScope->Callback().SetCancellationFunction([&]() { openAction.Cancel(); }); - inputStream = openAction.get(); - } + void CreateOrOpenConfigurationSet(Context& context) + { + std::string argPath{ context.Args.GetArg(Args::Type::OutputFile) }; - OpenConfigurationSetResult openResult = nullptr; + if (std::filesystem::exists(argPath)) { - auto openAction = context.Get().Processor().OpenConfigurationSetAsync(inputStream); - auto cancellationScope = progressScope->Callback().SetCancellationFunction([&]() { openAction.Cancel(); }); - openResult = openAction.get(); + anon::OpenConfigurationSet(context, argPath, false); } - - progressScope.reset(); - - if (FAILED_LOG(static_cast(openResult.ResultCode().value))) + else { - AICLI_LOG(Config, Error, << "Failed to open configuration set at " << (isRemote ? argPath : absolutePath.u8string()) << " with error 0x" << Logging::SetHRFormat << static_cast(openResult.ResultCode().value)); - - switch (openResult.ResultCode()) - { - case WINGET_CONFIG_ERROR_INVALID_FIELD_TYPE: - context.Reporter.Error() << Resource::String::ConfigurationFieldInvalidType(Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Field()) }) << std::endl; - break; - case WINGET_CONFIG_ERROR_INVALID_FIELD_VALUE: - context.Reporter.Error() << Resource::String::ConfigurationFieldInvalidValue(Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Field()) }, Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Value()) }) << std::endl; - break; - case WINGET_CONFIG_ERROR_MISSING_FIELD: - context.Reporter.Error() << Resource::String::ConfigurationFieldMissing(Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Field()) }) << std::endl; - break; - case WINGET_CONFIG_ERROR_UNKNOWN_CONFIGURATION_FILE_VERSION: - context.Reporter.Error() << Resource::String::ConfigurationFileVersionUnknown(Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Value()) }) << std::endl; - break; - case WINGET_CONFIG_ERROR_INVALID_CONFIGURATION_FILE: - case WINGET_CONFIG_ERROR_INVALID_YAML: - default: - context.Reporter.Error() << Resource::String::ConfigurationFileInvalidYAML << std::endl; - break; - } - - if (openResult.Line() != 0) - { - context.Reporter.Error() << Resource::String::SeeLineAndColumn(openResult.Line(), openResult.Column()) << std::endl; - } - - AICLI_TERMINATE_CONTEXT(openResult.ResultCode()); - } + // TODO: support other schema versions or pick up latest. + ConfigurationSet set; + set.SchemaVersion(L"0.2"); - ConfigurationSet result = openResult.Set(); + std::wstring argPathWide = Utility::ConvertToUTF16(argPath); + auto absolutePath = std::filesystem::weakly_canonical(std::filesystem::path{ argPathWide }); + anon::SetNameAndOrigin(set, absolutePath); - // Temporary block on using schema 0.3 while experimental - if (result.SchemaVersion() == L"0.3") - { - AICLI_RETURN_IF_TERMINATED(context << EnsureFeatureEnabled(Settings::ExperimentalFeature::Feature::Configuration03)); - } - - // Fill out the information about the set based on it coming from a file. - if (isRemote) - { - result.Name(Utility::GetFileNameFromURI(argPath).wstring()); - result.Origin(argPathWide); - // Do not set path. This means ${WinGetConfigRoot} not supported in remote configs. - } - else - { - // TODO: Consider how to properly determine a good value for name and origin. - result.Name(absolutePath.filename().wstring()); - result.Origin(absolutePath.parent_path().wstring()); - result.Path(absolutePath.wstring()); + context.Get().Set(set); } - - context.Get().Set(result); } void ShowConfigurationSet(Context& context) @@ -1067,9 +1248,9 @@ namespace AppInstaller::CLI::Workflow progressScope->Callback().SetProgressMessage(gettingDetailString); auto getDetailsOperation = configContext.Processor().GetSetDetailsAsync(configContext.Set(), ConfigurationUnitDetailFlags::ReadOnly); - auto unification = CreateProgressCancellationUnification(std::move(progressScope), getDetailsOperation); + auto unification = anon::CreateProgressCancellationUnification(std::move(progressScope), getDetailsOperation); - OutputHelper outputHelper{ context }; + anon::OutputHelper outputHelper{ context }; uint32_t unitsShown = 0; getDetailsOperation.Progress([&](const IAsyncOperationWithProgress& operation, const GetConfigurationUnitDetailsResult&) @@ -1082,7 +1263,7 @@ namespace AppInstaller::CLI::Workflow for (unitsShown; unitsShown < unitResults.Size(); ++unitsShown) { GetConfigurationUnitDetailsResult unitResult = unitResults.GetAt(unitsShown); - LogFailedGetConfigurationUnitDetails(unitResult.Unit(), unitResult.ResultInformation()); + anon::LogFailedGetConfigurationUnitDetails(unitResult.Unit(), unitResult.ResultInformation()); outputHelper.OutputConfigurationUnitInformation(unitResult.Unit()); } @@ -1127,7 +1308,7 @@ namespace AppInstaller::CLI::Workflow for (unitsShown; unitsShown < unitResults.Size(); ++unitsShown) { GetConfigurationUnitDetailsResult unitResult = unitResults.GetAt(unitsShown); - LogFailedGetConfigurationUnitDetails(unitResult.Unit(), unitResult.ResultInformation()); + anon::LogFailedGetConfigurationUnitDetails(unitResult.Unit(), unitResult.ResultInformation()); outputHelper.OutputConfigurationUnitInformation(unitResult.Unit()); } } @@ -1180,7 +1361,7 @@ namespace AppInstaller::CLI::Workflow { auto applyOperation = configContext.Processor().ApplySetAsync(configContext.Set(), ApplyConfigurationSetFlags::None); - ApplyConfigurationSetProgressOutput progress{ context, applyOperation }; + anon::ApplyConfigurationSetProgressOutput progress{ context, applyOperation }; result = applyOperation.get(); progress.HandleUnreportedProgress(result); @@ -1207,7 +1388,7 @@ namespace AppInstaller::CLI::Workflow { auto testOperation = configContext.Processor().TestSetAsync(configContext.Set()); - TestConfigurationSetProgressOutput progress{ context, testOperation }; + anon::TestConfigurationSetProgressOutput progress{ context, testOperation }; result = testOperation.get(); progress.HandleUnreportedProgress(result); @@ -1270,7 +1451,7 @@ namespace AppInstaller::CLI::Workflow { ConfigurationUnit unit = unitResult.Unit(); - OutputConfigurationUnitHeader(context, unit, unit.Type()); + anon::OutputConfigurationUnitHeader(context, unit, unit.Type()); switch (resultCode) { @@ -1306,7 +1487,7 @@ namespace AppInstaller::CLI::Workflow progressScope->Callback().SetProgressMessage(gettingDetailString); auto getLocalDetailsOperation = configContext.Processor().GetSetDetailsAsync(configContext.Set(), ConfigurationUnitDetailFlags::Local); - auto unification = CreateProgressCancellationUnification(std::move(progressScope), getLocalDetailsOperation); + auto unification = anon::CreateProgressCancellationUnification(std::move(progressScope), getLocalDetailsOperation); HRESULT getLocalHR = S_OK; GetConfigurationSetDetailsResult getLocalResult = nullptr; @@ -1340,7 +1521,7 @@ namespace AppInstaller::CLI::Workflow progressScope->Callback().SetProgressMessage(gettingDetailString); auto getCatalogDetailsOperation = configContext.Processor().GetSetDetailsAsync(configContext.Set(), ConfigurationUnitDetailFlags::Catalog); - unification = CreateProgressCancellationUnification(std::move(progressScope), getCatalogDetailsOperation); + unification = anon::CreateProgressCancellationUnification(std::move(progressScope), getCatalogDetailsOperation); HRESULT getCatalogHR = S_OK; GetConfigurationSetDetailsResult getCatalogResult = nullptr; @@ -1400,13 +1581,14 @@ namespace AppInstaller::CLI::Workflow { if (needsHeader) { - OutputConfigurationUnitHeader(context, unit, unit.Type()); + anon::OutputConfigurationUnitHeader(context, unit, unit.Type()); + needsHeader = false; foundIssue = true; } }; - if (GetValueSetString(unit.Metadata(), s_Directive_Module).empty()) + if (anon::GetValueSetString(unit.Metadata(), anon::s_Directive_Module).empty()) { outputHeaderIfNeeded(); context.Reporter.Warn() << " "_liv << Resource::String::ConfigurationUnitModuleNotProvidedWarning << std::endl; @@ -1429,23 +1611,23 @@ namespace AppInstaller::CLI::Workflow if (FAILED(catalogUnitResult.ResultInformation().ResultCode())) { outputHeaderIfNeeded(); - OutputUnitRunFailure(context, unit, catalogUnitResult.ResultInformation()); + anon::OutputUnitRunFailure(context, unit, catalogUnitResult.ResultInformation()); continue; } // If not already prerelease, try with prerelease and warn if found - std::optional allowPrereleaseDirective = GetValueSetBool(unit.Metadata(), s_Directive_AllowPrerelease); + std::optional allowPrereleaseDirective = anon::GetValueSetBool(unit.Metadata(), anon::s_Directive_AllowPrerelease); if (!allowPrereleaseDirective || !allowPrereleaseDirective.value()) { // Check if the configuration unit is prerelease but the author forgot it ConfigurationUnit clone = unit.Copy(); - clone.Metadata().Insert(s_Directive_AllowPrerelease, PropertyValue::CreateBoolean(true)); + clone.Metadata().Insert(anon::s_Directive_AllowPrerelease, PropertyValue::CreateBoolean(true)); progressScope = context.Reporter.BeginAsyncProgress(true); progressScope->Callback().SetProgressMessage(gettingDetailString); auto getUnitDetailsOperation = configContext.Processor().GetUnitDetailsAsync(clone, ConfigurationUnitDetailFlags::Catalog); - auto unitUnification = CreateProgressCancellationUnification(std::move(progressScope), getUnitDetailsOperation); + auto unitUnification = anon::CreateProgressCancellationUnification(std::move(progressScope), getUnitDetailsOperation); IConfigurationUnitProcessorDetails prereleaseDetails; @@ -1489,7 +1671,7 @@ namespace AppInstaller::CLI::Workflow { ConfigurationContext& configContext = context.Get(); auto units = configContext.Set().Units(); - auto validationOrder = GetConfigurationSetUnitValidationOrder(units.GetView()); + auto validationOrder = anon::GetConfigurationSetUnitValidationOrder(units.GetView()); Configuration::WingetDscModuleUnitValidator wingetUnitValidator; @@ -1519,4 +1701,58 @@ namespace AppInstaller::CLI::Workflow { context.Reporter.Info() << Resource::String::ConfigurationValidationFoundNoIssues << std::endl; } + + void AddWinGetPackageAndResource(Execution::Context& context) + { + auto wingetUnit = anon::CreateWinGetUnit(context); + auto configUnit = anon::CreateConfigurationUnit(context, wingetUnit); + + ConfigurationContext& configContext = context.Get(); + if (wingetUnit.has_value()) + { + configContext.Set().Units().Append(wingetUnit.value()); + } + + if (configUnit.has_value()) + { + configContext.Set().Units().Append(configUnit.value()); + } + } + + void WriteConfigFile(Execution::Context& context) + { + try + { + std::string argPath{ context.Args.GetArg(Args::Type::OutputFile) }; + + context.Reporter.Info() << Resource::String::ConfigurationExportAddingToFile(Utility::LocIndView{ argPath }) << std::endl; + + auto tempFilePath = Runtime::GetNewTempFilePath(); + + { + std::ofstream tempStream{ tempFilePath }; + tempStream << "# Created using winget configure export " << Runtime::GetClientVersion().get() << std::endl; + } + + auto openAction = Streams::FileRandomAccessStream::OpenAsync( + tempFilePath.wstring(), + FileAccessMode::ReadWrite); + + auto stream = openAction.get(); + stream.Seek(stream.Size()); + + ConfigurationContext& configContext = context.Get(); + configContext.Set().Serialize(openAction.get()); + + auto absolutePath = std::filesystem::weakly_canonical(std::filesystem::path{ argPath }); + std::filesystem::rename(tempFilePath, absolutePath); + + context.Reporter.Info() << Resource::String::ConfigurationExportSuccessful << std::endl; + } + catch (...) + { + context.Reporter.Error() << Resource::String::ConfigurationExportFailed << std::endl; + throw; + } + } } diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.h b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.h index d1b50fc104..274cb3fa31 100644 --- a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.h +++ b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.h @@ -17,6 +17,12 @@ namespace AppInstaller::CLI::Workflow // Outputs: ConfigurationSet void OpenConfigurationSet(Execution::Context& context); + // Creates or opens the configuration set. + // Required Args: OutputFile + // Inputs: ConfigurationProcessor + // Outputs: ConfigurationSet + void CreateOrOpenConfigurationSet(Execution::Context& context); + // Outputs the configuration set. // Required Args: None // Inputs: ConfigurationSet @@ -84,4 +90,16 @@ namespace AppInstaller::CLI::Workflow // Inputs: None // Outputs: None void ValidateAllGoodMessage(Execution::Context& context); + + // Adds a configuration unit with the winget package and/or exports resource given. + // Required Args: None + // Inputs: ConfigurationProcessor, ConfigurationSet + // Outputs: None + void AddWinGetPackageAndResource(Execution::Context& context); + + // Write the configuration file. + // Required Args: OutputFile + // Inputs: ConfigurationProcessor, ConfigurationSet + // Outputs: None + void WriteConfigFile(Execution::Context& context); } diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs index 8a0e8ba7fa..d4e620d684 100644 --- a/src/AppInstallerCLIE2ETests/Constants.cs +++ b/src/AppInstallerCLIE2ETests/Constants.cs @@ -307,6 +307,7 @@ public class ErrorCode public const int CONFIG_ERROR_MISSING_FIELD = unchecked((int)0x8A15C00E); public const int CONFIG_ERROR_TEST_FAILED = unchecked((int)0x8A15C00F); public const int CONFIG_ERROR_TEST_NOT_RUN = unchecked((int)0x8A15C010); + public const int WINGET_CONFIG_ERROR_GET_FAILED = unchecked((int)0x8A15C011); public const int CONFIG_ERROR_UNIT_NOT_INSTALLED = unchecked((int)0x8A15C101); public const int CONFIG_ERROR_UNIT_NOT_FOUND_REPOSITORY = unchecked((int)0x8A15C102); diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 593c83143d..229c7b4f3c 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -2889,6 +2889,50 @@ Please specify one of them using the --source option to proceed. The MSStore package could not be found. + + Adding configuration file: {0} + {Locked="{0}"} + + + Successfully exported + + + Getting configuration settings... + + + At least --packageId and/or --module with --resource must be provided + {Locked="--packageId,--module, --resource"} + + + Exports configuration resources to a configuration file. When used with --packageId, exports a WinGetPackage resource of the given package id. When used with --module and --resource, gets the settings of the resource and exports it to the configuration file. If the output configuration file already exists, appends the exported configuration resources. + {Locked="WinGetPackage,--packageId,--module, --resource"} + + + Exports configuration resources to a configuration file. + + + The module of the resource to export. + + + The package identifier to export. + + + The configuration resource to export. + + + The configuration unit failed getting its properties. + + + Failed exporting configuration. + + + Configure {0} + {Locked="{0}"} + + + Install {0} + {Locked="{0}"} + Some of the data present in the configuration file was truncated for this output; inspect the file contents for the complete content. diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp index 2c7398a852..daa3a14b82 100644 --- a/src/AppInstallerCommonCore/ExperimentalFeature.cpp +++ b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -49,9 +49,11 @@ namespace AppInstaller::Settings case ExperimentalFeature::Feature::Proxy: return userSettings.Get(); case ExperimentalFeature::Feature::ConfigureSelfElevation: - return userSettings.Get(); + return userSettings.Get(); case ExperimentalFeature::Feature::StoreDownload: return userSettings.Get(); + case ExperimentalFeature::Feature::ConfigureExport: + return userSettings.Get(); default: THROW_HR(E_UNEXPECTED); } @@ -92,7 +94,9 @@ namespace AppInstaller::Settings case Feature::ConfigureSelfElevation: return ExperimentalFeature{ "Configure Self Elevation", "configureSelfElevate", "https://aka.ms/winget-settings", Feature::ConfigureSelfElevation }; case Feature::StoreDownload: - return ExperimentalFeature{ "Store Download", "storeDownload", "https://aka.ms/winget-settings", Feature::StoreDownload }; + return ExperimentalFeature{ "Store Download", "storeDownload", "https://aka.ms/winget-settings", Feature::StoreDownload }; + case Feature::ConfigureExport: + return ExperimentalFeature{ "Configure Export", "configureExport", "https://aka.ms/winget-settings", Feature::ConfigureExport }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h index ab8850cd08..b2a55e4ef0 100644 --- a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h +++ b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -27,8 +27,9 @@ namespace AppInstaller::Settings Configuration03 = 0x4, Proxy = 0x8, SideBySide = 0x10, - ConfigureSelfElevation = 0x20, + ConfigureSelfElevation = 0x20, StoreDownload = 0x40, + ConfigureExport = 0x80, Max, // This MUST always be after all experimental features // Features listed after Max will not be shown with the features command diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index e8b06fedce..e3d3935ffd 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -76,6 +76,7 @@ namespace AppInstaller::Settings EFProxy, EFConfigureSelfElevation, EFStoreDownload, + EFConfigureExport, // Telemetry TelemetryDisable, // Install behavior @@ -159,6 +160,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFProxy, bool, bool, false, ".experimentalFeatures.proxy"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFConfigureSelfElevation, bool, bool, false, ".experimentalFeatures.configureSelfElevate"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFStoreDownload, bool, bool, false, ".experimentalFeatures.storeDownload"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFConfigureExport, bool, bool, false, ".experimentalFeatures.configureExport"sv); // Telemetry SETTINGMAPPING_SPECIALIZATION(Setting::TelemetryDisable, bool, bool, false, ".telemetry.disable"sv); // Install behavior diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index 0851745014..59c00be486 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -265,6 +265,7 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(EFProxy) WINGET_VALIDATE_PASS_THROUGH(EFConfigureSelfElevation) WINGET_VALIDATE_PASS_THROUGH(EFStoreDownload) + WINGET_VALIDATE_PASS_THROUGH(EFConfigureExport) WINGET_VALIDATE_PASS_THROUGH(AnonymizePathForDisplay) WINGET_VALIDATE_PASS_THROUGH(TelemetryDisable) WINGET_VALIDATE_PASS_THROUGH(InteractivityDisable) diff --git a/src/AppInstallerSharedLib/Errors.cpp b/src/AppInstallerSharedLib/Errors.cpp index 8aa7b6f77b..d16bd0faf3 100644 --- a/src/AppInstallerSharedLib/Errors.cpp +++ b/src/AppInstallerSharedLib/Errors.cpp @@ -263,6 +263,7 @@ namespace AppInstaller WINGET_HRESULT_INFO(WINGET_CONFIG_ERROR_MISSING_FIELD, "The configuration is missing a field."), WINGET_HRESULT_INFO(WINGET_CONFIG_ERROR_TEST_FAILED, "Some of the configuration units failed while testing their state."), WINGET_HRESULT_INFO(WINGET_CONFIG_ERROR_TEST_NOT_RUN, "Configuration state was not tested."), + WINGET_HRESULT_INFO(WINGET_CONFIG_ERROR_GET_FAILED, "The configuration unit failed getting its properties."), // Configuration Processor Errors WINGET_HRESULT_INFO(WINGET_CONFIG_ERROR_UNIT_NOT_INSTALLED, "The configuration unit was not installed."), diff --git a/src/AppInstallerSharedLib/Public/AppInstallerErrors.h b/src/AppInstallerSharedLib/Public/AppInstallerErrors.h index 8353feb55f..bfbb7c9b9d 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerErrors.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerErrors.h @@ -198,6 +198,7 @@ #define WINGET_CONFIG_ERROR_MISSING_FIELD ((HRESULT)0x8A15C00E) #define WINGET_CONFIG_ERROR_TEST_FAILED ((HRESULT)0x8A15C00F) #define WINGET_CONFIG_ERROR_TEST_NOT_RUN ((HRESULT)0x8A15C010) +#define WINGET_CONFIG_ERROR_GET_FAILED ((HRESULT)0x8A15C011) // Configuration Processor Errors #define WINGET_CONFIG_ERROR_UNIT_NOT_INSTALLED ((HRESULT)0x8A15C101) diff --git a/src/Microsoft.Management.Configuration.Processor/Exceptions/ErrorCodes.cs b/src/Microsoft.Management.Configuration.Processor/Exceptions/ErrorCodes.cs index 40ee9560cd..3639d2f71c 100644 --- a/src/Microsoft.Management.Configuration.Processor/Exceptions/ErrorCodes.cs +++ b/src/Microsoft.Management.Configuration.Processor/Exceptions/ErrorCodes.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -70,5 +70,10 @@ internal static class ErrorCodes /// The module where the DSC resource is implemented requires admin. /// internal const int WinGetConfigUnitImportModuleAdmin = unchecked((int)0x8A15C111); + + /// + /// The property type of a unit is not supported. + /// + internal const int WinGetConfigUnitUnsupportedType = unchecked((int)0x8A15C112); } } diff --git a/src/Microsoft.Management.Configuration.Processor/Exceptions/UnitPropertyUnsupportedException.cs b/src/Microsoft.Management.Configuration.Processor/Exceptions/UnitPropertyUnsupportedException.cs new file mode 100644 index 0000000000..205f3e010a --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/Exceptions/UnitPropertyUnsupportedException.cs @@ -0,0 +1,51 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Exceptions +{ + using System; + + /// + /// The property type of a unit is not supported. + /// + internal class UnitPropertyUnsupportedException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// Name. + /// Type. + /// Inner exception. + public UnitPropertyUnsupportedException(string name, Type type, Exception inner) + : base($"Property {name} of type {type.FullName} is not supported.", inner) + { + this.HResult = ErrorCodes.WinGetConfigUnitUnsupportedType; + this.Name = name; + this.Type = type; + } + + /// + /// Initializes a new instance of the class. + /// + /// Type. + public UnitPropertyUnsupportedException(Type type) + : base($"Type {type.FullName} is not supported.") + { + this.HResult = ErrorCodes.WinGetConfigUnitUnsupportedType; + this.Type = type; + } + + /// + /// Gets the name. + /// + public string? Name { get; } + + /// + /// Gets the type. + /// + public Type Type { get; } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Extensions/HashtableExtensions.cs b/src/Microsoft.Management.Configuration.Processor/Extensions/HashtableExtensions.cs new file mode 100644 index 0000000000..b11b4e2005 --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/Extensions/HashtableExtensions.cs @@ -0,0 +1,54 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Extensions +{ + using System.Collections; + using Microsoft.Management.Configuration.Processor.Exceptions; + using Microsoft.Management.Configuration.Processor.Helpers; + using Windows.Foundation.Collections; + + /// + /// Extensions for Hashtable. + /// + internal static class HashtableExtensions + { + /// + /// Convert a hashtable to a value set. + /// + /// hashtable. + /// Value set. + public static ValueSet ToValueSet(this Hashtable hashtable) + { + var valueSet = new ValueSet(); + + foreach (DictionaryEntry entry in hashtable) + { + if (entry.Key is string key) + { + if (entry.Value is null) + { + valueSet.Add(key, null); + } + else + { + var value = TypeHelpers.GetCompatibleValueSetValueOfProperty(entry.Value.GetType(), entry.Value); + if (value != null) + { + valueSet.Add(key, value); + } + } + } + else + { + throw new UnitPropertyUnsupportedException(entry.Key.GetType()); + } + } + + return valueSet; + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Helpers/TypeHelpers.cs b/src/Microsoft.Management.Configuration.Processor/Helpers/TypeHelpers.cs index abb32ce1cc..e9ad218a3a 100644 --- a/src/Microsoft.Management.Configuration.Processor/Helpers/TypeHelpers.cs +++ b/src/Microsoft.Management.Configuration.Processor/Helpers/TypeHelpers.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -6,8 +6,12 @@ namespace Microsoft.Management.Configuration.Processor.Helpers { + using System; + using System.Collections; using System.Collections.Generic; using System.Reflection; + using Microsoft.Management.Configuration.Processor.Exceptions; + using Microsoft.Management.Configuration.Processor.Extensions; using Windows.Foundation.Collections; /// @@ -74,18 +78,78 @@ public static ValueSet GetAllPropertiesValues(object obj) var result = new ValueSet(); foreach (PropertyInfo property in obj.GetType().GetProperties()) { - // Specialize here. - if (property.PropertyType.IsEnum) + var key = property.Name; + var value = GetCompatibleValueSetValueOfProperty(property.PropertyType, property.GetValue(obj)); + result.Add(key, value); + } + + return result; + } + + /// + /// Gets a compatible type for a ValueSet value. + /// + /// Type. + /// Value. + /// Value converted to a compatible type. + public static object? GetCompatibleValueSetValueOfProperty(Type type, object? value) + { + if (value == null) + { + return null; + } + + // Specialize here. + if (type.IsEnum) + { + return value.ToString(); + } + else if (type == typeof(Hashtable)) + { + Hashtable hashtable = (Hashtable)value; + return hashtable.ToValueSet(); + } + else if (type.IsArray) + { + var valueSetArray = new ValueSet(); + int index = 0; + foreach (object arrayObj in (Array)value) + { + var arrayValue = GetCompatibleValueSetValueOfProperty(arrayObj.GetType(), arrayObj); + if (arrayValue != null) + { + valueSetArray.Add(index.ToString(), arrayValue); + index++; + } + } + + if (valueSetArray.Count > 0) + { + valueSetArray.Add("treatAsArray", true); + } + + return valueSetArray; + } + else if (type == typeof(string)) + { + // Ignore empty strings. + string propertyString = (string)value; + if (!string.IsNullOrEmpty(propertyString)) { - result.Add(property.Name, property.GetValue(obj)?.ToString()); + return propertyString; } else { - result.Add(property.Name, property.GetValue(obj)); + return null; } } + else if (type.IsValueType) + { + return value; + } - return result; + // This might be too restrictive but anything else is going to be some object that we don't support anyway. + throw new UnitPropertyUnsupportedException(value.GetType()); } } } diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/Errors.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/Errors.cs index e2d3e1fadf..c512fa32b1 100644 --- a/src/Microsoft.Management.Configuration.UnitTests/Helpers/Errors.cs +++ b/src/Microsoft.Management.Configuration.UnitTests/Helpers/Errors.cs @@ -31,6 +31,7 @@ internal static class Errors public static readonly int WINGET_CONFIG_ERROR_MISSING_FIELD = unchecked((int)0x8A15C00E); public static readonly int WINGET_CONFIG_ERROR_TEST_FAILED = unchecked((int)0x8A15C00F); public static readonly int WINGET_CONFIG_ERROR_TEST_NOT_RUN = unchecked((int)0x8A15C010); + public static readonly int WINGET_CONFIG_ERROR_GET_FAILED = unchecked((int)0x8A15C011); // Configuration Processor Errors public static readonly int WINGET_CONFIG_ERROR_UNIT_NOT_INSTALLED = unchecked((int)0x8A15C101); diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/HashtableExtensionsTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/HashtableExtensionsTests.cs new file mode 100644 index 0000000000..2c5d21d4fc --- /dev/null +++ b/src/Microsoft.Management.Configuration.UnitTests/Tests/HashtableExtensionsTests.cs @@ -0,0 +1,134 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Tests +{ + using System.Collections; + using Microsoft.Management.Configuration.Processor.Exceptions; + using Microsoft.Management.Configuration.Processor.Extensions; + using Microsoft.Management.Configuration.UnitTests.Fixtures; + using Windows.Foundation.Collections; + using Xunit; + using Xunit.Abstractions; + + /// + /// Hashtable extension tests. + /// + [Collection("UnitTestCollection")] + public class HashtableExtensionsTests + { + private readonly UnitTestFixture fixture; + private readonly ITestOutputHelper log; + + /// + /// Initializes a new instance of the class. + /// + /// Unit test fixture. + /// Log helper. + public HashtableExtensionsTests(UnitTestFixture fixture, ITestOutputHelper log) + { + this.fixture = fixture; + this.log = log; + } + + /// + /// Tests ToValueSet with simple types. + /// + [Fact] + public void ToValueSet_Test() + { + var ht = new Hashtable() + { + { "key1", "value1" }, + { "key2", 2 }, + { "key3", true }, + }; + + var valueSet = ht.ToValueSet(); + + Assert.True(valueSet.ContainsKey("key1")); + Assert.Equal("value1", (string)valueSet["key1"]); + + Assert.True(valueSet.ContainsKey("key2")); + Assert.Equal(2, (int)valueSet["key2"]); + + Assert.True(valueSet.ContainsKey("key3")); + Assert.True((bool)valueSet["key3"]); + } + + /// + /// Test for inner hashtables. + /// + [Fact] + public void ToValueSet_InnerHashtable() + { + var ht = new Hashtable() + { + { "hashtableKey", new Hashtable() + { + { "key1", "value1" }, + { "key2", 2 }, + { "key3", true }, + } + }, + }; + + var valueSet = ht.ToValueSet(); + + Assert.True(valueSet.ContainsKey("hashtableKey")); + var resultValueSet = (ValueSet)valueSet["hashtableKey"]; + + Assert.True(resultValueSet.ContainsKey("key1")); + Assert.Equal("value1", (string)resultValueSet["key1"]); + + Assert.True(resultValueSet.ContainsKey("key2")); + Assert.Equal(2, (int)resultValueSet["key2"]); + + Assert.True(resultValueSet.ContainsKey("key3")); + Assert.True((bool)resultValueSet["key3"]); + } + + /// + /// Test for inner arrays. + /// + [Fact] + public void ToValueSet_InnerArray() + { + var ht = new Hashtable() + { + { + "arrayKey", new string[] + { + "s1", + "s2", + "s3", + } + }, + }; + + var valueSet = ht.ToValueSet(); + + Assert.True(valueSet.ContainsKey("arrayKey")); + var resultValueSet = (ValueSet)valueSet["arrayKey"]; + Assert.True(resultValueSet.ContainsKey("treatAsArray")); + Assert.Equal(4, resultValueSet.Count); + } + + /// + /// Test when a key is not a string. + /// + [Fact] + public void ToValueSet_KeyNotString() + { + var ht = new Hashtable() + { + { 1, "value" }, + }; + + Assert.Throws(() => ht.ToValueSet()); + } + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/OpenConfigurationSetTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/OpenConfigurationSetTests.cs index 04c9316f93..65e6e00803 100644 --- a/src/Microsoft.Management.Configuration.UnitTests/Tests/OpenConfigurationSetTests.cs +++ b/src/Microsoft.Management.Configuration.UnitTests/Tests/OpenConfigurationSetTests.cs @@ -482,7 +482,7 @@ public void TestSet_Serialize_0_2() properties: configurationVersion: 0.2 assertions: - - resource: FakeModule + - resource: FakeModule/FakeResource id: TestId directives: description: FakeDescription @@ -493,7 +493,7 @@ public void TestSet_Serialize_0_2() TestBool: false TestInt: 1234 resources: - - resource: FakeModule2 + - resource: FakeModule2/FakeResource2 id: TestId2 dependsOn: - TestId @@ -526,17 +526,17 @@ public void TestSet_Serialize_0_2() Assert.Equal("0.2", set.SchemaVersion); Assert.Equal(2, set.Units.Count); - Assert.Equal("FakeModule", set.Units[0].Type); + Assert.Equal("FakeResource", set.Units[0].Type); Assert.Equal(ConfigurationUnitIntent.Assert, set.Units[0].Intent); Assert.Equal("TestId", set.Units[0].Identifier); - this.VerifyValueSet(set.Units[0].Metadata, new ("description", "FakeDescription"), new ("allowPrerelease", true), new ("securityContext", "elevated")); + this.VerifyValueSet(set.Units[0].Metadata, new ("description", "FakeDescription"), new ("allowPrerelease", true), new ("securityContext", "elevated"), new ("module", "FakeModule")); this.VerifyValueSet(set.Units[0].Settings, new ("TestString", "Hello"), new ("TestBool", false), new ("TestInt", 1234)); - Assert.Equal("FakeModule2", set.Units[1].Type); + Assert.Equal("FakeResource2", set.Units[1].Type); Assert.Equal(ConfigurationUnitIntent.Apply, set.Units[1].Intent); Assert.Equal("TestId2", set.Units[1].Identifier); this.VerifyStringArray(set.Units[1].Dependencies, "TestId", "dependency2", "dependency3"); - this.VerifyValueSet(set.Units[1].Metadata, new ("description", "FakeDescription2"), new ("securityContext", "elevated")); + this.VerifyValueSet(set.Units[1].Metadata, new ("description", "FakeDescription2"), new ("securityContext", "elevated"), new ("module", "FakeModule2")); ValueSet mapping = new ValueSet(); mapping.Add("Key", "TestValue"); @@ -757,7 +757,7 @@ private void VerifyValueSet(ValueSet values, params KeyValuePair foreach (var expectation in expected) { - Assert.True(values.ContainsKey(expectation.Key)); + Assert.True(values.ContainsKey(expectation.Key), $"Not Found {expectation.Key}"); object value = values[expectation.Key]; switch (expectation.Value) diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/TypeHelpersTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/TypeHelpersTests.cs index 91345b4105..c18a616b72 100644 --- a/src/Microsoft.Management.Configuration.UnitTests/Tests/TypeHelpersTests.cs +++ b/src/Microsoft.Management.Configuration.UnitTests/Tests/TypeHelpersTests.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -6,6 +6,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests { + using System.Collections; using System.Collections.Generic; using Microsoft.Management.Configuration.Processor.Helpers; using Microsoft.Management.Configuration.UnitTests.Fixtures; @@ -136,5 +137,75 @@ public void GetAllPropertiesValuesTests() Assert.True(set.TryGetValue("Property3", out object v3)); Assert.Equal(e.ToString(), v3); } + + /// + /// Verifies when a property is a Hashtable. It must be converted to a ValueSet. + /// + [Fact] + public void GetAllPropertiesValuesTest_Hashtable() + { + string k1 = "key1"; + string k2 = "key2"; + int v1 = 7; + string v2 = "value2"; + dynamic obj = new + { + Property1 = new Hashtable + { + { k1, v1 }, + { k2, v2 }, + }, + }; + + ValueSet set = TypeHelpers.GetAllPropertiesValues(obj); + Assert.Single(set); + + Assert.True(set.ContainsKey("Property1")); + Assert.True(set.TryGetValue("Property1", out object valueSetResultObj)); + + ValueSet? valueSetResult = valueSetResultObj as ValueSet; + Assert.NotNull(valueSetResult); + Assert.Equal(2, valueSetResult.Count); + Assert.True(valueSetResult.ContainsKey(k1)); + Assert.Equal(v1, (int)valueSetResult[k1]); + Assert.True(valueSetResult.ContainsKey(k2)); + Assert.Equal(v2, (string)valueSetResult[k2]); + } + + /// + /// Verifies when a property is an array. It must generate a ValueSet + /// where the keys are the index and a key treatAsArray means the value + /// must be treated an array. + /// + [Fact] + public void GetAllPropertiesValuesTest_Array() + { + dynamic obj = new + { + Property1 = new int[] + { + 1, + 2, + 3, + 4, + }, + }; + + ValueSet set = TypeHelpers.GetAllPropertiesValues(obj); + Assert.Single(set); + + Assert.True(set.ContainsKey("Property1")); + Assert.True(set.TryGetValue("Property1", out object valueSetResultObj)); + + ValueSet? valueSetResult = valueSetResultObj as ValueSet; + Assert.NotNull(valueSetResult); + Assert.Equal(5, valueSetResult.Count); + + Assert.True(valueSetResult.ContainsKey("treatAsArray")); + Assert.True(valueSetResult.ContainsKey("0")); + Assert.True(valueSetResult.ContainsKey("1")); + Assert.True(valueSetResult.ContainsKey("2")); + Assert.True(valueSetResult.ContainsKey("3")); + } } } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.cpp index 2107fdef17..215ac30e52 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.cpp +++ b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.cpp @@ -15,6 +15,11 @@ using namespace winrt::Windows::Foundation; namespace winrt::Microsoft::Management::Configuration::implementation { + namespace anon + { + static constexpr std::string_view s_nullValue = "null"; + } + std::unique_ptr ConfigurationSetSerializer::CreateSerializer(hstring version) { // Create the parser based on the version selected @@ -46,13 +51,37 @@ namespace winrt::Microsoft::Management::Configuration::implementation for (const auto& [key, value] : valueSet) { - std::string keyName = winrt::to_string(key); - const auto& currentValueSet = value.try_as(); + if (value != nullptr) + { + std::string keyName = winrt::to_string(key); + emitter << Key << keyName << Value; + WriteYamlValue(emitter, value); + } + } + + emitter << EndMap; + } + + void ConfigurationSetSerializer::WriteYamlValue(AppInstaller::YAML::Emitter& emitter, const winrt::Windows::Foundation::IInspectable& value) + { + if (value == nullptr) + { + emitter << anon::s_nullValue; + } + else + { + const auto& currentValueSet = value.try_as(); if (currentValueSet) { - emitter << AppInstaller::YAML::Key << keyName; - WriteYamlValueSet(emitter, currentValueSet); + if (currentValueSet.HasKey(L"treatAsArray")) + { + WriteYamlValueSetAsArray(emitter, currentValueSet); + } + else + { + WriteYamlValueSet(emitter, currentValueSet); + } } else { @@ -61,15 +90,15 @@ namespace winrt::Microsoft::Management::Configuration::implementation if (type == PropertyType::Boolean) { - emitter << AppInstaller::YAML::Key << keyName << AppInstaller::YAML::Value << property.GetBoolean(); + emitter << property.GetBoolean(); } else if (type == PropertyType::String) { - emitter << AppInstaller::YAML::Key << keyName << AppInstaller::YAML::Value << AppInstaller::Utility::ConvertToUTF8(property.GetString()); + emitter << AppInstaller::Utility::ConvertToUTF8(property.GetString()); } else if (type == PropertyType::Int64) { - emitter << AppInstaller::YAML::Key << keyName << AppInstaller::YAML::Value << property.GetInt64(); + emitter << property.GetInt64(); } else { @@ -77,8 +106,35 @@ namespace winrt::Microsoft::Management::Configuration::implementation } } } + } - emitter << EndMap; + void ConfigurationSetSerializer::WriteYamlValueSetAsArray(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSetArray) + { + std::vector> arrayValues; + for (const auto& arrayValue : valueSetArray) + { + if (arrayValue.Key() != L"treatAsArray") + { + arrayValues.emplace_back(std::make_pair(std::stoi(arrayValue.Key().c_str()), arrayValue.Value())); + } + } + + std::sort( + arrayValues.begin(), + arrayValues.end(), + [](const std::pair& a, const std::pair& b) + { + return a.first < b.first; + }); + + emitter << BeginSeq; + + for (const auto& arrayValue : arrayValues) + { + WriteYamlValue(emitter, arrayValue.second); + } + + emitter << EndSeq; } void ConfigurationSetSerializer::WriteYamlConfigurationUnits(AppInstaller::YAML::Emitter& emitter, const std::vector& units) @@ -89,7 +145,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation { // Resource emitter << BeginMap; - emitter << Key << GetConfigurationFieldName(ConfigurationField::Resource) << Value << AppInstaller::Utility::ConvertToUTF8(unit.Type()); + emitter << Key << GetConfigurationFieldName(ConfigurationField::Resource) << Value << AppInstaller::Utility::ConvertToUTF8(GetResourceName(unit)); // Id if (!unit.Identifier().empty()) @@ -112,9 +168,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation } // Directives - const auto& metadata = unit.Metadata(); - emitter << Key << GetConfigurationFieldName(ConfigurationField::Directives); - WriteYamlValueSet(emitter, metadata); + WriteResourceDirectives(emitter, unit); // Settings const auto& settings = unit.Settings(); @@ -126,4 +180,21 @@ namespace winrt::Microsoft::Management::Configuration::implementation emitter << EndSeq; } + + winrt::hstring ConfigurationSetSerializer::GetResourceName(const ConfigurationUnit& unit) + { + return unit.Type(); + } + + void ConfigurationSetSerializer::WriteResourceDirectives(AppInstaller::YAML::Emitter& emitter, const ConfigurationUnit& unit) + { + const auto& metadata = unit.Metadata(); + emitter << Key << GetConfigurationFieldName(ConfigurationField::Directives); + WriteYamlValueSet(emitter, metadata); + } + + winrt::hstring ConfigurationSetSerializer::GetSchemaVersionComment(winrt::hstring version) + { + return winrt::to_hstring(L"# yaml-language-server: $schema=https://aka.ms/configuration-dsc-schema/") + version; + } } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.h b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.h index bf522172c4..3c7e94223f 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.h +++ b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.h @@ -25,8 +25,13 @@ namespace winrt::Microsoft::Management::Configuration::implementation protected: ConfigurationSetSerializer() = default; + void WriteYamlConfigurationUnits(AppInstaller::YAML::Emitter& emitter, const std::vector& units); void WriteYamlValueSet(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSet); + void WriteYamlValue(AppInstaller::YAML::Emitter& emitter, const winrt::Windows::Foundation::IInspectable& value); + void WriteYamlValueSetAsArray(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSetArray); + winrt::hstring GetSchemaVersionComment(winrt::hstring version); - void WriteYamlConfigurationUnits(AppInstaller::YAML::Emitter& emitter, const std::vector& units); + virtual winrt::hstring GetResourceName(const ConfigurationUnit& unit) = 0; + virtual void WriteResourceDirectives(AppInstaller::YAML::Emitter& emitter, const ConfigurationUnit& unit) = 0; }; } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.cpp index 5631e1d2e3..1d7293f88e 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.cpp +++ b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.cpp @@ -9,6 +9,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation { using namespace AppInstaller::YAML; + using namespace winrt::Windows::Foundation; hstring ConfigurationSetSerializer_0_2::Serialize(ConfigurationSet* configurationSet) { @@ -50,6 +51,37 @@ namespace winrt::Microsoft::Management::Configuration::implementation emitter << EndMap; emitter << EndMap; - return winrt::to_hstring(emitter.str()); + return GetSchemaVersionComment(configurationSet->SchemaVersion()) + winrt::to_hstring(L"\n") + winrt::to_hstring(emitter.str()); + } + + winrt::hstring ConfigurationSetSerializer_0_2::GetResourceName(const ConfigurationUnit& unit) + { + const auto& metadata = unit.Metadata(); + const auto moduleKey = GetConfigurationFieldNameHString(ConfigurationField::ModuleDirective); + if (metadata.HasKey(moduleKey)) + { + auto object = metadata.Lookup(moduleKey); + auto property = object.try_as(); + if (property && property.Type() == PropertyType::String) + { + return property.GetString() + '/' + unit.Type(); + } + } + + return unit.Type(); + } + + void ConfigurationSetSerializer_0_2::WriteResourceDirectives(AppInstaller::YAML::Emitter& emitter, const ConfigurationUnit& unit) + { + auto metadata = unit.Metadata(); + + const auto moduleKey = GetConfigurationFieldNameHString(ConfigurationField::ModuleDirective); + if (metadata.HasKey(moduleKey)) + { + metadata.Remove(moduleKey); + } + + emitter << Key << GetConfigurationFieldName(ConfigurationField::Directives); + WriteYamlValueSet(emitter, metadata); } } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.h b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.h index c7034215d9..29f2773d00 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.h +++ b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.h @@ -18,5 +18,9 @@ namespace winrt::Microsoft::Management::Configuration::implementation ConfigurationSetSerializer_0_2& operator=(ConfigurationSetSerializer_0_2&&) = default; hstring Serialize(ConfigurationSet* configurationSet) override; + + protected: + winrt::hstring GetResourceName(const ConfigurationUnit& unit) override; + void WriteResourceDirectives(AppInstaller::YAML::Emitter& emitter, const ConfigurationUnit& unit) override; }; } diff --git a/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters b/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters index ed58521881..5c2caffcba 100644 --- a/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters +++ b/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters @@ -222,11 +222,15 @@ Internals - - Parser + + Parser + + + Parser + From 0daffd36a5d2cae8e3b4f84cda4830790823f29f Mon Sep 17 00:00:00 2001 From: Ruben Guerrero Date: Thu, 2 May 2024 13:36:27 -0700 Subject: [PATCH 6/8] Create local Microsoft.WindowsPackageManager.Utils nuget (#4439) This is a really simple script to locally create a Microsoft.WindowsPackageManager.Utils nuget package and optionally add it to an existing source. This is to help local development by not requiring a new version to nuget.org. ###### Microsoft Reviewers: [Open in CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/microsoft/winget-cli/pull/4439) --- .gitignore | 3 + .../scripts/CreateLocalNuget.ps1 | 85 +++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 src/WinGetUtilInterop/scripts/CreateLocalNuget.ps1 diff --git a/.gitignore b/.gitignore index 705f174d84..af9e14a497 100644 --- a/.gitignore +++ b/.gitignore @@ -345,3 +345,6 @@ src/PowerShell/Microsoft.WinGet.Client/Crescendo/*.psm1 # Dev PowerShell module path src/PowerShell/scripts/Module + +# Interop nuget +src/WinGetUtilInterop/scripts/Nuget* diff --git a/src/WinGetUtilInterop/scripts/CreateLocalNuget.ps1 b/src/WinGetUtilInterop/scripts/CreateLocalNuget.ps1 new file mode 100644 index 0000000000..2b46e7907e --- /dev/null +++ b/src/WinGetUtilInterop/scripts/CreateLocalNuget.ps1 @@ -0,0 +1,85 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +<# + .SYNOPSIS + Creates a local Microsoft.WindowsPackageManager.Utils nuget package and add it to a local nuget feed. +#> + +[CmdletBinding()] +param ( + [Parameter(Mandatory)] + [ValidateSet("Debug", "Release", "ReleaseStatic")] + [string] + $Configuration, + + [Parameter(Mandatory)] + [string] + $NugetVersion, + + [string] + $BuildRoot = "", + + [string] + $LocalNugetSource = "" +) + +if ($BuildRoot -eq "") +{ + $BuildRoot = "$PSScriptRoot\..\.."; +} + +$local:repoPath = "$PSScriptRoot\..\..\..\" + +# Create all directories and copy files in location expected from the nuspec. +# The paths contains 'release' but it whatever configuration the param is. +$local:nugetWorkingDir = "$PSScriptRoot\NugetFiles" +$local:x64NugetPath = "$nugetWorkingDir\Build.x64release\src\x64\Release\WinGetUtil" +$local:x86NugetPath = "$nugetWorkingDir\Build.x86release\src\x86\Release\WinGetUtil" +$local:manifestsNugetPath = "$nugetWorkingDir\Build.x64release\schemas\JSON\manifests" +$local:interopNugetPath = "$nugetWorkingDir\Build.x64release\src\WinGetUtilInterop\bin\Release\netstandard2.1" +$local:targetsNugetPath = "$nugetWorkingDir\Build.x64release\src\WinGetUtilInterop\build" + +Write-Host "Prepare nuget files" +if (Test-Path $nugetWorkingDir) +{ + Remove-Item $nugetWorkingDir -Recurse +} +New-Item $nugetWorkingDir -ItemType directory | Out-Null +New-Item $x64NugetPath -ItemType directory | Out-Null +New-Item $x86NugetPath -ItemType directory | Out-Null +New-Item $manifestsNugetPath -ItemType directory | Out-Null +New-Item $interopNugetPath -ItemType directory | Out-Null +New-Item $targetsNugetPath -ItemType directory | Out-Null + +function CopyFile([string]$in, [string]$out) +{ + $copyErrors = $null + Copy-Item $in $out -Force -ErrorVariable copyErrors -ErrorAction SilentlyContinue + $copyErrors | ForEach-Object { Write-Warning $_ } +} + +CopyFile "$BuildRoot\x64\$Configuration\WinGetUtil\WinGetUtil.dll" "$x64NugetPath\WinGetUtil.dll" +CopyFile "$BuildRoot\x64\$Configuration\WinGetUtil\WinGetUtil.pdb" "$x64NugetPath\WinGetUtil.pdb" +CopyFile "$BuildRoot\x86\$Configuration\WinGetUtil\WinGetUtil.dll" "$x86NugetPath\WinGetUtil.dll" +CopyFile "$BuildRoot\x86\$Configuration\WinGetUtil\WinGetUtil.pdb" "$x86NugetPath\WinGetUtil.pdb" +CopyFile "$repoPath\src\WinGetUtilInterop\bin\$Configuration\netstandard2.1\WinGetUtilInterop.dll" "$interopNugetPath\WinGetUtilInterop.dll" +CopyFile "$repoPath\src\WinGetUtilInterop\bin\$Configuration\netstandard2.1\WinGetUtilInterop.pdb" "$interopNugetPath\WinGetUtilInterop.pdb" +CopyFile "$repoPath\src\WinGetUtilInterop\build\Microsoft.WindowsPackageManager.Utils.targets" "$targetsNugetPath\Microsoft.WindowsPackageManager.Utils.targets" +CopyFile "$repoPath\WinGetUtil.nuspec" "$nugetWorkingDir\WinGetUtil.nuspec" +Copy-Item "$repoPath\schemas\JSON\manifests" $manifestsNugetPath -Recurse + +# Create nuget +Write-Host "Creating nuget package" +$local:result = nuget pack .\NugetFiles\WinGetUtil.nuspec -Version $NugetVersion -OutputDirectory NugetOut +$local:outFile = $result -match "nupkg" +$outFile = $outFile[0] +$outFile = $outFile.Substring($outFile.IndexOf("'") + 1, $outFile.LastIndexOf("'") - $outFile.IndexOf("'") - 1) +Write-Host "Created $outFile" + +if ($LocalNugetSource -ne "") +{ + Write-Host "Adding $outFile to local nuget feed" + nuget add $outFile -Source $LocalNugetSource +} + From 2979fd81dedd79f06bb0061159f6764b4ac70a9a Mon Sep 17 00:00:00 2001 From: Alvin Joy <89687635+alvinsjoy@users.noreply.github.com> Date: Fri, 3 May 2024 11:11:00 +0530 Subject: [PATCH 7/8] Use GitHub's built in markdown highlighting to highlight notes in README (#4441) --- README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ae2fe25c2d..33d044433a 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,13 @@ If you are new to the Windows Package Manager, you might want to [Explore the Windows Package Manager tool](https://docs.microsoft.com/learn/modules/explore-windows-package-manager-tool/?WT.mc_id=AZ-MVP-5004737). The client has access to packages from two default sources. The first is "msstore" the Microsoft Store (free Apps rated "e" for everyone). The second is "winget" the [WinGet community repository](https://github.com/microsoft/winget-pkgs). -> **Note**: Group policy may be configured and modify configured sources. Run `winget --info` to see any configured policies. +> [!NOTE] +> Group policy may be configured and modify configured sources. Run `winget --info` to see any configured policies. ## Installing The Client -> **Note**: The client requires Windows 10 1809 (build 17763) or later at this time. Windows Server 2019 is not supported as the Microsoft Store is not available nor are updated dependencies. It may be possible to install on Windows Server 2022, this should be considered experimental (not supported), and requires dependencies to be manually installed as well. +> [!NOTE] +> The client requires Windows 10 1809 (build 17763) or later at this time. Windows Server 2019 is not supported as the Microsoft Store is not available nor are updated dependencies. It may be possible to install on Windows Server 2022, this should be considered experimental (not supported), and requires dependencies to be manually installed as well. ### Microsoft Store [Recommended] @@ -24,7 +26,8 @@ There are two methods to get development releases: * Install a [Windows 10 or Windows 11 Insider](https://insider.windows.com/) build. * Join the Windows Package Manager Insider program by [signing up](http://aka.ms/winget-InsiderProgram). -> **Note**: It may take a few days to get the updated App Installer after you receive e-mail confirmation from joining the Windows Package Manager Insider program. If you decide to install the latest release from GitHub, and you have successfully joined the insider program, you will receive updates when the next development release has been published in the Microsoft Store. +> [!NOTE] +> It may take a few days to get the updated App Installer after you receive e-mail confirmation from joining the Windows Package Manager Insider program. If you decide to install the latest release from GitHub, and you have successfully joined the insider program, you will receive updates when the next development release has been published in the Microsoft Store. Once you have received the updated App Installer from the Microsoft Store you should be able to execute `winget features` to see experimental features. Some users have reported [issues](https://github.com/microsoft/winget-cli/issues/210) with the client not being on their PATH. @@ -32,7 +35,8 @@ Once you have received the updated App Installer from the Microsoft Store you sh The same Microsoft Store package will be made available via our [Releases](https://github.com/microsoft/winget-cli/releases). Note that installing this package will give you the WinGet client, but it will not enable automatic updates from the Microsoft Store if you have not joined the Windows Package Manager Insider program. -> **Note**: You may need to install the [VC++ v14 Desktop Framework Package](https://docs.microsoft.com/troubleshoot/cpp/c-runtime-packages-desktop-bridge#how-to-install-and-update-desktop-framework-packages). +> [!NOTE] +> You may need to install the [VC++ v14 Desktop Framework Package](https://docs.microsoft.com/troubleshoot/cpp/c-runtime-packages-desktop-bridge#how-to-install-and-update-desktop-framework-packages). > This should only be necessary on older builds of Windows 10 and only if you get an error about missing framework packages. ### Troubleshooting @@ -99,7 +103,8 @@ The client is built around the concept of sources; a set of packages effectively * Universal Windows Platform Development * Check [.vsconfig file](.vsconfig) for full components list * [Windows SDK for Windows 11 (10.0.22000.194)](https://developer.microsoft.com/en-us/windows/downloads/sdk-archive/) - > **Note**: You can also get it through `winget install Microsoft.WindowsSDK --version 10.0.22000.832` (use --force if you have a newer version installed) or via Visual Studio > Get Tools and Features > Individual Components > Windows 10 SDK (10.0.22000.0) +> [!NOTE] +> You can also get it through `winget install Microsoft.WindowsSDK --version 10.0.22000.832` (use --force if you have a newer version installed) or via Visual Studio > Get Tools and Features > Individual Components > Windows 10 SDK (10.0.22000.0) * The following extensions: * [Microsoft Visual Studio Installer Projects](https://marketplace.visualstudio.com/items?itemName=VisualStudioClient.MicrosoftVisualStudio2022InstallerProjects) From 4073c4304e6a87db3cbf0826bb694d9296072cd3 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Tue, 7 May 2024 13:33:11 -0500 Subject: [PATCH 8/8] Remove Installer Parent Directory when Empty (#4451) --- src/AppInstallerCLICore/Workflows/DownloadFlow.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp b/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp index 307851bfa3..be853966df 100644 --- a/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp @@ -149,6 +149,17 @@ namespace AppInstaller::CLI::Workflow try { std::filesystem::remove(path); + + // It is assumed that the parent of the installer path will always be a directory + // If it isn't, then something went severely wrong. However, we will check that + // it is a directory here just to be safe. If it is an empty directory, remove it + + if (std::filesystem::is_directory(path.parent_path()) && + std::filesystem::is_empty(path.parent_path()) + ) + { + std::filesystem::remove(path.parent_path()); + } } catch (const std::exception& e) {