commit 190b81298eec67c993f62cb3c05bd5d13cb7e8b9 parent 0d824f4d7e370d78250f0050b89cee998c1d9c66 Author: JohnMcPMS <johnmcp@microsoft.com> Date: Tue, 24 Oct 2023 17:25:21 -0700 Configuration Schema 0.3 (#3779) This change adds a new 0.3 configuration schema (as experimental). This new schema is more aligned with the PowerShell DSC v3 schema, and supports variables, parameters, and configuration nesting (groups). This change only implements the parsing of the new schema; future changes will implement handling the new features. Diffstat:
61 files changed, 2213 insertions(+), 398 deletions(-)
diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -417,6 +417,8 @@ SARL schematab sddl SECUREFILEPATH +secureobject +securestring seof servercert servercertificate diff --git a/azure-pipelines.yml b/azure-pipelines.yml @@ -305,6 +305,7 @@ jobs: codeCoverageEnabled: true platform: '$(buildPlatform)' configuration: '$(BuildConfiguration)' + diagnosticsEnabled: true condition: succeededOrFailed() - task: PowerShell@2 diff --git a/doc/Settings.md b/doc/Settings.md @@ -285,6 +285,17 @@ You can enable the feature as shown below. }, ``` +### resume + +This feature enables support for some commands to resume. +You can enable the feature as shown below. + +```json + "experimentalFeatures": { + "resume": true + }, +``` + ### reboot This feature enables support for initiating a reboot. @@ -294,4 +305,15 @@ You can enable the feature as shown below. "experimentalFeatures": { "reboot": true }, +``` + +### configuration03 + +This feature enables the configuration schema 0.3. +You can enable the feature as shown below. + +```json + "experimentalFeatures": { + "configuration03": true + }, ``` \ No newline at end of file diff --git a/src/AppInstallerCLICore/Commands/DebugCommand.cpp b/src/AppInstallerCLICore/Commands/DebugCommand.cpp @@ -88,6 +88,7 @@ namespace AppInstaller::CLI OutputProxyStubInterfaceRegistration<winrt::Windows::Foundation::Collections::IIterable<winrt::Microsoft::Management::Configuration::ConfigurationUnit>>(context); OutputProxyStubInterfaceRegistration<winrt::Windows::Foundation::Collections::IIterable<winrt::Microsoft::Management::Configuration::ConfigurationSet>>(context); OutputProxyStubInterfaceRegistration<winrt::Windows::Foundation::Collections::IIterable<winrt::Microsoft::Management::Configuration::ConfigurationConflict>>(context); + OutputProxyStubInterfaceRegistration<winrt::Windows::Foundation::Collections::IIterable<winrt::Microsoft::Management::Configuration::ConfigurationParameter>>(context); OutputProxyStubInterfaceRegistration<winrt::Windows::Foundation::Collections::IIterable<winrt::Microsoft::Management::Configuration::IConfigurationUnitSettingDetails>>(context); OutputProxyStubInterfaceRegistration<winrt::Windows::Foundation::Collections::IIterable<winrt::Microsoft::Management::Configuration::ConfigurationConflictSetting>>(context); OutputProxyStubInterfaceRegistration<winrt::Windows::Foundation::Collections::IIterable<winrt::Microsoft::Management::Configuration::GetConfigurationUnitDetailsResult>>(context); diff --git a/src/AppInstallerCLICore/ExecutionContext.h b/src/AppInstallerCLICore/ExecutionContext.h @@ -41,6 +41,9 @@ // Also returns the specified value from the current function. #define AICLI_TERMINATE_CONTEXT_RETURN(_hr_,_ret_) AICLI_TERMINATE_CONTEXT_ARGS(context,_hr_,_ret_) +// Returns if the context is terminated. +#define AICLI_RETURN_IF_TERMINATED(_context_) if ((_context_).IsTerminated()) { return; } + namespace AppInstaller::CLI { struct Command; diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp @@ -864,6 +864,12 @@ namespace AppInstaller::CLI::Workflow 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. // TODO: Consider how to properly determine a good value for name and origin. result.Name(absolutePath.filename().wstring()); diff --git a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj @@ -61,8 +61,10 @@ <None Remove="TestData\Configuration\PSGallery_NoSettings.yml" /> <None Remove="testdata\configuration\ResourceNotFound.yml" /> <None Remove="testdata\configuration\ResourcesNotASequence.yml" /> + <None Remove="TestData\Configuration\ShowDetails_TestRepo_0_3.yml" /> <None Remove="testdata\configuration\UnitNotAMap.yml" /> <None Remove="testdata\configuration\UnknownVersion.yml" /> + <None Remove="TestData\Configuration\WithParameters_0_3.yml" /> <None Remove="TestData\localsource.json" /> </ItemGroup> diff --git a/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs @@ -7,6 +7,7 @@ namespace AppInstallerCLIE2ETests { using AppInstallerCLIE2ETests.Helpers; + using Microsoft.VisualBasic; using NUnit.Framework; /// <summary> @@ -15,6 +16,15 @@ namespace AppInstallerCLIE2ETests public class ConfigureShowCommand { /// <summary> + /// One time teardown. + /// </summary> + [OneTimeTearDown] + public void OneTimeTearDown() + { + WinGetSettingsHelper.ConfigureFeature("configuration03", false); + } + + /// <summary> /// Simple test to confirm that a resource without a module specified can be discovered in the PSGallery. /// </summary> [Test] @@ -62,5 +72,42 @@ namespace AppInstallerCLIE2ETests Assert.AreEqual(0, result.ExitCode); Assert.True(result.StdOut.Contains(Constants.LocalModuleDescriptor)); } + + /// <summary> + /// A schema 0.3 config file is not allowed without the experimental feature. + /// </summary> + [Test] + public void ShowDetails_Schema0_3_Fails() + { + var result = TestCommon.RunAICLICommand("configure show", TestCommon.GetTestDataFile("Configuration\\ShowDetails_TestRepo_0_3.yml")); + Assert.AreEqual(Constants.ErrorCode.ERROR_EXPERIMENTAL_FEATURE_DISABLED, result.ExitCode); + } + + /// <summary> + /// A schema 0.3 config file is allowed with the experimental feature. + /// </summary> + [Test] + public void ShowDetails_Schema0_3_Succeeds() + { + TestCommon.EnsureModuleState(Constants.SimpleTestModuleName, present: false); + WinGetSettingsHelper.ConfigureFeature("configuration03", true); + + var result = TestCommon.RunAICLICommand("configure show", TestCommon.GetTestDataFile("Configuration\\ShowDetails_TestRepo_0_3.yml")); + Assert.AreEqual(0, result.ExitCode); + Assert.True(result.StdOut.Contains(Constants.TestRepoName)); + } + + /// <summary> + /// A schema 0.3 config file with parameters is blocked. + /// </summary> + [Test] + public void ShowDetails_Schema0_3_Parameters() + { + WinGetSettingsHelper.ConfigureFeature("configuration03", true); + + var result = TestCommon.RunAICLICommand("configure show", TestCommon.GetTestDataFile("Configuration\\WithParameters_0_3.yml")); + Assert.AreEqual(0, result.ExitCode); + Assert.True(result.StdOut.Contains("Failed to get detailed information about the configuration.")); + } } } diff --git a/src/AppInstallerCLIE2ETests/ConfigureValidateCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureValidateCommand.cs @@ -34,7 +34,7 @@ namespace AppInstallerCLIE2ETests { var result = TestCommon.RunAICLICommand(Command, TestCommon.GetTestDataFile("Configuration\\NotConfig.yml")); Assert.AreEqual(Constants.ErrorCode.CONFIG_ERROR_MISSING_FIELD, result.ExitCode); - Assert.True(result.StdOut.Contains("properties")); + Assert.True(result.StdOut.Contains("$schema")); Assert.True(result.StdOut.Contains("missing")); } diff --git a/src/AppInstallerCLIE2ETests/TestData/Configuration/ShowDetails_TestRepo_0_3.yml b/src/AppInstallerCLIE2ETests/TestData/Configuration/ShowDetails_TestRepo_0_3.yml @@ -0,0 +1,9 @@ +$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json +resources: + - name: Name1 + type: xE2ETestResource/E2EFileResource + metadata: + repository: AppInstallerCLIE2ETestsRepo + properties: + prop1: 3 + prop2: '4' diff --git a/src/AppInstallerCLIE2ETests/TestData/Configuration/WithParameters_0_3.yml b/src/AppInstallerCLIE2ETests/TestData/Configuration/WithParameters_0_3.yml @@ -0,0 +1,13 @@ +$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json +parameters: + param1: + type: string + defaultValue: value +resources: + - name: Name1 + type: xE2ETestResource/E2EFileResource + metadata: + repository: AppInstallerCLIE2ETestsRepo + properties: + prop1: 3 + prop2: '4' diff --git a/src/AppInstallerCLIPackage/Package.appxmanifest b/src/AppInstallerCLIPackage/Package.appxmanifest @@ -79,6 +79,7 @@ <Interface Name="Windows.Foundation.Collections.IIterable`1<Microsoft.Management.Configuration.ConfigurationUnit>" InterfaceId="0BB82BF3-EC6D-55DB-B399-08813A4EB204" /> <Interface Name="Windows.Foundation.Collections.IIterable`1<Microsoft.Management.Configuration.ConfigurationSet>" InterfaceId="6D54B059-3766-5DC9-81E3-83587EB3A58E" /> <Interface Name="Windows.Foundation.Collections.IIterable`1<Microsoft.Management.Configuration.ConfigurationConflict>" InterfaceId="41A1F29F-518B-5776-BCF2-E42FC9DDE32A" /> + <Interface Name="Windows.Foundation.Collections.IIterable`1<Microsoft.Management.Configuration.ConfigurationParameter>" InterfaceId="4257159F-6172-5202-A45D-4C4303A6C2C2" /> <Interface Name="Windows.Foundation.Collections.IIterable`1<Microsoft.Management.Configuration.IConfigurationUnitSettingDetails>" InterfaceId="FC91924A-215F-50A7-9AEE-254B4D7A50CB" /> <Interface Name="Windows.Foundation.Collections.IIterable`1<Microsoft.Management.Configuration.ConfigurationConflictSetting>" InterfaceId="EB1E5A3C-A444-5394-B7B3-F1593937E31E" /> <Interface Name="Windows.Foundation.Collections.IIterable`1<Microsoft.Management.Configuration.GetConfigurationUnitDetailsResult>" InterfaceId="3A034399-0F2B-51C2-A9C5-4BC6E9940068" /> diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -44,6 +44,8 @@ namespace AppInstaller::Settings return userSettings.Get<Setting::EFWindowsFeature>(); case ExperimentalFeature::Feature::Resume: return userSettings.Get<Setting::EFResume>(); + case ExperimentalFeature::Feature::Configuration03: + return userSettings.Get<Setting::EFConfiguration03>(); case ExperimentalFeature::Feature::Reboot: return userSettings.Get<Setting::EFReboot>(); default: @@ -79,6 +81,8 @@ namespace AppInstaller::Settings return ExperimentalFeature{ "Windows Feature Dependencies", "windowsFeature", "https://aka.ms/winget-settings", Feature::WindowsFeature }; case Feature::Resume: return ExperimentalFeature{ "Resume", "resume", "https://aka.ms/winget-settings", Feature::Resume }; + case Feature::Configuration03: + return ExperimentalFeature{ "Configuration Schema 0.3", "configuration03", "https://aka.ms/winget-settings", Feature::Configuration03 }; case Feature::Reboot: return ExperimentalFeature{ "Reboot", "reboot", "https://aka.ms/winget-settings", Feature::Reboot }; default: diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -25,7 +25,8 @@ namespace AppInstaller::Settings DirectMSI = 0x1, WindowsFeature = 0x2, Resume = 0x4, - Reboot = 0x8, + Configuration03 = 0x8, + Reboot = 0x10, 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 @@ -72,6 +72,7 @@ namespace AppInstaller::Settings EFDirectMSI, EFWindowsFeature, EFResume, + EFConfiguration03, EFReboot, // Telemetry TelemetryDisable, @@ -150,6 +151,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFDirectMSI, bool, bool, false, ".experimentalFeatures.directMSI"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFWindowsFeature, bool, bool, false, ".experimentalFeatures.windowsFeature"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFResume, bool, bool, false, ".experimentalFeatures.resume"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFConfiguration03, bool, bool, false, ".experimentalFeatures.configuration03"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFReboot, bool, bool, false, ".experimentalFeatures.reboot"sv); // Telemetry SETTINGMAPPING_SPECIALIZATION(Setting::TelemetryDisable, bool, bool, false, ".telemetry.disable"sv); diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp @@ -261,6 +261,7 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(EFDirectMSI) WINGET_VALIDATE_PASS_THROUGH(EFWindowsFeature) WINGET_VALIDATE_PASS_THROUGH(EFResume) + WINGET_VALIDATE_PASS_THROUGH(EFConfiguration03) WINGET_VALIDATE_PASS_THROUGH(EFReboot) WINGET_VALIDATE_PASS_THROUGH(AnonymizePathForDisplay) WINGET_VALIDATE_PASS_THROUGH(TelemetryDisable) diff --git a/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj b/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj @@ -403,6 +403,7 @@ <ClInclude Include="Public\winget\Certificates.h" /> <ClInclude Include="Public\winget\ConfigurationSetProcessorHandlers.h" /> <ClInclude Include="Public\winget\GroupPolicy.h" /> + <ClInclude Include="Public\winget\IConfigurationStaticsInternals.h" /> <ClInclude Include="Public\winget\ILifetimeWatcher.h" /> <ClInclude Include="Public\winget\JsonSchemaValidation.h" /> <ClInclude Include="Public\winget\JsonUtil.h" /> diff --git a/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj.filters b/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj.filters @@ -101,6 +101,9 @@ <ClInclude Include="Public\winget\JsonUtil.h"> <Filter>Public\winget</Filter> </ClInclude> + <ClInclude Include="Public\winget\IConfigurationStaticsInternals.h"> + <Filter>Public\winget</Filter> + </ClInclude> <ClInclude Include="Public\winget\SQLiteStatementBuilder.h"> <Filter>Public\winget</Filter> </ClInclude> diff --git a/src/AppInstallerSharedLib/Public/AppInstallerLogging.h b/src/AppInstallerSharedLib/Public/AppInstallerLogging.h @@ -11,11 +11,11 @@ #include <type_traits> #include <vector> -#define AICLI_LOG(_channel_,_level_,_outstream_) \ +#define AICLI_LOG_DIRECT(_logger_,_channel_,_level_,_outstream_) \ do { \ auto _aicli_log_channel = AppInstaller::Logging::Channel:: _channel_; \ auto _aicli_log_level = AppInstaller::Logging::Level:: _level_; \ - auto& _aicli_log_log = AppInstaller::Logging::Log(); \ + auto& _aicli_log_log = _logger_; \ if (_aicli_log_log.IsEnabled(_aicli_log_channel, _aicli_log_level)) \ { \ AppInstaller::Logging::LoggingStream _aicli_log_strstr; \ @@ -24,6 +24,8 @@ } \ } while (0, 0) +#define AICLI_LOG(_channel_,_level_,_outstream_) AICLI_LOG_DIRECT(AppInstaller::Logging::Log(),_channel_,_level_,_outstream_) + // Consider using this macro when the string might be larger than 4K. // The normal macro has some buffering that occurs; it can cut off larger strings and is slower. #define AICLI_LOG_LARGE_STRING(_channel_,_level_,_headerStream_,_largeString_) \ diff --git a/src/AppInstallerSharedLib/Public/winget/IConfigurationStaticsInternals.h b/src/AppInstallerSharedLib/Public/winget/IConfigurationStaticsInternals.h @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <Unknwn.h> +#include <winrt/Windows.Foundation.h> + +namespace AppInstaller::WinRT +{ + // Flag values for SetExperimentalState. + enum class ConfigurationStaticsInternalsStateFlags : UINT32 + { + None = 0, + Configuration03 = 0x1, + All = Configuration03 + }; + + DEFINE_ENUM_FLAG_OPERATORS(ConfigurationStaticsInternalsStateFlags); + + MIDL_INTERFACE("C3886148-148A-4A3D-8018-9CDACDFC0B8D") + IConfigurationStaticsInternals : public IUnknown + { + public: + virtual /* [local] */ HRESULT STDMETHODCALLTYPE SetExperimentalState( + UINT32 state) = 0; + }; +} diff --git a/src/Microsoft.Management.Configuration.OutOfProc/Prepare-ConfigurationOOPTests.ps1 b/src/Microsoft.Management.Configuration.OutOfProc/Prepare-ConfigurationOOPTests.ps1 @@ -29,7 +29,7 @@ if (-not [System.String]::IsNullOrEmpty($PackageLayoutPath)) # Configure crash dump and log file settings $Local:settingsExport = ConvertFrom-Json (wingetdev.exe settings export) $Local:settingsFilePath = $Local:settingsExport.userSettingsFile - $Local:settingsFileContent = ConvertTo-Json @{ debugging= @{ enableSelfInitiatedMinidump=$true ; keepAllLogFiles=$true } } + $Local:settingsFileContent = ConvertTo-Json @{ debugging= @{ enableSelfInitiatedMinidump=$true ; keepAllLogFiles=$true } ; experimentalFeatures= @{ configuration03=$true } } Set-Content -Path $Local:settingsFilePath -Value $Local:settingsFileContent } diff --git a/src/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitAndResource.cs b/src/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitAndResource.cs @@ -29,7 +29,7 @@ namespace Microsoft.Management.Configuration.Processor.Helpers ConfigurationUnitInternal configurationUnitInternal, DscResourceInfoInternal dscResourceInfoInternal) { - if (!configurationUnitInternal.Unit.Type.Equals(dscResourceInfoInternal.Name, StringComparison.OrdinalIgnoreCase)) + if (!configurationUnitInternal.ResourceName.Equals(dscResourceInfoInternal.Name, StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException(); } diff --git a/src/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitInternal.cs b/src/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitInternal.cs @@ -8,6 +8,7 @@ namespace Microsoft.Management.Configuration.Processor.Helpers { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.IO; using Microsoft.Management.Configuration.Processor.Constants; using Microsoft.Management.Configuration.Processor.Exceptions; @@ -15,8 +16,8 @@ namespace Microsoft.Management.Configuration.Processor.Helpers using Windows.Foundation.Collections; /// <summary> - /// Wrapper around Configuration units and its directives. Creates a normalized directives map - /// for consumption. + /// Wrapper around Configuration units and its directives. + /// Creates a normalized directives map for consumption. /// </summary> internal class ConfigurationUnitInternal { @@ -32,10 +33,11 @@ namespace Microsoft.Management.Configuration.Processor.Helpers /// <param name="configurationFilePath">The configuration file path.</param> public ConfigurationUnitInternal( ConfigurationUnit unit, - string configurationFilePath) + string? configurationFilePath) { this.Unit = unit; this.InitializeDirectives(); + this.InitializeNames(); string? moduleName = this.GetDirective<string>(DirectiveConstants.Module); if (string.IsNullOrEmpty(moduleName)) @@ -69,18 +71,24 @@ namespace Microsoft.Management.Configuration.Processor.Helpers public ConfigurationUnit Unit { get; } /// <summary> + /// Gets a value indicating whether the unit type should be treated as the resource name. + /// </summary> + public bool UnitTypeIsResourceName { get; init; } = false; + + /// <summary> /// Gets the module specification. /// </summary> public ModuleSpecification? Module { get; } /// <summary> - /// Creates a string that identifies this unit for diagnostics. + /// Gets the resource name *only*. For example, "Resource". /// </summary> - /// <returns>The string that identifies this unit for diagnostics.</returns> - public string ToIdentifyingString() - { - return $"{this.Unit.Type} [{this.Module?.ToString() ?? "<no module>"}]"; - } + public string ResourceName { get; private set; } + + /// <summary> + /// Gets the qualified name, which includes the module. For example, "Module/Resource". + /// </summary> + public string QualifiedName { get; private set; } /// <summary> /// Gets the directive value from the unit taking into account the directives overlay. @@ -197,12 +205,7 @@ namespace Microsoft.Management.Configuration.Processor.Helpers { if (string.IsNullOrEmpty(this.configurationFileRootPath)) { - throw new UnitSettingConfigRootException(this.Unit.Type, settingName); - } - - if (this.configurationFileRootPath == null) - { - throw new ArgumentException(); + throw new UnitSettingConfigRootException(this.QualifiedName, settingName); } return value.Replace(ConfigRootVar, this.configurationFileRootPath, StringComparison.OrdinalIgnoreCase); @@ -220,5 +223,62 @@ namespace Microsoft.Management.Configuration.Processor.Helpers this.normalizedDirectives.Add(normalizedKey, directive.Value); } } + + private string ConstructQualifiedName(string? moduleName) + { + return $"{(moduleName == null ? string.Empty : $"{moduleName}/")}{this.ResourceName}"; + } + + [MemberNotNull(nameof(ResourceName), nameof(QualifiedName))] + private void InitializeNames() + { + // Determine ResourceName, QualifiedName, and the module directive + string unitType = this.Unit.Type; + string? moduleDirective = this.GetDirective<string>(DirectiveConstants.Module); + + if (this.UnitTypeIsResourceName) + { + this.ResourceName = unitType; + this.QualifiedName = this.ConstructQualifiedName(moduleDirective); + return; + } + + int unitTypeDividerPosition = unitType.IndexOf('/'); + + if (unitTypeDividerPosition == unitType.Length - 1) + { + throw new ArgumentException($"Invalid unit Type: {unitType}"); + } + + string? moduleName; + + if (unitTypeDividerPosition == -1) + { + moduleName = moduleDirective; + this.ResourceName = unitType; + this.QualifiedName = this.ConstructQualifiedName(moduleDirective); + } + else + { + moduleName = unitType.Substring(0, unitTypeDividerPosition); + this.ResourceName = unitType.Substring(unitTypeDividerPosition + 1); + this.QualifiedName = unitType; + } + + if (moduleName != null) + { + if (moduleDirective != null) + { + if (moduleName != moduleDirective) + { + throw new ArgumentException($"Mismatched module specifiers: {moduleName} != {moduleDirective}"); + } + } + else + { + this.normalizedDirectives.Add(DirectiveConstants.Module, moduleName); + } + } + } } } diff --git a/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/HostedEnvironment.cs b/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/HostedEnvironment.cs @@ -131,7 +131,7 @@ namespace Microsoft.Management.Configuration.Processor.Runspaces public DscResourceInfoInternal? GetDscResource(ConfigurationUnitInternal unitInternal) { using PowerShell pwsh = PowerShell.Create(this.Runspace); - var result = this.DscModule.GetDscResource(pwsh, unitInternal.Unit.Type, unitInternal.Module); + var result = this.DscModule.GetDscResource(pwsh, unitInternal.ResourceName, unitInternal.Module); this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); return result; } @@ -298,7 +298,7 @@ namespace Microsoft.Management.Configuration.Processor.Runspaces var result = this.powerShellGet.FindDscResource( pwsh, - unitInternal.Unit.Type, + unitInternal.ResourceName, unitInternal.GetDirective<string>(DirectiveConstants.Module), unitInternal.GetSemanticVersion(), unitInternal.GetSemanticMinVersion(), diff --git a/src/Microsoft.Management.Configuration.Processor/Public/PowerShellConfigurationSetProcessorFactory.cs b/src/Microsoft.Management.Configuration.Processor/Public/PowerShellConfigurationSetProcessorFactory.cs @@ -68,11 +68,17 @@ namespace Microsoft.Management.Configuration.Processor /// </summary> /// <param name="set">Configuration Set.</param> /// <returns>Configuration set processor.</returns> - public IConfigurationSetProcessor CreateSetProcessor(ConfigurationSet set) + public IConfigurationSetProcessor CreateSetProcessor(ConfigurationSet? set) { try { - this.OnDiagnostics(DiagnosticLevel.Verbose, $"Creating set processor for `{set.Name}`..."); + this.OnDiagnostics(DiagnosticLevel.Verbose, $"Creating set processor for `{set?.Name ?? "<null>"}`..."); + + if (set != null && (set.Parameters.Count > 0 || set.Variables.Count > 0)) + { + this.OnDiagnostics(DiagnosticLevel.Error, $" Parameters/variables are not yet supported."); + throw new NotImplementedException(); + } var envFactory = new ProcessorEnvironmentFactory(this.ProcessorType); var processorEnvironment = envFactory.CreateEnvironment( diff --git a/src/Microsoft.Management.Configuration.Processor/Set/ConfigurationSetProcessor.cs b/src/Microsoft.Management.Configuration.Processor/Set/ConfigurationSetProcessor.cs @@ -23,14 +23,14 @@ namespace Microsoft.Management.Configuration.Processor.Set /// </summary> internal sealed class ConfigurationSetProcessor : IConfigurationSetProcessor { - private readonly ConfigurationSet configurationSet; + private readonly ConfigurationSet? configurationSet; /// <summary> /// Initializes a new instance of the <see cref="ConfigurationSetProcessor"/> class. /// </summary> /// <param name="processorEnvironment">The processor environment.</param> /// <param name="configurationSet">Configuration set.</param> - public ConfigurationSetProcessor(IProcessorEnvironment processorEnvironment, ConfigurationSet configurationSet) + public ConfigurationSetProcessor(IProcessorEnvironment processorEnvironment, ConfigurationSet? configurationSet) { this.ProcessorEnvironment = processorEnvironment; this.configurationSet = configurationSet; @@ -56,8 +56,8 @@ namespace Microsoft.Management.Configuration.Processor.Set { try { - var configurationUnitInternal = new ConfigurationUnitInternal(unit, this.configurationSet.Path); - this.OnDiagnostics(DiagnosticLevel.Verbose, $"Creating unit processor for: {configurationUnitInternal.ToIdentifyingString()}..."); + var configurationUnitInternal = new ConfigurationUnitInternal(unit, this.configurationSet?.Path) { UnitTypeIsResourceName = IsUnitTypeResourceName(this.configurationSet?.SchemaVersion) }; + this.OnDiagnostics(DiagnosticLevel.Verbose, $"Creating unit processor for: {configurationUnitInternal.QualifiedName}..."); var dscResourceInfo = this.PrepareUnitForProcessing(configurationUnitInternal); @@ -87,8 +87,8 @@ namespace Microsoft.Management.Configuration.Processor.Set { try { - var unitInternal = new ConfigurationUnitInternal(unit, this.configurationSet.Path); - this.OnDiagnostics(DiagnosticLevel.Verbose, $"Getting unit details [{detailFlags}] for: {unitInternal.ToIdentifyingString()}"); + var unitInternal = new ConfigurationUnitInternal(unit, this.configurationSet?.Path); + this.OnDiagnostics(DiagnosticLevel.Verbose, $"Getting unit details [{detailFlags}] for: {unitInternal.QualifiedName}"); // (Local | Download | Load) will all work off of local files, so if any one is an option just use the local module info if found. DscResourceInfoInternal? dscResourceInfo = null; @@ -160,7 +160,7 @@ namespace Microsoft.Management.Configuration.Processor.Set { // Well, this is awkward. throw new InstallDscResourceException( - unit.Type, + unitInternal.ResourceName, PowerShellHelpers.CreateModuleSpecification(foundModuleInfo.Name, foundModuleInfo.Version)); } @@ -176,6 +176,11 @@ namespace Microsoft.Management.Configuration.Processor.Set } } + private static bool IsUnitTypeResourceName(string? schemaVersion) + { + return schemaVersion != null && schemaVersion == "0.1"; + } + /// <summary> /// Finds the module and preferred resource name for processing the configuration unit. /// </summary> @@ -193,7 +198,7 @@ namespace Microsoft.Management.Configuration.Processor.Set foundModule = this.ProcessorEnvironment.FindModule(unitInternal); if (foundModule != null) { - resourceName = unitInternal.Unit.Type; + resourceName = unitInternal.ResourceName; } } else @@ -236,7 +241,7 @@ namespace Microsoft.Management.Configuration.Processor.Set if (findUnitModuleResult is null) { - throw new FindDscResourceNotFoundException(unitInternal.Unit.Type, unitInternal.Module); + throw new FindDscResourceNotFoundException(unitInternal.ResourceName, unitInternal.Module); } this.ProcessorEnvironment.InstallModule(findUnitModuleResult.Value.Module); @@ -245,7 +250,7 @@ namespace Microsoft.Management.Configuration.Processor.Set dscResourceInfo = this.ProcessorEnvironment.GetDscResource(unitInternal); if (dscResourceInfo is null) { - throw new InstallDscResourceException(unitInternal.Unit.Type, unitInternal.Module); + throw new InstallDscResourceException(unitInternal.ResourceName, unitInternal.Module); } } diff --git a/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessor.cs b/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessor.cs @@ -52,7 +52,7 @@ namespace Microsoft.Management.Configuration.Processor.Unit /// <returns>A <see cref="IGetSettingsResult"/>.</returns> public IGetSettingsResult GetSettings() { - this.OnDiagnostics(DiagnosticLevel.Verbose, $"Invoking `Get` for resource: {this.unitResource.UnitInternal.ToIdentifyingString()}..."); + this.OnDiagnostics(DiagnosticLevel.Verbose, $"Invoking `Get` for resource: {this.unitResource.UnitInternal.QualifiedName}..."); var result = new GetSettingsResult(this.Unit); @@ -79,7 +79,7 @@ namespace Microsoft.Management.Configuration.Processor.Unit /// <returns>A <see cref="ITestSettingsResult"/>.</returns> public ITestSettingsResult TestSettings() { - this.OnDiagnostics(DiagnosticLevel.Verbose, $"Invoking `Test` for resource: {this.unitResource.UnitInternal.ToIdentifyingString()}..."); + this.OnDiagnostics(DiagnosticLevel.Verbose, $"Invoking `Test` for resource: {this.unitResource.UnitInternal.QualifiedName}..."); if (this.Unit.Intent == ConfigurationUnitIntent.Inform) { @@ -114,7 +114,7 @@ namespace Microsoft.Management.Configuration.Processor.Unit /// <returns>A <see cref="IApplySettingsResult"/>.</returns> public IApplySettingsResult ApplySettings() { - this.OnDiagnostics(DiagnosticLevel.Verbose, $"Invoking `Apply` for resource: {this.unitResource.UnitInternal.ToIdentifyingString()}..."); + this.OnDiagnostics(DiagnosticLevel.Verbose, $"Invoking `Apply` for resource: {this.unitResource.UnitInternal.QualifiedName}..."); if (this.Unit.Intent == ConfigurationUnitIntent.Inform || this.Unit.Intent == ConfigurationUnitIntent.Assert) diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/TestConfigurationUnitProcessor.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/TestConfigurationUnitProcessor.cs @@ -42,6 +42,13 @@ namespace Microsoft.Management.Configuration.UnitTests.Helpers internal delegate ITestSettingsResult TestSettingsDelegateType(); /// <summary> + /// The delegate for TestSettings that passes the unit in. + /// </summary> + /// <param name="unit">The unit.</param> + /// <returns>The result.</returns> + internal delegate ITestSettingsResult TestSettingsDelegateWithUnitType(ConfigurationUnit unit); + + /// <summary> /// Gets or sets the directives overlay. /// </summary> public IReadOnlyDictionary<string, object>? DirectivesOverlay { get; set; } @@ -77,6 +84,11 @@ namespace Microsoft.Management.Configuration.UnitTests.Helpers internal TestSettingsDelegateType? TestSettingsDelegate { get; set; } /// <summary> + /// Gets or sets the delegate object for TestSettings that takes in the unit. + /// </summary> + internal TestSettingsDelegateWithUnitType? TestSettingsDelegateWithUnit { get; set; } + + /// <summary> /// Gets the number of times TestSettings is called. /// </summary> internal int TestSettingsCalls { get; private set; } = 0; @@ -122,7 +134,11 @@ namespace Microsoft.Management.Configuration.UnitTests.Helpers public ITestSettingsResult TestSettings() { ++this.TestSettingsCalls; - if (this.TestSettingsDelegate != null) + if (this.TestSettingsDelegateWithUnit != null) + { + return this.TestSettingsDelegateWithUnit(this.Unit); + } + else if (this.TestSettingsDelegate != null) { return this.TestSettingsDelegate(); } diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationProcessorTestTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationProcessorTestTests.cs @@ -216,6 +216,30 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests this.RunTestSetTestForResultTypes(new ConfigurationTestResult[] { ConfigurationTestResult.Positive, ConfigurationTestResult.Positive, ConfigurationTestResult.Positive, ConfigurationTestResult.NotRun }, ConfigurationTestResult.Positive); } + private TestSettingsResultInstance PositiveResult(ConfigurationUnit unit) + { + TestSettingsResultInstance positiveResult = new TestSettingsResultInstance(unit); + positiveResult.TestResult = ConfigurationTestResult.Positive; + return positiveResult; + } + + private TestSettingsResultInstance NegativeResult(ConfigurationUnit unit) + { + TestSettingsResultInstance negativeResult = new TestSettingsResultInstance(unit); + negativeResult.TestResult = ConfigurationTestResult.Negative; + return negativeResult; + } + + private TestSettingsResultInstance FailedResult(ConfigurationUnit unit, string description, ConfigurationUnitResultSource resultSource) + { + TestSettingsResultInstance failedResult = new TestSettingsResultInstance(unit); + failedResult.TestResult = ConfigurationTestResult.Failed; + failedResult.InternalResult.ResultCode = new NullReferenceException(); + failedResult.InternalResult.Description = description; + failedResult.InternalResult.ResultSource = resultSource; + return failedResult; + } + /// <summary> /// Creates a test scenario where the units produce the given test results. /// </summary> @@ -229,17 +253,8 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests TestConfigurationProcessorFactory factory = new TestConfigurationProcessorFactory(); TestConfigurationSetProcessor setProcessor = factory.CreateTestProcessor(configurationSet); - TestSettingsResultInstance positiveResult = new TestSettingsResultInstance(configurationUnits[0]); - positiveResult.TestResult = ConfigurationTestResult.Positive; - - TestSettingsResultInstance negativeResult = new TestSettingsResultInstance(configurationUnits[0]); - negativeResult.TestResult = ConfigurationTestResult.Negative; - - TestSettingsResultInstance failedResult = new TestSettingsResultInstance(configurationUnits[0]); - failedResult.TestResult = ConfigurationTestResult.Failed; - failedResult.InternalResult.ResultCode = new NullReferenceException(); - failedResult.InternalResult.Description = "Failed again"; - failedResult.InternalResult.ResultSource = ConfigurationUnitResultSource.UnitProcessing; + string failedDescription = "Failed again"; + ConfigurationUnitResultSource failedResultSource = ConfigurationUnitResultSource.UnitProcessing; for (int i = 0; i < resultTypes.Length; ++i) { @@ -250,16 +265,16 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests switch (resultTypes[i]) { case ConfigurationTestResult.Positive: - unitProcessor.TestSettingsDelegate = () => positiveResult; + unitProcessor.TestSettingsDelegateWithUnit = (ConfigurationUnit unit) => this.PositiveResult(unit); break; case ConfigurationTestResult.Negative: - unitProcessor.TestSettingsDelegate = () => negativeResult; + unitProcessor.TestSettingsDelegateWithUnit = (ConfigurationUnit unit) => this.NegativeResult(unit); break; case ConfigurationTestResult.NotRun: configurationUnits[i].Intent = ConfigurationUnitIntent.Inform; break; case ConfigurationTestResult.Failed: - unitProcessor.TestSettingsDelegate = () => failedResult; + unitProcessor.TestSettingsDelegateWithUnit = (ConfigurationUnit unit) => this.FailedResult(unit, failedDescription, failedResultSource); break; } } @@ -298,8 +313,8 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests case ConfigurationTestResult.Failed: Assert.NotNull(unitResult.ResultInformation.ResultCode); Assert.IsType<NullReferenceException>(unitResult.ResultInformation.ResultCode); - Assert.Equal(failedResult.ResultInformation.Description, unitResult.ResultInformation.Description); - Assert.Equal(failedResult.ResultInformation.ResultSource, unitResult.ResultInformation.ResultSource); + Assert.Equal(failedDescription, unitResult.ResultInformation.Description); + Assert.Equal(failedResultSource, unitResult.ResultInformation.ResultSource); summaryEventResult = unitResult.ResultInformation.ResultCode.HResult; resultSource = unitResult.ResultInformation.ResultSource; break; diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationUnitInternalTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationUnitInternalTests.cs @@ -59,7 +59,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests string boolDirective2 = "boolDirective2"; bool boolDirective2Value = false; - var unit = new ConfigurationUnit(); + var unit = new ConfigurationUnit().Assign(new { Type = $"{unitModule}/unitResource" }); unit.Metadata.Add(moduleDirective, unitModule); unit.Metadata.Add(versionDirective, unitVersion); unit.Metadata.Add(descriptionDirective, unitDescription); @@ -108,7 +108,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests { using var tmpFile = new TempFile("fakeConfigFile.yml", content: "content"); - var unit = new ConfigurationUnit(); + var unit = new ConfigurationUnit().Assign(new { Type = "unitModule/unitResource" }); unit.Settings.Add("var1", @"$WinGetConfigRoot\this\is\a\path.txt"); unit.Settings.Add("var2", @"${WinGetConfigRoot}\this\is\a\path.txt"); unit.Settings.Add("var3", @"this\is\a\$WINGETCONFIGROOT\path.txt"); @@ -147,7 +147,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests [Fact] public void GetExpandedSetting_ConfigRoot_Throw() { - var unit = new ConfigurationUnit(); + var unit = new ConfigurationUnit().Assign(new { Type = "unitModule/unitResource" }); unit.Settings.Add("var2", @"${WinGetConfigRoot}\this\is\a\path.txt"); var unitInternal = new ConfigurationUnitInternal(unit, null!); diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/OpenConfigurationSetTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/OpenConfigurationSetTests.cs @@ -7,11 +7,14 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests { using System; + using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using Microsoft.Management.Configuration.UnitTests.Fixtures; using Microsoft.Management.Configuration.UnitTests.Helpers; using Microsoft.VisualBasic; + using Newtonsoft.Json.Linq; + using Windows.Foundation.Collections; using Xunit; using Xunit.Abstractions; @@ -97,7 +100,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests Assert.Null(result.Set); Assert.NotNull(result.ResultCode); Assert.Equal(Errors.WINGET_CONFIG_ERROR_MISSING_FIELD, result.ResultCode.HResult); - Assert.Equal("properties", result.Field); + Assert.Equal("$schema", result.Field); Assert.Equal(0U, result.Line); Assert.Equal(0U, result.Column); } @@ -463,5 +466,243 @@ properties: Assert.Equal(5U, result.Line); Assert.NotEqual(0U, result.Column); } + + /// <summary> + /// Test for using version 0.3 schema. + /// </summary> + [Fact] + public void BasicVersion_0_3() + { + ConfigurationProcessor processor = this.CreateConfigurationProcessorWithDiagnostics(); + + OpenConfigurationSetResult result = processor.OpenConfigurationSet(this.CreateStream(@" +$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json +metadata: + a: 1 + b: '2' +variables: + v1: var1 + v2: 42 +resources: + - name: Name + type: Module/Resource + metadata: + e: '5' + f: 6 + properties: + c: 3 + d: '4' + dependsOn: + - g + - h + - name: Name2 + type: Module/Resource2 + dependsOn: + - m + properties: + l: '10' + metadata: + i: '7' + j: 8 + q: 42 +")); + + Assert.Null(result.ResultCode); + Assert.NotNull(result.Set); + Assert.Equal(string.Empty, result.Field); + Assert.Equal(string.Empty, result.Value); + Assert.Equal(0U, result.Line); + Assert.Equal(0U, result.Column); + + ConfigurationSet set = result.Set; + + Assert.Equal("0.3", set.SchemaVersion); + Assert.NotNull(set.SchemaUri); + Assert.Equal("https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json", set.SchemaUri.ToString()); + + this.VerifyValueSet(set.Metadata, new ("a", 1), new ("b", "2")); + this.VerifyValueSet(set.Variables, new ("v1", "var1"), new ("v2", 42)); + + Assert.Empty(set.Parameters); + + Assert.Equal(2, set.Units.Count); + + this.VerifyUnitProperties(set.Units[0], "Name", "Module/Resource"); + this.VerifyValueSet(set.Units[0].Metadata, new ("e", "5"), new ("f", 6)); + this.VerifyValueSet(set.Units[0].Settings, new ("c", 3), new ("d", "4")); + this.VerifyStringArray(set.Units[0].Dependencies, "g", "h"); + + this.VerifyUnitProperties(set.Units[1], "Name2", "Module/Resource2"); + this.VerifyValueSet(set.Units[1].Metadata, new ("i", "7"), new ("j", 8), new ("q", 42)); + this.VerifyValueSet(set.Units[1].Settings, new KeyValuePair<string, object>("l", "10")); + this.VerifyStringArray(set.Units[1].Dependencies, "m"); + } + + /// <summary> + /// Test for the successful parsing of default value of a parameter. + /// </summary> + /// <param name="type">The type.</param> + /// <param name="defaultValue">The default value.</param> + /// <param name="expectedValue">The expected value.</param> + /// <param name="expectedType">The expected type.</param> + /// <param name="secure">The secure state.</param> + [Theory] + [InlineData("string", "abc", "abc", Windows.Foundation.PropertyType.String)] + [InlineData("string", "'42'", "42", Windows.Foundation.PropertyType.String)] + [InlineData("securestring", "abcdef", "abcdef", Windows.Foundation.PropertyType.String, true)] + [InlineData("int", "42", 42, Windows.Foundation.PropertyType.Int64)] + [InlineData("bool", "true", true, Windows.Foundation.PropertyType.Boolean)] + [InlineData("object", "string", "string", Windows.Foundation.PropertyType.Inspectable)] + [InlineData("object", "42", 42, Windows.Foundation.PropertyType.Inspectable)] + [InlineData("secureobject", "string", "string", Windows.Foundation.PropertyType.Inspectable, true)] + [InlineData("secureobject", "42", 42, Windows.Foundation.PropertyType.Inspectable, true)] + public void Parameters_DefaultValue_Success(string type, string defaultValue, object expectedValue, Windows.Foundation.PropertyType expectedType, bool secure = false) + { + this.TestParameterDefaultValue(type, defaultValue, expectedValue, expectedType, secure); + } + + /// <summary> + /// Test for the failed parsing of default value of a parameter. + /// </summary> + /// <param name="type">The type.</param> + /// <param name="defaultValue">The default value.</param> + /// <param name="expectedValue">The expected value.</param> + [Theory] + [InlineData("string", "42")] + [InlineData("int", "abc")] + [InlineData("int", "'42'", "42")] + [InlineData("bool", "'true'", "true")] + public void Parameters_DefaultValue_Failure(string type, string defaultValue, object? expectedValue = null) + { + this.TestParameterDefaultValue(type, defaultValue, expectedValue); + } + + /// <summary> + /// Test to ensure that schema version and uri is working as expected. + /// </summary> + /// <param name="version">The version.</param> + /// <param name="uri">The uri.</param> + [Theory] + [InlineData("0.1", null)] + [InlineData("0.2", null)] + [InlineData("0.3", "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json")] + public void Schema_Version_Uri(string version, string? uri) + { + ConfigurationSet set = this.ConfigurationSet(); + + set.SchemaVersion = version; + if (uri != null) + { + Assert.Equal(uri, set.SchemaUri.AbsoluteUri); + } + else + { + Assert.Null(set.SchemaUri); + } + + if (!string.IsNullOrEmpty(uri)) + { + set.SchemaUri = new Uri(uri); + Assert.Equal(version, set.SchemaVersion); + } + } + + private void TestParameterDefaultValue(string type, string defaultValue, object? expectedValue = null, Windows.Foundation.PropertyType? expectedType = null, bool secure = false) + { + ConfigurationProcessor processor = this.CreateConfigurationProcessorWithDiagnostics(); + + OpenConfigurationSetResult result = processor.OpenConfigurationSet(this.CreateStream(string.Format( + @" +$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json +parameters: + {0}: + type: {0} + defaultValue: {1} +", + type, + defaultValue))); + + if (expectedType != null) + { + Assert.Null(result.ResultCode); + Assert.NotNull(result.Set); + Assert.Equal(string.Empty, result.Field); + Assert.Equal(string.Empty, result.Value); + Assert.Equal(0U, result.Line); + Assert.Equal(0U, result.Column); + + var parameters = result.Set.Parameters; + Assert.NotNull(parameters); + Assert.Single(parameters); + + Assert.Equal(type, parameters[0].Name); + Assert.Equal(expectedType, parameters[0].Type); + Assert.Equal(secure, parameters[0].IsSecure); + + switch (expectedValue ?? throw new ArgumentException("expectedValue")) + { + case int i: + Assert.Equal(i, (int)(long)parameters[0].DefaultValue); + break; + case string s: + Assert.Equal(s, (string)parameters[0].DefaultValue); + break; + case bool b: + Assert.Equal(b, (bool)parameters[0].DefaultValue); + break; + default: + Assert.Fail($"Add expected type `{expectedValue.GetType().Name}` to switch statement."); + break; + } + } + else + { + Assert.NotNull(result.ResultCode); + Assert.Equal(Errors.WINGET_CONFIG_ERROR_INVALID_FIELD_VALUE, result.ResultCode.HResult); + Assert.Null(result.Set); + Assert.Equal("defaultValue", result.Field); + Assert.Equal(expectedValue?.ToString() ?? defaultValue, result.Value); + Assert.NotEqual(0U, result.Line); + Assert.NotEqual(0U, result.Column); + } + } + + private void VerifyUnitProperties(ConfigurationUnit unit, string identifier, string type) + { + Assert.NotNull(unit); + Assert.Equal(identifier, unit.Identifier); + Assert.Equal(type, unit.Type); + } + + private void VerifyValueSet(ValueSet values, params KeyValuePair<string, object>[] expected) + { + Assert.NotNull(values); + Assert.Equal(expected.Length, values.Count); + + foreach (var expectation in expected) + { + Assert.True(values.ContainsKey(expectation.Key)); + object value = values[expectation.Key]; + + switch (expectation.Value) + { + case int i: + Assert.Equal(i, (int)(long)value); + break; + case string s: + Assert.Equal(s, (string)value); + break; + default: + Assert.Fail($"Add expected type `{expectation.Value.GetType().Name}` to switch statement."); + break; + } + } + } + + private void VerifyStringArray(IList<string> strings, params string[] expected) + { + Assert.NotNull(strings); + Assert.Equal(expected.Length, strings.Count); + } } } diff --git a/src/Microsoft.Management.Configuration/ArgumentValidation.cpp b/src/Microsoft.Management.Configuration/ArgumentValidation.cpp @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include <pch.h> +#include "ArgumentValidation.h" + +namespace winrt::Microsoft::Management::Configuration::implementation +{ + void EnsureSupportedType(Windows::Foundation::PropertyType type) + { + switch (type) + { + case winrt::Windows::Foundation::PropertyType::UInt8: + case winrt::Windows::Foundation::PropertyType::Int16: + case winrt::Windows::Foundation::PropertyType::UInt16: + case winrt::Windows::Foundation::PropertyType::Int32: + case winrt::Windows::Foundation::PropertyType::UInt32: + case winrt::Windows::Foundation::PropertyType::Int64: + case winrt::Windows::Foundation::PropertyType::UInt64: + case winrt::Windows::Foundation::PropertyType::Single: + case winrt::Windows::Foundation::PropertyType::Double: + case winrt::Windows::Foundation::PropertyType::Char16: + case winrt::Windows::Foundation::PropertyType::Boolean: + case winrt::Windows::Foundation::PropertyType::String: + case winrt::Windows::Foundation::PropertyType::Inspectable: + case winrt::Windows::Foundation::PropertyType::DateTime: + case winrt::Windows::Foundation::PropertyType::TimeSpan: + case winrt::Windows::Foundation::PropertyType::Guid: + case winrt::Windows::Foundation::PropertyType::UInt8Array: + case winrt::Windows::Foundation::PropertyType::Int16Array: + case winrt::Windows::Foundation::PropertyType::UInt16Array: + case winrt::Windows::Foundation::PropertyType::Int32Array: + case winrt::Windows::Foundation::PropertyType::UInt32Array: + case winrt::Windows::Foundation::PropertyType::Int64Array: + case winrt::Windows::Foundation::PropertyType::UInt64Array: + case winrt::Windows::Foundation::PropertyType::SingleArray: + case winrt::Windows::Foundation::PropertyType::DoubleArray: + case winrt::Windows::Foundation::PropertyType::Char16Array: + case winrt::Windows::Foundation::PropertyType::BooleanArray: + case winrt::Windows::Foundation::PropertyType::StringArray: + case winrt::Windows::Foundation::PropertyType::InspectableArray: + case winrt::Windows::Foundation::PropertyType::DateTimeArray: + case winrt::Windows::Foundation::PropertyType::TimeSpanArray: + case winrt::Windows::Foundation::PropertyType::GuidArray: + return; + } + + THROW_HR(E_INVALIDARG); + } + + bool IsValidObjectType(Windows::Foundation::IInspectable const& value, Windows::Foundation::PropertyType type) + { + auto propertyValue = value.try_as<Windows::Foundation::IPropertyValue>(); + + // If the type is an object, it is acceptable for the value to be a ValueSet directly + if (type == Windows::Foundation::PropertyType::Inspectable && + (propertyValue || value.try_as<Windows::Foundation::Collections::ValueSet>())) + { + return true; + } + + // If it wasn't an object type and a ValueSet, it must be an IPropertyValue + if (!propertyValue) + { + return false; + } + + // If it is an IPropertyValue, it must have the required type + return (propertyValue.Type() == type); + } + + void EnsureObjectType(Windows::Foundation::IInspectable const& value, Windows::Foundation::PropertyType type) + { + THROW_HR_IF(E_INVALIDARG, !IsValidObjectType(value, type)); + } + + bool IsComparableType(Windows::Foundation::PropertyType type) + { + switch (type) + { + case Windows::Foundation::PropertyType::UInt8: + case Windows::Foundation::PropertyType::Int16: + case Windows::Foundation::PropertyType::UInt16: + case Windows::Foundation::PropertyType::Int32: + case Windows::Foundation::PropertyType::UInt32: + case Windows::Foundation::PropertyType::Int64: + case Windows::Foundation::PropertyType::UInt64: + case Windows::Foundation::PropertyType::Single: + case Windows::Foundation::PropertyType::Double: + case Windows::Foundation::PropertyType::Char16: + case Windows::Foundation::PropertyType::DateTime: + case Windows::Foundation::PropertyType::TimeSpan: + return true; + } + + return false; + } + + void EnsureComparableType(Windows::Foundation::PropertyType type) + { + THROW_HR_IF(E_INVALIDARG, !IsComparableType(type)); + } + + bool IsLengthType(Windows::Foundation::PropertyType type) + { + switch (type) + { + case Windows::Foundation::PropertyType::String: + case Windows::Foundation::PropertyType::UInt8Array: + case Windows::Foundation::PropertyType::Int16Array: + case Windows::Foundation::PropertyType::UInt16Array: + case Windows::Foundation::PropertyType::Int32Array: + case Windows::Foundation::PropertyType::UInt32Array: + case Windows::Foundation::PropertyType::Int64Array: + case Windows::Foundation::PropertyType::UInt64Array: + case Windows::Foundation::PropertyType::SingleArray: + case Windows::Foundation::PropertyType::DoubleArray: + case Windows::Foundation::PropertyType::Char16Array: + case Windows::Foundation::PropertyType::BooleanArray: + case Windows::Foundation::PropertyType::StringArray: + case Windows::Foundation::PropertyType::InspectableArray: + case Windows::Foundation::PropertyType::DateTimeArray: + case Windows::Foundation::PropertyType::TimeSpanArray: + case Windows::Foundation::PropertyType::GuidArray: + case Windows::Foundation::PropertyType::PointArray: + case Windows::Foundation::PropertyType::SizeArray: + case Windows::Foundation::PropertyType::RectArray: + case Windows::Foundation::PropertyType::OtherTypeArray: + return true; + } + + return false; + } + + void EnsureLengthType(Windows::Foundation::PropertyType type) + { + THROW_HR_IF(E_INVALIDARG, !IsLengthType(type)); + } +} diff --git a/src/Microsoft.Management.Configuration/ArgumentValidation.h b/src/Microsoft.Management.Configuration/ArgumentValidation.h @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <winrt/Windows.Foundation.h> + +namespace winrt::Microsoft::Management::Configuration::implementation +{ + // Ensures that the given type is supported. + void EnsureSupportedType(Windows::Foundation::PropertyType type); + + // Ensures that the value object matches the expected property type. + bool IsValidObjectType(Windows::Foundation::IInspectable const& value, Windows::Foundation::PropertyType type); + + // Ensures that the value object matches the expected property type. + void EnsureObjectType(Windows::Foundation::IInspectable const& value, Windows::Foundation::PropertyType type); + + // Determines if the given type supports comparison. + bool IsComparableType(Windows::Foundation::PropertyType type); + + // Ensures that the given type supports comparison. + void EnsureComparableType(Windows::Foundation::PropertyType type); + + // Determines if the given type supports length restrictions. + bool IsLengthType(Windows::Foundation::PropertyType type); + + // Ensures that the given type supports length restrictions. + void EnsureLengthType(Windows::Foundation::PropertyType type); +} diff --git a/src/Microsoft.Management.Configuration/ConfigurationParameter.cpp b/src/Microsoft.Management.Configuration/ConfigurationParameter.cpp @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "ConfigurationParameter.h" +#include "ConfigurationParameter.g.cpp" +#include "ArgumentValidation.h" + +namespace winrt::Microsoft::Management::Configuration::implementation +{ + hstring ConfigurationParameter::Name() + { + return m_name; + } + + void ConfigurationParameter::Name(hstring const& value) + { + m_name = value; + } + + hstring ConfigurationParameter::Description() + { + return m_description; + } + + void ConfigurationParameter::Description(hstring const& value) + { + m_description = value; + } + + Windows::Foundation::Collections::ValueSet ConfigurationParameter::Metadata() + { + return m_metadata; + } + + void ConfigurationParameter::Metadata(const Windows::Foundation::Collections::ValueSet& value) + { + THROW_HR_IF(E_POINTER, !value); + m_metadata = value; + } + + bool ConfigurationParameter::IsSecure() + { + return m_isSecure; + } + + void ConfigurationParameter::IsSecure(bool value) + { + m_isSecure = value; + } + + Windows::Foundation::PropertyType ConfigurationParameter::Type() + { + return m_type; + } + + void ConfigurationParameter::Type(Windows::Foundation::PropertyType value) + { + EnsureSupportedType(value); + m_type = value; + } + + Windows::Foundation::IInspectable ConfigurationParameter::DefaultValue() + { + return m_defaultValue; + } + + void ConfigurationParameter::DefaultValue(Windows::Foundation::IInspectable const& value) + { + if (value) + { + EnsureObjectType(value, m_type); + } + + m_defaultValue = value; + } + + Windows::Foundation::Collections::IVector<Windows::Foundation::IInspectable> ConfigurationParameter::AllowedValues() + { + return m_allowedValues; + } + + void ConfigurationParameter::AllowedValues(Windows::Foundation::Collections::IVector<Windows::Foundation::IInspectable> const& value) + { + if (value) + { + for (const auto& item : value) + { + if (item) + { + EnsureObjectType(item, m_type); + } + } + } + + m_allowedValues = value; + } + + void ConfigurationParameter::AllowedValues(std::vector<Windows::Foundation::IInspectable>&& value) + { + m_allowedValues = winrt::single_threaded_vector<Windows::Foundation::IInspectable>(std::move(value)); + } + + uint32_t ConfigurationParameter::MinimumLength() + { + return m_minimumLength; + } + + void ConfigurationParameter::MinimumLength(uint32_t value) + { + EnsureLengthType(m_type); + m_minimumLength = value; + } + + uint32_t ConfigurationParameter::MaximumLength() + { + return m_maximumLength; + } + + void ConfigurationParameter::MaximumLength(uint32_t value) + { + EnsureLengthType(m_type); + m_maximumLength = value; + } + + Windows::Foundation::IInspectable ConfigurationParameter::MinimumValue() + { + return m_minimumValue; + } + + void ConfigurationParameter::MinimumValue(Windows::Foundation::IInspectable const& value) + { + if (value) + { + EnsureObjectType(value, m_type); + EnsureComparableType(m_type); + } + + m_minimumValue = value; + } + + Windows::Foundation::IInspectable ConfigurationParameter::MaximumValue() + { + return m_maximumValue; + } + + void ConfigurationParameter::MaximumValue(Windows::Foundation::IInspectable const& value) + { + if (value) + { + EnsureObjectType(value, m_type); + EnsureComparableType(m_type); + } + + m_maximumValue = value; + } + + Windows::Foundation::IInspectable ConfigurationParameter::ProvidedValue() + { + return m_providedValue; + } + + void ConfigurationParameter::ProvidedValue(Windows::Foundation::IInspectable const& value) + { + if (value) + { + EnsureObjectType(value, m_type); + } + + m_providedValue = value; + } + + HRESULT STDMETHODCALLTYPE ConfigurationParameter::SetLifetimeWatcher(IUnknown* watcher) + { + return AppInstaller::WinRT::LifetimeWatcherBase::SetLifetimeWatcher(watcher); + } +} diff --git a/src/Microsoft.Management.Configuration/ConfigurationParameter.h b/src/Microsoft.Management.Configuration/ConfigurationParameter.h @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ConfigurationParameter.g.h" +#include <winget/ILifetimeWatcher.h> +#include <winrt/Windows.Foundation.h> +#include <winrt/Windows.Foundation.Collections.h> +#include <limits> + +namespace winrt::Microsoft::Management::Configuration::implementation +{ + struct ConfigurationParameter : ConfigurationParameterT<ConfigurationParameter, winrt::cloaked<AppInstaller::WinRT::ILifetimeWatcher>>, AppInstaller::WinRT::LifetimeWatcherBase + { + ConfigurationParameter() = default; + + hstring Name(); + void Name(hstring const& value); + + hstring Description(); + void Description(hstring const& value); + + Windows::Foundation::Collections::ValueSet Metadata(); + void Metadata(const Windows::Foundation::Collections::ValueSet& value); + + bool IsSecure(); + void IsSecure(bool value); + + Windows::Foundation::PropertyType Type(); + void Type(Windows::Foundation::PropertyType value); + + Windows::Foundation::IInspectable DefaultValue(); + void DefaultValue(Windows::Foundation::IInspectable const& value); + + Windows::Foundation::Collections::IVector<Windows::Foundation::IInspectable> AllowedValues(); + void AllowedValues(Windows::Foundation::Collections::IVector<Windows::Foundation::IInspectable> const& value); + + uint32_t MinimumLength(); + void MinimumLength(uint32_t value); + + uint32_t MaximumLength(); + void MaximumLength(uint32_t value); + + Windows::Foundation::IInspectable MinimumValue(); + void MinimumValue(Windows::Foundation::IInspectable const& value); + + Windows::Foundation::IInspectable MaximumValue(); + void MaximumValue(Windows::Foundation::IInspectable const& value); + + Windows::Foundation::IInspectable ProvidedValue(); + void ProvidedValue(Windows::Foundation::IInspectable const& value); + + HRESULT STDMETHODCALLTYPE SetLifetimeWatcher(IUnknown* watcher); + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + void AllowedValues(std::vector<Windows::Foundation::IInspectable>&& value); + + private: + hstring m_name; + hstring m_description; + Windows::Foundation::Collections::ValueSet m_metadata; + bool m_isSecure = false; + Windows::Foundation::PropertyType m_type = Windows::Foundation::PropertyType::Inspectable; + Windows::Foundation::IInspectable m_defaultValue; + Windows::Foundation::Collections::IVector<Windows::Foundation::IInspectable> m_allowedValues; + uint32_t m_minimumLength = 0; + uint32_t m_maximumLength = std::numeric_limits<uint32_t>::max(); + Windows::Foundation::IInspectable m_minimumValue; + Windows::Foundation::IInspectable m_maximumValue; + Windows::Foundation::IInspectable m_providedValue; +#endif + }; +} + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) +namespace winrt::Microsoft::Management::Configuration::factory_implementation +{ + struct ConfigurationParameter : ConfigurationParameterT<ConfigurationParameter, implementation::ConfigurationParameter> + { + }; +} +#endif diff --git a/src/Microsoft.Management.Configuration/ConfigurationProcessor.cpp b/src/Microsoft.Management.Configuration/ConfigurationProcessor.cpp @@ -269,21 +269,28 @@ namespace winrt::Microsoft::Management::Configuration::implementation } std::unique_ptr<ConfigurationSetParser> parser = ConfigurationSetParser::Create(inputString); + + // Temporary block on parsing 0.3 schema while it is experimental. + if (parser->GetSchemaVersion() == L"0.3" && !m_supportSchema03) + { + result->Initialize(APPINSTALLER_CLI_ERROR_EXPERIMENTAL_FEATURE_DISABLED); + co_return *result; + } + if (FAILED(parser->Result())) { result->Initialize(parser->Result(), parser->Field(), parser->Value(), parser->Line(), parser->Column()); co_return *result; } - auto configurationSet = make_self<wil::details::module_count_wrapper<implementation::ConfigurationSet>>(); - configurationSet->Initialize(parser->GetConfigurationUnits()); + parser->Parse(); if (FAILED(parser->Result())) { result->Initialize(parser->Result(), parser->Field(), parser->Value(), parser->Line(), parser->Column()); co_return *result; } - configurationSet->SchemaVersion(parser->GetSchemaVersion()); + auto configurationSet = parser->GetConfigurationSet(); PropagateLifetimeWatcher(configurationSet.as<Windows::Foundation::IUnknown>()); result->Initialize(*configurationSet); @@ -670,6 +677,11 @@ namespace winrt::Microsoft::Management::Configuration::implementation // While diagnostics can be important, a failure to send them should not cause additional issues. catch (...) {} + void ConfigurationProcessor::SetSupportsSchema03(bool value) + { + m_supportSchema03 = value; + } + void ConfigurationProcessor::SendDiagnosticsImpl(const IDiagnosticInformation& information) { std::lock_guard<std::recursive_mutex> lock{ m_diagnosticsMutex }; diff --git a/src/Microsoft.Management.Configuration/ConfigurationProcessor.h b/src/Microsoft.Management.Configuration/ConfigurationProcessor.h @@ -90,6 +90,9 @@ namespace winrt::Microsoft::Management::Configuration::implementation // Sends diagnostics objects to the event. void SendDiagnostics(const IDiagnosticInformation& information); + // Temporary entry point to enable experimental schema support. + void SetSupportsSchema03(bool value); + private: GetConfigurationSetDetailsResult GetSetDetailsImpl( const ConfigurationSet& configurationSet, @@ -119,6 +122,8 @@ namespace winrt::Microsoft::Management::Configuration::implementation DiagnosticLevel m_minimumLevel = DiagnosticLevel::Informational; std::recursive_mutex m_diagnosticsMutex; bool m_isHandlingDiagnostics = false; + // Temporary value to enable experimental schema support. + bool m_supportSchema03 = true; #endif }; } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSet.cpp b/src/Microsoft.Management.Configuration/ConfigurationSet.cpp @@ -20,11 +20,16 @@ namespace winrt::Microsoft::Management::Configuration::implementation { } - void ConfigurationSet::Initialize(std::vector<Configuration::ConfigurationUnit>&& units) + void ConfigurationSet::Units(std::vector<Configuration::ConfigurationUnit>&& units) { m_units = winrt::single_threaded_vector<Configuration::ConfigurationUnit>(std::move(units)); } + void ConfigurationSet::Parameters(std::vector<Configuration::ConfigurationParameter>&& value) + { + m_parameters = winrt::single_threaded_vector<Configuration::ConfigurationParameter>(std::move(value)); + } + bool ConfigurationSet::IsFromHistory() const { return false; @@ -104,6 +109,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation void ConfigurationSet::SchemaVersion(const hstring& value) { THROW_HR_IF(E_INVALIDARG, !ConfigurationSetParser::IsRecognizedSchemaVersion(value)); + m_schemaUri = ConfigurationSetParser::GetSchemaUriForVersion(value); m_schemaVersion = value; } @@ -128,6 +134,51 @@ namespace winrt::Microsoft::Management::Configuration::implementation THROW_HR(E_NOTIMPL); } + Windows::Foundation::Collections::ValueSet ConfigurationSet::Metadata() + { + return m_metadata; + } + + void ConfigurationSet::Metadata(const Windows::Foundation::Collections::ValueSet& value) + { + THROW_HR_IF(E_POINTER, !value); + m_metadata = value; + } + + Windows::Foundation::Collections::IVector<ConfigurationParameter> ConfigurationSet::Parameters() + { + return m_parameters; + } + + void ConfigurationSet::Parameters(const Windows::Foundation::Collections::IVector<ConfigurationParameter>& value) + { + THROW_HR_IF(E_POINTER, !value); + m_parameters = value; + } + + Windows::Foundation::Collections::ValueSet ConfigurationSet::Variables() + { + return m_variables; + } + + void ConfigurationSet::Variables(const Windows::Foundation::Collections::ValueSet& value) + { + THROW_HR_IF(E_POINTER, !value); + m_variables = value; + } + + Windows::Foundation::Uri ConfigurationSet::SchemaUri() + { + return m_schemaUri; + } + + void ConfigurationSet::SchemaUri(const Windows::Foundation::Uri& value) + { + THROW_HR_IF(E_INVALIDARG, !ConfigurationSetParser::IsRecognizedSchemaUri(value)); + m_schemaVersion = ConfigurationSetParser::GetSchemaVersionForUri(value); + m_schemaUri = value; + } + HRESULT STDMETHODCALLTYPE ConfigurationSet::SetLifetimeWatcher(IUnknown* watcher) { return AppInstaller::WinRT::LifetimeWatcherBase::SetLifetimeWatcher(watcher); diff --git a/src/Microsoft.Management.Configuration/ConfigurationSet.h b/src/Microsoft.Management.Configuration/ConfigurationSet.h @@ -13,12 +13,14 @@ namespace winrt::Microsoft::Management::Configuration::implementation { using WinRT_Self = ::winrt::Microsoft::Management::Configuration::ConfigurationSet; using ConfigurationUnit = ::winrt::Microsoft::Management::Configuration::ConfigurationUnit; + using ConfigurationParameter = ::winrt::Microsoft::Management::Configuration::ConfigurationParameter; ConfigurationSet(); #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) ConfigurationSet(const guid& instanceIdentifier); - void Initialize(std::vector<Configuration::ConfigurationUnit>&& units); + void Units(std::vector<Configuration::ConfigurationUnit>&& units); + void Parameters(std::vector<Configuration::ConfigurationParameter>&& value); bool IsFromHistory() const; #endif @@ -51,6 +53,18 @@ namespace winrt::Microsoft::Management::Configuration::implementation void Remove(); + Windows::Foundation::Collections::ValueSet Metadata(); + void Metadata(const Windows::Foundation::Collections::ValueSet& value); + + Windows::Foundation::Collections::IVector<ConfigurationParameter> Parameters(); + void Parameters(const Windows::Foundation::Collections::IVector<ConfigurationParameter>& value); + + Windows::Foundation::Collections::ValueSet Variables(); + void Variables(const Windows::Foundation::Collections::ValueSet& value); + + Windows::Foundation::Uri SchemaUri(); + void SchemaUri(const Windows::Foundation::Uri& value); + HRESULT STDMETHODCALLTYPE SetLifetimeWatcher(IUnknown* watcher); #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) @@ -63,6 +77,10 @@ namespace winrt::Microsoft::Management::Configuration::implementation Windows::Foundation::Collections::IVector<ConfigurationUnit> m_units{ winrt::single_threaded_vector<ConfigurationUnit>() }; hstring m_schemaVersion; winrt::event<Windows::Foundation::TypedEventHandler<WinRT_Self, ConfigurationSetChangeData>> m_configurationSetChange; + Windows::Foundation::Collections::ValueSet m_metadata; + Windows::Foundation::Collections::IVector<ConfigurationParameter> m_parameters{ winrt::single_threaded_vector<ConfigurationParameter>() }; + Windows::Foundation::Collections::ValueSet m_variables; + Windows::Foundation::Uri m_schemaUri = nullptr; #endif }; } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetParser.cpp @@ -1,7 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #include "pch.h" -#include <ConfigurationSetParser.h> +#include "ConfigurationSetParser.h" +#include "ParsingMacros.h" +#include "ArgumentValidation.h" #include <AppInstallerErrors.h> #include <AppInstallerLogging.h> @@ -11,11 +13,142 @@ #include "ConfigurationSetParserError.h" #include "ConfigurationSetParser_0_1.h" #include "ConfigurationSetParser_0_2.h" +#include "ConfigurationSetParser_0_3.h" +using namespace AppInstaller::Utility; using namespace AppInstaller::YAML; namespace winrt::Microsoft::Management::Configuration::implementation { + namespace + { + struct SchemaVersionAndUri + { + std::string_view Version; + std::wstring_view VersionWide; + std::string_view Uri; + std::wstring_view UriWide; + }; + +#define SCHEMA_VERSION_MAP_ITEM(_version_,_uri_) _version_, TEXT(_version_), _uri_, TEXT(_uri_) + + // Please keep in sorted order with the highest version last. + // Duplicate URIs are supported, but duplicate versions are not. The highest version for a URI will be the one mapped to, the lower versions will be aliases. + SchemaVersionAndUri SchemaVersionAndUriMap[] = + { + { SCHEMA_VERSION_MAP_ITEM("0.1", "") }, + { SCHEMA_VERSION_MAP_ITEM("0.2", "") }, + { SCHEMA_VERSION_MAP_ITEM("0.3", "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json") }, + }; + + Windows::Foundation::IInspectable GetIInspectableFromNode(const Node& node); + + // Fills the ValueSet from the given node, which is assumed to be a map. + void FillValueSetFromMap(const Node& mapNode, const Windows::Foundation::Collections::ValueSet& valueSet) + { + for (const auto& mapItem : mapNode.Mapping()) + { + // Insert returns true if it replaces an existing key, and that indicates an invalid map. + THROW_HR_IF(WINGET_CONFIG_ERROR_INVALID_CONFIGURATION_FILE, valueSet.Insert(mapItem.first.as<std::wstring>(), GetIInspectableFromNode(mapItem.second))); + } + } + + // Returns the appropriate IPropertyValue for the given node, which is assumed to be a scalar. + Windows::Foundation::IInspectable GetPropertyValueFromScalar(const Node& node) + { + ::winrt::Windows::Foundation::IInspectable result; + + switch (node.GetTagType()) + { + case Node::TagType::Null: + return Windows::Foundation::PropertyValue::CreateEmpty(); + case Node::TagType::Bool: + return Windows::Foundation::PropertyValue::CreateBoolean(node.as<bool>()); + case Node::TagType::Str: + return Windows::Foundation::PropertyValue::CreateString(node.as<std::wstring>()); + case Node::TagType::Int: + return Windows::Foundation::PropertyValue::CreateInt64(node.as<int64_t>()); + case Node::TagType::Float: + THROW_HR(E_NOTIMPL); + case Node::TagType::Timestamp: + THROW_HR(E_NOTIMPL); + default: + THROW_HR(E_UNEXPECTED); + } + } + + // Returns the appropriate IPropertyValue for the given node, which is assumed to be a scalar. + Windows::Foundation::IInspectable GetPropertyValueFromSequence(const Node& sequenceNode) + { + Windows::Foundation::Collections::ValueSet result; + size_t index = 0; + + for (const Node& sequenceItem : sequenceNode.Sequence()) + { + std::wostringstream strstr; + strstr << index++; + result.Insert(strstr.str(), GetIInspectableFromNode(sequenceItem)); + } + + result.Insert(L"treatAsArray", Windows::Foundation::PropertyValue::CreateBoolean(true)); + return result; + } + + // Returns the appropriate IInspectable for the given node. + Windows::Foundation::IInspectable GetIInspectableFromNode(const Node& node) + { + ::winrt::Windows::Foundation::IInspectable result; + + switch (node.GetType()) + { + case Node::Type::Invalid: + case Node::Type::None: + // Leave value as null + break; + case Node::Type::Scalar: + result = GetPropertyValueFromScalar(node); + break; + case Node::Type::Sequence: + result = GetPropertyValueFromSequence(node); + break; + case Node::Type::Mapping: + { + Windows::Foundation::Collections::ValueSet subset; + FillValueSetFromMap(node, subset); + result = std::move(subset); + } + break; + default: + THROW_HR(E_UNEXPECTED); + } + + return result; + } + + // Contains the qualified resource name information. + struct QualifiedResourceName + { + QualifiedResourceName(hstring input) + { + std::wstring_view inputView = input; + size_t pos = inputView.find('/'); + + if (pos != std::wstring_view::npos) + { + Module = inputView.substr(0, pos); + Resource = inputView.substr(pos + 1); + } + else + { + Resource = input; + } + } + + hstring Module; + hstring Resource; + }; + } + std::unique_ptr<ConfigurationSetParser> ConfigurationSetParser::Create(std::string_view input) { AICLI_LOG_LARGE_STRING(Config, Verbose, << "Parsing configuration set:", input); @@ -41,32 +174,45 @@ namespace winrt::Microsoft::Management::Configuration::implementation return std::make_unique<ConfigurationSetParserError>(WINGET_CONFIG_ERROR_INVALID_YAML, documentError, documentErrorMark); } - Node& propertiesNode = document[GetFieldName(FieldName::Properties)]; - if (!propertiesNode) - { - AICLI_LOG(Config, Error, << "No properties"); - return std::make_unique<ConfigurationSetParserError>(WINGET_CONFIG_ERROR_MISSING_FIELD, GetFieldName(FieldName::Properties)); - } - else if (!propertiesNode.IsMap()) - { - AICLI_LOG(Config, Error, << "Invalid properties type"); - return std::make_unique<ConfigurationSetParserError>(WINGET_CONFIG_ERROR_INVALID_FIELD_TYPE, GetFieldName(FieldName::Properties), propertiesNode.Mark()); - } + // The schema version for parsing the rest of the document + std::string schemaUriString; + std::string schemaVersionString; - Node& versionNode = propertiesNode[GetFieldName(FieldName::ConfigurationVersion)]; - if (!versionNode) + Node& schemaNode = document[GetFieldName(FieldName::Schema)]; + if (schemaNode.IsScalar()) { - AICLI_LOG(Config, Error, << "No configuration version"); - return std::make_unique<ConfigurationSetParserError>(WINGET_CONFIG_ERROR_MISSING_FIELD, GetFieldName(FieldName::ConfigurationVersion)); + schemaUriString = schemaNode.as<std::string>(); + schemaVersionString = GetSchemaVersionForUri(schemaUriString); + AICLI_LOG(Config, Verbose, << "Configuration schema `" << schemaNode.as<std::string>() << "` mapped to version `" << schemaVersionString << "`."); } - else if (!versionNode.IsScalar()) + + // If we recognize the schema, use that version. + // If we didn't recognize it, try using the older format. + if (schemaVersionString.empty()) { - AICLI_LOG(Config, Error, << "Invalid configuration version type"); - return std::make_unique<ConfigurationSetParserError>(WINGET_CONFIG_ERROR_INVALID_FIELD_TYPE, GetFieldName(FieldName::ConfigurationVersion), versionNode.Mark()); + std::unique_ptr<ConfigurationSetParser> oldFormatError = GetSchemaVersionFromOldFormat(document, schemaVersionString); + + // We have no schema version at all... + if (oldFormatError) + { + // If the schema was provided and we didn't recognize it, make that the error. + if (schemaNode.IsScalar()) + { + AICLI_LOG(Config, Error, << "Unknown configuration schema: " << schemaUriString); + return std::make_unique<ConfigurationSetParserError>(WINGET_CONFIG_ERROR_UNKNOWN_CONFIGURATION_FILE_VERSION, GetFieldName(FieldName::Schema), schemaUriString); + } + else + { + // Otherwise, this is an older format file (or neither). The proper error came back from that function. + return oldFormatError; + } + } } - AppInstaller::Utility::SemanticVersion schemaVersion(versionNode.as<std::string>()); + // Create the parser based on the version selected + SemanticVersion schemaVersion(std::move(schemaVersionString)); + // TODO: Consider having the version/uri/type information all together in the future if (schemaVersion.PartAt(0).Integer == 0 && schemaVersion.PartAt(1).Integer == 1) { return std::make_unique<ConfigurationSetParser_0_1>(std::move(document)); @@ -75,32 +221,101 @@ namespace winrt::Microsoft::Management::Configuration::implementation { return std::make_unique<ConfigurationSetParser_0_2>(std::move(document)); } + else if (schemaVersion.PartAt(0).Integer == 0 && schemaVersion.PartAt(1).Integer == 3) + { + return std::make_unique<ConfigurationSetParser_0_3>(std::move(document)); + } AICLI_LOG(Config, Error, << "Unknown configuration version: " << schemaVersion.ToString()); - return std::make_unique<ConfigurationSetParserError>(WINGET_CONFIG_ERROR_UNKNOWN_CONFIGURATION_FILE_VERSION, GetFieldName(FieldName::ConfigurationVersion), versionNode.as<std::string>()); + return std::make_unique<ConfigurationSetParserError>(WINGET_CONFIG_ERROR_UNKNOWN_CONFIGURATION_FILE_VERSION, GetFieldName(FieldName::ConfigurationVersion), schemaVersion.ToString()); } bool ConfigurationSetParser::IsRecognizedSchemaVersion(hstring value) try { - using namespace AppInstaller::Utility; - SemanticVersion schemaVersion(ConvertToUTF8(value)); - return (schemaVersion == SemanticVersion{ "0.1" } || schemaVersion == SemanticVersion{ "0.2" }); + for (const auto& item : SchemaVersionAndUriMap) + { + if (schemaVersion == SemanticVersion{ std::string{ item.Version } }) + { + return true; + } + } + + return false; } catch (...) { LOG_CAUGHT_EXCEPTION(); return false; } + bool ConfigurationSetParser::IsRecognizedSchemaUri(const Windows::Foundation::Uri& value) + { + return !GetSchemaVersionForUri(value).empty(); + } + + Windows::Foundation::Uri ConfigurationSetParser::GetSchemaUriForVersion(hstring value) + { + for (const auto& item : SchemaVersionAndUriMap) + { + if (value == item.VersionWide) + { + return item.Uri.empty() ? nullptr : Windows::Foundation::Uri{ item.UriWide }; + } + } + + return nullptr; + } + + hstring ConfigurationSetParser::GetSchemaVersionForUri(Windows::Foundation::Uri value) + { + // Do a reverse search in order to give the highest version back for a given URI. + auto itr = std::rbegin(SchemaVersionAndUriMap); + auto end = std::rend(SchemaVersionAndUriMap); + for (; itr != end; ++itr) + { + const auto& item = *itr; + if (!item.Uri.empty()) + { + Windows::Foundation::Uri uri{ item.UriWide }; + if (value.Equals(uri)) + { + return hstring{ item.VersionWide }; + } + } + } + + return {}; + } + + std::string ConfigurationSetParser::GetSchemaVersionForUri(std::string_view value) + { + // Do a reverse search in order to give the highest version back for a given URI. + auto itr = std::rbegin(SchemaVersionAndUriMap); + auto end = std::rend(SchemaVersionAndUriMap); + for (; itr != end; ++itr) + { + const auto& item = *itr; + if (!item.Uri.empty()) + { + if (item.Uri == value) + { + return std::string{ item.Version }; + } + } + } + + return {}; + } + hstring ConfigurationSetParser::LatestVersion() { - return hstring{ L"0.2" }; + return hstring{ std::rbegin(SchemaVersionAndUriMap)->VersionWide }; } void ConfigurationSetParser::SetError(hresult result, std::string_view field, std::string_view value, uint32_t line, uint32_t column) { AICLI_LOG(Config, Error, << "ConfigurationSetParser error: " << AppInstaller::Logging::SetHRFormat << result << " for " << field << " with value `" << value << "` at [line " << line << ", col " << column << "]"); m_result = result; - m_field = AppInstaller::Utility::ConvertToUTF16(field); - m_value = AppInstaller::Utility::ConvertToUTF16(value); + m_field = ConvertToUTF16(field); + m_value = ConvertToUTF16(value); m_line = line; m_column = column; } @@ -117,7 +332,29 @@ namespace winrt::Microsoft::Management::Configuration::implementation case FieldName::ConfigurationVersion: return "configurationVersion"sv; case FieldName::Properties: return "properties"sv; case FieldName::Resource: return "resource"sv; + case FieldName::Directives: return "directives"sv; + case FieldName::Settings: return "settings"sv; + case FieldName::Assertions: return "assertions"sv; + case FieldName::Id: return "id"sv; + case FieldName::DependsOn: return "dependsOn"sv; + + case FieldName::Resources: return "resources"sv; case FieldName::ModuleDirective: return "module"sv; + + case FieldName::Schema: return "$schema"sv; + case FieldName::Metadata: return "metadata"sv; + case FieldName::Parameters: return "parameters"sv; + case FieldName::Variables: return "variables"sv; + case FieldName::Type: return "type"sv; + case FieldName::Description: return "description"sv; + case FieldName::Name: return "name"sv; + case FieldName::IsGroupMetadata: return "isGroup"sv; + case FieldName::DefaultValue: return "defaultValue"sv; + case FieldName::AllowedValues: return "allowedValues"sv; + case FieldName::MinimumLength: return "minLength"sv; + case FieldName::MaximumLength: return "maxLength"sv; + case FieldName::MinimumValue: return "minValue"sv; + case FieldName::MaximumValue: return "maxValue"sv; } THROW_HR(E_UNEXPECTED); @@ -125,6 +362,212 @@ namespace winrt::Microsoft::Management::Configuration::implementation hstring ConfigurationSetParser::GetFieldNameHString(FieldName fieldName) { - return hstring{ AppInstaller::Utility::ConvertToUTF16(GetFieldName(fieldName)) }; + return hstring{ ConvertToUTF16(GetFieldName(fieldName)) }; + } + + const Node& ConfigurationSetParser::GetAndEnsureField(const Node& parent, FieldName field, bool required, std::optional<Node::Type> type) + { + const Node& fieldNode = parent[GetFieldName(field)]; + + if (fieldNode) + { + if (type && fieldNode.GetType() != type.value()) + { + SetError(WINGET_CONFIG_ERROR_INVALID_FIELD_TYPE, GetFieldName(field), fieldNode.Mark()); + } + } + else if (required) + { + SetError(WINGET_CONFIG_ERROR_MISSING_FIELD, GetFieldName(field)); + } + + return fieldNode; + } + + void ConfigurationSetParser::EnsureFieldAbsent(const Node& parent, FieldName field) + { + const Node& fieldNode = parent[GetFieldName(field)]; + + if (fieldNode) + { + SetError(WINGET_CONFIG_ERROR_INVALID_FIELD_VALUE, GetFieldName(field), fieldNode.Mark(), fieldNode.as<std::string>()); + } + } + + void ConfigurationSetParser::ParseValueSet(const Node& node, FieldName field, bool required, const Windows::Foundation::Collections::ValueSet& valueSet) + { + const Node& mapNode = CHECK_ERROR(GetAndEnsureField(node, field, required, Node::Type::Mapping)); + + if (mapNode) + { + FillValueSetFromMap(mapNode, valueSet); + } + } + + void ConfigurationSetParser::ParseMapping(const AppInstaller::YAML::Node& node, FieldName field, bool required, AppInstaller::YAML::Node::Type elementType, std::function<void(std::string, const AppInstaller::YAML::Node&)> operation) + { + const Node& mapNode = CHECK_ERROR(GetAndEnsureField(node, field, required, Node::Type::Mapping)); + if (!mapNode) + { + return; + } + + std::ostringstream strstr; + strstr << GetFieldName(field); + size_t index = 0; + + for (const auto& mapItem : mapNode.Mapping()) + { + std::string name = mapItem.first.as<std::string>(); + if (name.empty()) + { + strstr << '[' << index << ']'; + FIELD_VALUE_ERROR(strstr.str(), name, mapItem.first.Mark()); + } + + if (mapItem.second.GetType() != elementType) + { + strstr << '[' << index << ']'; + FIELD_TYPE_ERROR(strstr.str(), mapItem.second.Mark()); + } + index++; + + CHECK_ERROR(operation(std::move(name), mapItem.second)); + } + } + + void ConfigurationSetParser::ParseSequence(const AppInstaller::YAML::Node& node, FieldName field, bool required, std::optional<Node::Type> elementType, std::function<void(const AppInstaller::YAML::Node&)> operation) + { + const Node& sequenceNode = CHECK_ERROR(GetAndEnsureField(node, field, required, Node::Type::Sequence)); + if (!sequenceNode) + { + return; + } + + std::ostringstream strstr; + strstr << GetFieldName(field); + size_t index = 0; + + for (const Node& item : sequenceNode.Sequence()) + { + if (elementType && item.GetType() != elementType.value()) + { + strstr << '[' << index << ']'; + FIELD_TYPE_ERROR(strstr.str(), item.Mark()); + } + index++; + + CHECK_ERROR(operation(item)); + } + } + + std::unique_ptr<ConfigurationSetParser> ConfigurationSetParser::GetSchemaVersionFromOldFormat(AppInstaller::YAML::Node& document, std::string& schemaVersionString) + { + Node& propertiesNode = document[GetFieldName(FieldName::Properties)]; + if (!propertiesNode) + { + AICLI_LOG(Config, Error, << "No properties"); + // Even though this is for the "older" format, if there is no properties entry then give an error for the newer format since this is probably neither. + return std::make_unique<ConfigurationSetParserError>(WINGET_CONFIG_ERROR_MISSING_FIELD, GetFieldName(FieldName::Schema)); + } + else if (!propertiesNode.IsMap()) + { + AICLI_LOG(Config, Error, << "Invalid properties type"); + return std::make_unique<ConfigurationSetParserError>(WINGET_CONFIG_ERROR_INVALID_FIELD_TYPE, GetFieldName(FieldName::Properties), propertiesNode.Mark()); + } + + Node& versionNode = propertiesNode[GetFieldName(FieldName::ConfigurationVersion)]; + if (!versionNode) + { + AICLI_LOG(Config, Error, << "No configuration version"); + return std::make_unique<ConfigurationSetParserError>(WINGET_CONFIG_ERROR_MISSING_FIELD, GetFieldName(FieldName::ConfigurationVersion)); + } + else if (!versionNode.IsScalar()) + { + AICLI_LOG(Config, Error, << "Invalid configuration version type"); + return std::make_unique<ConfigurationSetParserError>(WINGET_CONFIG_ERROR_INVALID_FIELD_TYPE, GetFieldName(FieldName::ConfigurationVersion), versionNode.Mark()); + } + + schemaVersionString = versionNode.as<std::string>(); + return {}; + } + + void ConfigurationSetParser::GetStringValueForUnit(const Node& node, FieldName field, bool required, ConfigurationUnit* unit, void(ConfigurationUnit::* propertyFunction)(const hstring& value)) + { + const Node& valueNode = CHECK_ERROR(GetAndEnsureField(node, field, required, Node::Type::Scalar)); + + if (valueNode) + { + hstring value{ valueNode.as<std::wstring>() }; + FIELD_MISSING_ERROR_IF(value.empty() && required, GetFieldName(field)); + + (unit->*propertyFunction)(std::move(value)); + } + } + + void ConfigurationSetParser::GetStringArrayForUnit(const Node& node, FieldName field, bool required, ConfigurationUnit* unit, void(ConfigurationUnit::* propertyFunction)(std::vector<hstring>&& value)) + { + std::vector<hstring> arrayValue; + CHECK_ERROR(ParseSequence(node, field, required, Node::Type::Scalar, [&](const AppInstaller::YAML::Node& item) + { + arrayValue.emplace_back(item.as<std::wstring>()); + })); + + if (!arrayValue.empty()) + { + (unit->*propertyFunction)(std::move(arrayValue)); + } + } + + void ConfigurationSetParser::ValidateType(ConfigurationUnit* unit, const Node& unitNode, FieldName typeField, bool moveModuleNameToMetadata, bool moduleNameRequiredInType) + { + QualifiedResourceName qualifiedName{ unit->Type() }; + + const Node& typeNode = CHECK_ERROR(GetAndEnsureField(unitNode, typeField, true, Node::Type::Scalar)); + FIELD_VALUE_ERROR_IF(qualifiedName.Resource.empty(), GetFieldName(typeField), ConvertToUTF8(unit->Type()), typeNode.Mark()); + + if (!qualifiedName.Module.empty()) + { + // If the module is provided in both the resource name and the directives, ensure that it matches + hstring moduleDirectiveFieldName = GetFieldNameHString(FieldName::ModuleDirective); + auto moduleDirective = unit->Metadata().TryLookup(moduleDirectiveFieldName); + if (moduleDirective) + { + auto moduleProperty = moduleDirective.try_as<Windows::Foundation::IPropertyValue>(); + FIELD_TYPE_ERROR_IF(!moduleProperty, GetFieldName(FieldName::ModuleDirective), unitNode.Mark()); + FIELD_TYPE_ERROR_IF(moduleProperty.Type() != Windows::Foundation::PropertyType::String, GetFieldName(FieldName::ModuleDirective), unitNode.Mark()); + hstring moduleValue = moduleProperty.GetString(); + FIELD_VALUE_ERROR_IF(qualifiedName.Module != moduleValue, GetFieldName(FieldName::ModuleDirective), ConvertToUTF8(moduleValue), unitNode.Mark()); + } + else if (moveModuleNameToMetadata) + { + unit->Metadata().Insert(moduleDirectiveFieldName, Windows::Foundation::PropertyValue::CreateString(qualifiedName.Module)); + } + + if (moveModuleNameToMetadata) + { + // Set the unit name to be just the resource portion + unit->Type(qualifiedName.Resource); + } + } + else if (moduleNameRequiredInType) + { + FIELD_VALUE_ERROR(GetFieldName(typeField), ConvertToUTF8(unit->Type()), typeNode.Mark()); + } + } + + void ConfigurationSetParser::ParseObject(const Node& node, FieldName fieldForErrors, Windows::Foundation::PropertyType type, Windows::Foundation::IInspectable& result) + { + try + { + Windows::Foundation::IInspectable object = GetIInspectableFromNode(node); + FIELD_VALUE_ERROR_IF(!IsValidObjectType(object, type), GetFieldName(fieldForErrors), node.as<std::string>(), node.Mark()); + result = std::move(object); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + FIELD_VALUE_ERROR(GetFieldName(fieldForErrors), node.as<std::string>(), node.Mark()); + } } } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser.h b/src/Microsoft.Management.Configuration/ConfigurationSetParser.h @@ -2,6 +2,7 @@ // Licensed under the MIT License. #pragma once #include <ConfigurationUnit.h> +#include <ConfigurationSet.h> #include <winget/Yaml.h> #include <winrt/Windows.Storage.Streams.h> #include <memory> @@ -22,6 +23,19 @@ namespace winrt::Microsoft::Management::Configuration::implementation // This will only return true for a version that we fully recognize. static bool IsRecognizedSchemaVersion(hstring value); + // Determines if the given value is a recognized schema URI. + // This will only return true for a URI that we fully recognize. + static bool IsRecognizedSchemaUri(const Windows::Foundation::Uri& value); + + // Gets the schema URI associated with the given version, or null if there is not one. + static Windows::Foundation::Uri GetSchemaUriForVersion(hstring value); + + // Gets the schema version associated with the given URI, or null if there is not one. + static hstring GetSchemaVersionForUri(Windows::Foundation::Uri value); + + // Gets the schema version associated with the given URI, or null if there is not one. + static std::string GetSchemaVersionForUri(std::string_view value); + // Gets the latest schema version. static hstring LatestVersion(); @@ -32,12 +46,17 @@ namespace winrt::Microsoft::Management::Configuration::implementation ConfigurationSetParser(ConfigurationSetParser&&) = default; ConfigurationSetParser& operator=(ConfigurationSetParser&&) = default; - // Retrieve the configuration units from the parser. - virtual std::vector<Configuration::ConfigurationUnit> GetConfigurationUnits() = 0; + // Parse the full document. + virtual void Parse() = 0; // Retrieves the schema version of the parser. virtual hstring GetSchemaVersion() = 0; + using ConfigurationSetPtr = decltype(make_self<wil::details::module_count_wrapper<implementation::ConfigurationSet>>()); + + // Retrieve the configuration set from the parser. + ConfigurationSetPtr GetConfigurationSet() const { return m_configurationSet; } + // The latest result code from the parser. hresult Result() const { return m_result; } @@ -63,20 +82,77 @@ namespace winrt::Microsoft::Management::Configuration::implementation // The various field names that are used in parsing. enum class FieldName { + // v0.1 and v0.2 ConfigurationVersion, Properties, Resource, + Directives, + Settings, + Assertions, + Id, + DependsOn, + + // Universal + Resources, ModuleDirective, + + // v0.3 + Schema, + Metadata, + Parameters, + Variables, + Type, + Description, + Name, + IsGroupMetadata, + DefaultValue, + AllowedValues, + MinimumLength, + MaximumLength, + MinimumValue, + MaximumValue, }; // Gets the value of the field name. static std::string_view GetFieldName(FieldName fieldName); static hstring GetFieldNameHString(FieldName fieldName); + ConfigurationSetPtr m_configurationSet; hresult m_result; hstring m_field; hstring m_value; uint32_t m_line = 0; uint32_t m_column = 0; + + // Gets the given `field` from the `parent` node, checking against the requirement and type. + const AppInstaller::YAML::Node& GetAndEnsureField(const AppInstaller::YAML::Node& parent, FieldName field, bool required, std::optional<AppInstaller::YAML::Node::Type> type); + + // Errors if the given `field` is present. + void EnsureFieldAbsent(const AppInstaller::YAML::Node& parent, FieldName field); + + // Parse the ValueSet named `field` from the given `node`. + void ParseValueSet(const AppInstaller::YAML::Node& node, FieldName field, bool required, const Windows::Foundation::Collections::ValueSet& valueSet); + + // Parse the mapping named `field` from the given `node`. + void ParseMapping(const AppInstaller::YAML::Node& node, FieldName field, bool required, AppInstaller::YAML::Node::Type elementType, std::function<void(std::string, const AppInstaller::YAML::Node&)> operation); + + // Parse the sequence named `field` from the given `node`. + void ParseSequence(const AppInstaller::YAML::Node& node, FieldName field, bool required, std::optional<AppInstaller::YAML::Node::Type> elementType, std::function<void(const AppInstaller::YAML::Node&)> operation); + + // Gets the string value in `field` from the given `node`, setting this value on `unit` using the `propertyFunction`. + void GetStringValueForUnit(const AppInstaller::YAML::Node& node, FieldName field, bool required, ConfigurationUnit* unit, void(ConfigurationUnit::* propertyFunction)(const hstring& value)); + + // Gets the string array in `field` from the given `node`, setting this value on `unit` using the `propertyFunction`. + void GetStringArrayForUnit(const AppInstaller::YAML::Node& node, FieldName field, bool required, ConfigurationUnit* unit, void(ConfigurationUnit::* propertyFunction)(std::vector<hstring>&& value)); + + // Validates the unit's Type property for correctness and consistency with the metadata. Should be called after parsing the Metadata value. + void ValidateType(ConfigurationUnit* unit, const AppInstaller::YAML::Node& unitNode, FieldName typeField, bool moveModuleNameToMetadata, bool moduleNameRequiredInType); + + // Parses an object from the given node, attempting to treat it as the requested type if possible. + void ParseObject(const AppInstaller::YAML::Node& node, FieldName fieldForErrors, Windows::Foundation::PropertyType type, Windows::Foundation::IInspectable& result); + + private: + // Support older schema parsing. + static std::unique_ptr<ConfigurationSetParser> GetSchemaVersionFromOldFormat(AppInstaller::YAML::Node& document, std::string& schemaVersionString); }; } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParserError.h b/src/Microsoft.Management.Configuration/ConfigurationSetParserError.h @@ -19,7 +19,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation SetError(result, field, mark); } - std::vector<Configuration::ConfigurationUnit> GetConfigurationUnits() override { return {}; } + void Parse() override {} hstring GetSchemaVersion() override { return {}; } }; diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_1.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_1.cpp @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include "ConfigurationSetParser_0_1.h" +#include "ParsingMacros.h" #include <AppInstallerErrors.h> #include <AppInstallerStrings.h> @@ -12,110 +13,17 @@ namespace winrt::Microsoft::Management::Configuration::implementation { using namespace AppInstaller::YAML; -#define CHECK_ERROR(_op_) (_op_); if (FAILED(m_result)) { return; } - -#define FIELD_TYPE_ERROR(_field_,_mark_) SetError(WINGET_CONFIG_ERROR_INVALID_FIELD_TYPE, (_field_), (_mark_)); return -#define FIELD_TYPE_ERROR_IF(_condition_,_field_,_mark_) if (_condition_) { FIELD_TYPE_ERROR(_field_,_mark_); } - -#define FIELD_MISSING_ERROR(_field_) SetError(WINGET_CONFIG_ERROR_MISSING_FIELD, (_field_)); return -#define FIELD_MISSING_ERROR_IF(_condition_,_field_) if (_condition_) { FIELD_MISSING_ERROR(_field_); } - - namespace - { - Windows::Foundation::IInspectable GetIInspectableFromNode(const Node& node); - - // Returns the appropriate IPropertyValue for the given node, which is assumed to be a scalar. - Windows::Foundation::IInspectable GetPropertyValueFromScalar(const Node& node) - { - ::winrt::Windows::Foundation::IInspectable result; - - switch (node.GetTagType()) - { - case Node::TagType::Null: - return Windows::Foundation::PropertyValue::CreateEmpty(); - case Node::TagType::Bool: - return Windows::Foundation::PropertyValue::CreateBoolean(node.as<bool>()); - case Node::TagType::Str: - return Windows::Foundation::PropertyValue::CreateString(node.as<std::wstring>()); - case Node::TagType::Int: - return Windows::Foundation::PropertyValue::CreateInt64(node.as<int64_t>()); - case Node::TagType::Float: - THROW_HR(E_NOTIMPL); - case Node::TagType::Timestamp: - THROW_HR(E_NOTIMPL); - default: - THROW_HR(E_UNEXPECTED); - } - } - - // Returns the appropriate IPropertyValue for the given node, which is assumed to be a scalar. - Windows::Foundation::IInspectable GetPropertyValueFromSequence(const Node& sequenceNode) - { - Windows::Foundation::Collections::ValueSet result; - size_t index = 0; - - for (const Node& sequenceItem : sequenceNode.Sequence()) - { - std::wostringstream strstr; - strstr << index++; - result.Insert(strstr.str(), GetIInspectableFromNode(sequenceItem)); - } - - result.Insert(L"treatAsArray", Windows::Foundation::PropertyValue::CreateBoolean(true)); - return result; - } - - // Fills the ValueSet from the given node, which is assumed to be a map. - void FillValueSetFromMap(const Node& mapNode, const Windows::Foundation::Collections::ValueSet& valueSet) - { - for (const auto& mapItem : mapNode.Mapping()) - { - // Insert returns true if it replaces an existing key, and that indicates an invalid map. - THROW_HR_IF(WINGET_CONFIG_ERROR_INVALID_CONFIGURATION_FILE, valueSet.Insert(mapItem.first.as<std::wstring>(), GetIInspectableFromNode(mapItem.second))); - } - } - - // Returns the appropriate IInspectable for the given node. - Windows::Foundation::IInspectable GetIInspectableFromNode(const Node& node) - { - ::winrt::Windows::Foundation::IInspectable result; - - switch (node.GetType()) - { - case Node::Type::Invalid: - case Node::Type::None: - // Leave value as null - break; - case Node::Type::Scalar: - result = GetPropertyValueFromScalar(node); - break; - case Node::Type::Sequence: - result = GetPropertyValueFromSequence(node); - break; - case Node::Type::Mapping: - { - Windows::Foundation::Collections::ValueSet subset; - FillValueSetFromMap(node, subset); - result = std::move(subset); - } - break; - default: - THROW_HR(E_UNEXPECTED); - } - - return result; - } - } - - std::vector<Configuration::ConfigurationUnit> ConfigurationSetParser_0_1::GetConfigurationUnits() + void ConfigurationSetParser_0_1::Parse() { - std::vector<Configuration::ConfigurationUnit> result; + std::vector<Configuration::ConfigurationUnit> units; const Node& properties = m_document[GetFieldName(FieldName::Properties)]; - ParseConfigurationUnitsFromSubsection(properties, "assertions", ConfigurationUnitIntent::Assert, result); - ParseConfigurationUnitsFromSubsection(properties, "parameters", ConfigurationUnitIntent::Inform, result); - ParseConfigurationUnitsFromSubsection(properties, "resources", ConfigurationUnitIntent::Apply, result); - // TODO: Additional semantic validation? - return result; + ParseConfigurationUnitsFromField(properties, FieldName::Assertions, ConfigurationUnitIntent::Assert, units); + ParseConfigurationUnitsFromField(properties, FieldName::Parameters, ConfigurationUnitIntent::Inform, units); + ParseConfigurationUnitsFromField(properties, FieldName::Resources, ConfigurationUnitIntent::Apply, units); + + m_configurationSet = make_self<wil::details::module_count_wrapper<implementation::ConfigurationSet>>(); + m_configurationSet->Units(std::move(units)); + m_configurationSet->SchemaVersion(GetSchemaVersion()); } hstring ConfigurationSetParser_0_1::GetSchemaVersion() @@ -124,119 +32,23 @@ namespace winrt::Microsoft::Management::Configuration::implementation return s_schemaVersion; } - void ConfigurationSetParser_0_1::ParseConfigurationUnitsFromSubsection(const Node& document, std::string_view subsection, ConfigurationUnitIntent intent, std::vector<Configuration::ConfigurationUnit>& result) + void ConfigurationSetParser_0_1::ParseConfigurationUnitsFromField(const Node& document, FieldName field, ConfigurationUnitIntent intent, std::vector<Configuration::ConfigurationUnit>& result) { - if (FAILED(m_result)) - { - return; - } - - Node subsectionNode = document[subsection]; - - if (!subsectionNode.IsDefined()) - { - return; - } - - FIELD_TYPE_ERROR_IF(!subsectionNode.IsSequence(), subsection, subsectionNode.Mark()); - - std::ostringstream strstr; - strstr << subsection; - size_t index = 0; - - for (const Node& item : subsectionNode.Sequence()) - { - if (!item.IsMap()) + ParseSequence(document, field, false, Node::Type::Mapping, [&](const Node& item) { - strstr << '[' << index << ']'; - FIELD_TYPE_ERROR(strstr.str(), item.Mark()); - } - index++; - - auto configurationUnit = make_self<wil::details::module_count_wrapper<ConfigurationUnit>>(); - - ParseConfigurationUnit(configurationUnit.get(), item, intent); - - result.emplace_back(*configurationUnit); - } + auto configurationUnit = make_self<wil::details::module_count_wrapper<ConfigurationUnit>>(); + ParseConfigurationUnit(configurationUnit.get(), item, intent); + result.emplace_back(*configurationUnit); + }); } void ConfigurationSetParser_0_1::ParseConfigurationUnit(ConfigurationUnit* unit, const Node& unitNode, ConfigurationUnitIntent intent) { - CHECK_ERROR(GetStringValueForUnit(unitNode, GetFieldName(FieldName::Resource), true, unit, &ConfigurationUnit::Type)); - CHECK_ERROR(GetStringValueForUnit(unitNode, "id", false, unit, &ConfigurationUnit::Identifier)); + CHECK_ERROR(GetStringValueForUnit(unitNode, FieldName::Resource, true, unit, &ConfigurationUnit::Type)); + CHECK_ERROR(GetStringValueForUnit(unitNode, FieldName::Id, false, unit, &ConfigurationUnit::Identifier)); unit->Intent(intent); - CHECK_ERROR(GetStringArrayForUnit(unitNode, "dependsOn", unit, &ConfigurationUnit::Dependencies)); - CHECK_ERROR(GetValueSet(unitNode, "directives", false, unit->Metadata())); - CHECK_ERROR(GetValueSet(unitNode, "settings", false, unit->Settings())); - } - - void ConfigurationSetParser_0_1::GetStringValueForUnit(const Node& item, std::string_view valueName, bool required, ConfigurationUnit* unit, void(ConfigurationUnit::* propertyFunction)(const hstring& value)) - { - const Node& valueNode = item[valueName]; - - if (valueNode) - { - FIELD_TYPE_ERROR_IF(!valueNode.IsScalar(), valueName, valueNode.Mark()); - } - else - { - FIELD_MISSING_ERROR_IF(required, valueName); - return; - } - - hstring value{ valueNode.as<std::wstring>() }; - FIELD_MISSING_ERROR_IF(value.empty() && required, valueName); - - (unit->*propertyFunction)(std::move(value)); - } - - void ConfigurationSetParser_0_1::GetStringArrayForUnit(const Node& item, std::string_view arrayName, ConfigurationUnit* unit, void(ConfigurationUnit::* propertyFunction)(std::vector<hstring>&& value)) - { - const Node& arrayNode = item[arrayName]; - - if (!arrayNode) - { - return; - } - - FIELD_TYPE_ERROR_IF(!arrayNode.IsSequence(), arrayName, arrayNode.Mark()); - - std::vector<hstring> arrayValue; - - std::ostringstream strstr; - strstr << arrayName; - size_t index = 0; - - for (const Node& arrayItem : arrayNode.Sequence()) - { - if (!arrayItem.IsScalar()) - { - strstr << '[' << index << ']'; - FIELD_TYPE_ERROR(strstr.str(), arrayItem.Mark()); - } - index++; - - arrayValue.emplace_back(arrayItem.as<std::wstring>()); - } - - (unit->*propertyFunction)(std::move(arrayValue)); - } - - void ConfigurationSetParser_0_1::GetValueSet(const Node& item, std::string_view mapName, bool required, const Windows::Foundation::Collections::ValueSet& valueSet) - { - const Node& mapNode = item[mapName]; - - if (mapNode) - { - FIELD_TYPE_ERROR_IF(!mapNode.IsMap(), mapName, mapNode.Mark()); - } - else - { - FIELD_MISSING_ERROR_IF(required, mapName); - return; - } - - FillValueSetFromMap(mapNode, valueSet); + CHECK_ERROR(GetStringArrayForUnit(unitNode, FieldName::DependsOn, false, unit, &ConfigurationUnit::Dependencies)); + CHECK_ERROR(ParseValueSet(unitNode, FieldName::Directives, false, unit->Metadata())); + CHECK_ERROR(ParseValueSet(unitNode, FieldName::Settings, false, unit->Settings())); } } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_1.h b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_1.h @@ -19,18 +19,14 @@ namespace winrt::Microsoft::Management::Configuration::implementation ConfigurationSetParser_0_1(ConfigurationSetParser_0_1&&) = default; ConfigurationSetParser_0_1& operator=(ConfigurationSetParser_0_1&&) = default; - // Retrieve the configuration units from the parser. - std::vector<Configuration::ConfigurationUnit> GetConfigurationUnits() override; + void Parse() override; // Retrieves the schema version of the parser. hstring GetSchemaVersion() override; protected: - void ParseConfigurationUnitsFromSubsection(const AppInstaller::YAML::Node& document, std::string_view subsection, ConfigurationUnitIntent intent, std::vector<Configuration::ConfigurationUnit>& result); + void ParseConfigurationUnitsFromField(const AppInstaller::YAML::Node& document, FieldName field, ConfigurationUnitIntent intent, std::vector<Configuration::ConfigurationUnit>& result); virtual void ParseConfigurationUnit(ConfigurationUnit* unit, const AppInstaller::YAML::Node& unitNode, ConfigurationUnitIntent intent); - void GetStringValueForUnit(const AppInstaller::YAML::Node& item, std::string_view valueName, bool required, ConfigurationUnit* unit, void(ConfigurationUnit::* propertyFunction)(const hstring& value)); - void GetStringArrayForUnit(const AppInstaller::YAML::Node& item, std::string_view arrayName, ConfigurationUnit* unit, void(ConfigurationUnit::* propertyFunction)(std::vector<hstring>&& value)); - void GetValueSet(const AppInstaller::YAML::Node& item, std::string_view mapName, bool required, const Windows::Foundation::Collections::ValueSet& valueSet); AppInstaller::YAML::Node m_document; }; diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_2.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_2.cpp @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include "ConfigurationSetParser_0_2.h" +#include "ParsingMacros.h" #include <AppInstallerErrors.h> #include <AppInstallerStrings.h> @@ -12,38 +13,6 @@ namespace winrt::Microsoft::Management::Configuration::implementation { using namespace AppInstaller::YAML; -#define FIELD_TYPE_ERROR(_field_,_mark_) SetError(WINGET_CONFIG_ERROR_INVALID_FIELD_TYPE, (_field_), (_mark_)); return -#define FIELD_TYPE_ERROR_IF(_condition_,_field_,_mark_) if (_condition_) { FIELD_TYPE_ERROR(_field_,_mark_); } - -#define FIELD_VALUE_ERROR(_field_,_value_,_mark_) SetError(WINGET_CONFIG_ERROR_INVALID_FIELD_VALUE, (_field_), (_mark_), (_value_)); return -#define FIELD_VALUE_ERROR_IF(_condition_,_field_,_value_,_mark_) if (_condition_) { FIELD_VALUE_ERROR(_field_,_value_,_mark_); } - - namespace - { - // Contains the qualified resource name information. - struct QualifiedResourceName - { - QualifiedResourceName(hstring input) - { - std::wstring_view inputView = input; - size_t pos = inputView.find('/'); - - if (pos != std::wstring_view::npos) - { - Module = inputView.substr(0, pos); - Resource = inputView.substr(pos + 1); - } - else - { - Resource = input; - } - } - - hstring Module; - hstring Resource; - }; - } - hstring ConfigurationSetParser_0_2::GetSchemaVersion() { static hstring s_schemaVersion{ L"0.2" }; @@ -52,35 +21,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation void ConfigurationSetParser_0_2::ParseConfigurationUnit(ConfigurationUnit* unit, const Node& unitNode, ConfigurationUnitIntent intent) { - using namespace AppInstaller::Utility; - - ConfigurationSetParser_0_1::ParseConfigurationUnit(unit, unitNode, intent); - - // Move module qualification into directives if present - QualifiedResourceName qualifiedName{ unit->Type() }; - - FIELD_VALUE_ERROR_IF(qualifiedName.Resource.empty(), GetFieldName(FieldName::Resource), ConvertToUTF8(unit->Type()), unitNode.Mark()); - - if (!qualifiedName.Module.empty()) - { - // If the module is provided in both the resource name and the directives, ensure that it matches - hstring moduleDirectiveFieldName = GetFieldNameHString(FieldName::ModuleDirective); - auto moduleDirective = unit->Metadata().TryLookup(moduleDirectiveFieldName); - if (moduleDirective) - { - auto moduleProperty = moduleDirective.try_as<Windows::Foundation::IPropertyValue>(); - FIELD_TYPE_ERROR_IF(!moduleProperty, GetFieldName(FieldName::ModuleDirective), unitNode.Mark()); - FIELD_TYPE_ERROR_IF(moduleProperty.Type() != Windows::Foundation::PropertyType::String, GetFieldName(FieldName::ModuleDirective), unitNode.Mark()); - hstring moduleValue = moduleProperty.GetString(); - FIELD_VALUE_ERROR_IF(qualifiedName.Module != moduleValue, GetFieldName(FieldName::ModuleDirective), ConvertToUTF8(moduleValue), unitNode.Mark()); - } - else - { - unit->Metadata().Insert(moduleDirectiveFieldName, Windows::Foundation::PropertyValue::CreateString(qualifiedName.Module)); - } - - // Set the unit name to be just the resource portion - unit->Type(qualifiedName.Resource); - } + CHECK_ERROR(ConfigurationSetParser_0_1::ParseConfigurationUnit(unit, unitNode, intent)); + ValidateType(unit, unitNode, FieldName::Resource, true, false); } } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_3.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_3.cpp @@ -0,0 +1,251 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "ConfigurationSetParser_0_3.h" +#include "ParsingMacros.h" +#include "ArgumentValidation.h" + +#include <AppInstallerErrors.h> +#include <AppInstallerStrings.h> + +#include <sstream> + +namespace winrt::Microsoft::Management::Configuration::implementation +{ + using namespace AppInstaller::YAML; + + void ConfigurationSetParser_0_3::Parse() + { + auto result = make_self<wil::details::module_count_wrapper<implementation::ConfigurationSet>>(); + + CHECK_ERROR(ParseValueSet(m_document, FieldName::Metadata, false, result->Metadata())); + CHECK_ERROR(ParseParameters(result)); + CHECK_ERROR(ParseValueSet(m_document, FieldName::Variables, false, result->Variables())); + + std::vector<Configuration::ConfigurationUnit> units; + CHECK_ERROR(ParseConfigurationUnitsFromField(m_document, FieldName::Resources, units)); + result->Units(std::move(units)); + + result->SchemaVersion(GetSchemaVersion()); + m_configurationSet = std::move(result); + } + + hstring ConfigurationSetParser_0_3::GetSchemaVersion() + { + static hstring s_schemaVersion{ L"0.3" }; + return s_schemaVersion; + } + + void ConfigurationSetParser_0_3::ParseParameters(ConfigurationSetParser::ConfigurationSetPtr& set) + { + std::vector<Configuration::ConfigurationParameter> parameters; + + ParseMapping(m_document, FieldName::Parameters, false, Node::Type::Mapping, [&](std::string name, const Node& item) + { + auto parameter = make_self<wil::details::module_count_wrapper<ConfigurationParameter>>(); + CHECK_ERROR(ParseParameter(parameter.get(), item)); + parameter->Name(hstring{ AppInstaller::Utility::ConvertToUTF16(name) }); + parameters.emplace_back(*parameter); + }); + + set->Parameters(std::move(parameters)); + } + + void ConfigurationSetParser_0_3::ParseParameter(ConfigurationParameter* parameter, const AppInstaller::YAML::Node& node) + { + CHECK_ERROR(ParseParameterType(parameter, node)); + CHECK_ERROR(ParseValueSet(node, FieldName::Metadata, false, parameter->Metadata())); + CHECK_ERROR(GetStringValueForParameter(node, FieldName::Description, parameter, &ConfigurationParameter::Description)); + + Windows::Foundation::PropertyType parameterType = parameter->Type(); + CHECK_ERROR(ParseObjectValueForParameter(node, FieldName::DefaultValue, parameterType, parameter, &ConfigurationParameter::DefaultValue)); + + std::vector<Windows::Foundation::IInspectable> allowedValues; + + CHECK_ERROR(ParseSequence(node, FieldName::AllowedValues, false, std::nullopt, [&](const Node& item) + { + Windows::Foundation::IInspectable object; + CHECK_ERROR(ParseObject(item, FieldName::AllowedValues, parameterType, object)); + allowedValues.emplace_back(std::move(object)); + })); + + if (!allowedValues.empty()) + { + parameter->AllowedValues(std::move(allowedValues)); + } + + if (IsLengthType(parameterType)) + { + CHECK_ERROR(GetUInt32ValueForParameter(node, FieldName::MinimumLength, parameter, &ConfigurationParameter::MinimumLength)); + CHECK_ERROR(GetUInt32ValueForParameter(node, FieldName::MaximumLength, parameter, &ConfigurationParameter::MaximumLength)); + } + else + { + CHECK_ERROR(EnsureFieldAbsent(node, FieldName::MinimumLength)); + CHECK_ERROR(EnsureFieldAbsent(node, FieldName::MaximumLength)); + } + + if (IsComparableType(parameterType)) + { + CHECK_ERROR(ParseObjectValueForParameter(node, FieldName::MinimumValue, parameterType, parameter, &ConfigurationParameter::MinimumValue)); + CHECK_ERROR(ParseObjectValueForParameter(node, FieldName::MaximumValue, parameterType, parameter, &ConfigurationParameter::MaximumValue)); + } + else + { + CHECK_ERROR(EnsureFieldAbsent(node, FieldName::MinimumValue)); + CHECK_ERROR(EnsureFieldAbsent(node, FieldName::MaximumValue)); + } + } + + void ConfigurationSetParser_0_3::ParseParameterType(ConfigurationParameter* parameter, const AppInstaller::YAML::Node& node) + { + const Node& typeNode = CHECK_ERROR(GetAndEnsureField(node, FieldName::Type, true, Node::Type::Scalar)); + std::string typeValue = typeNode.as<std::string>(); + + if (typeValue == "string") + { + parameter->Type(Windows::Foundation::PropertyType::String); + } + else if (typeValue == "securestring") + { + parameter->Type(Windows::Foundation::PropertyType::String); + parameter->IsSecure(true); + } + else if (typeValue == "int") + { + parameter->Type(Windows::Foundation::PropertyType::Int64); + } + else if (typeValue == "bool") + { + parameter->Type(Windows::Foundation::PropertyType::Boolean); + } + else if (typeValue == "object") + { + parameter->Type(Windows::Foundation::PropertyType::Inspectable); + } + else if (typeValue == "secureobject") + { + parameter->Type(Windows::Foundation::PropertyType::Inspectable); + parameter->IsSecure(true); + } + else if (typeValue == "array") + { + parameter->Type(Windows::Foundation::PropertyType::InspectableArray); + } + else + { + FIELD_VALUE_ERROR(GetFieldName(FieldName::Type), typeValue, typeNode.Mark()); + } + + // TODO: Consider supporting an expanded set of type strings + } + + void ConfigurationSetParser_0_3::GetStringValueForParameter( + const Node& node, + FieldName field, + ConfigurationParameter* parameter, + void(ConfigurationParameter::* propertyFunction)(const hstring& value)) + { + const Node& valueNode = CHECK_ERROR(GetAndEnsureField(node, field, false, Node::Type::Scalar)); + + if (valueNode) + { + (parameter->*propertyFunction)(hstring{ valueNode.as<std::wstring>() }); + } + } + + void ConfigurationSetParser_0_3::GetUInt32ValueForParameter( + const AppInstaller::YAML::Node& node, + FieldName field, + ConfigurationParameter* parameter, + void(ConfigurationParameter::* propertyFunction)(uint32_t value)) + { + const Node& valueNode = CHECK_ERROR(GetAndEnsureField(node, field, false, Node::Type::Scalar)); + + if (valueNode) + { + int64_t value = valueNode.as<int64_t>(); + if (value < 0 || value > static_cast<int64_t>(std::numeric_limits<uint32_t>::max())) + { + FIELD_VALUE_ERROR(GetFieldName(field), valueNode.as<std::string>(), valueNode.Mark()); + } + (parameter->*propertyFunction)(static_cast<uint32_t>(value)); + } + } + + void ConfigurationSetParser_0_3::ParseObjectValueForParameter( + const AppInstaller::YAML::Node& node, + FieldName field, + Windows::Foundation::PropertyType type, + ConfigurationParameter* parameter, + void(ConfigurationParameter::* propertyFunction)(const Windows::Foundation::IInspectable& value)) + { + const Node& valueNode = CHECK_ERROR(GetAndEnsureField(node, field, false, std::nullopt)); + + if (valueNode) + { + Windows::Foundation::IInspectable valueObject; + CHECK_ERROR(ParseObject(valueNode, field, type, valueObject)); + + (parameter->*propertyFunction)(valueObject); + } + } + + void ConfigurationSetParser_0_3::ParseConfigurationUnitsFromField(const Node& document, FieldName field, std::vector<Configuration::ConfigurationUnit>& result) + { + ParseSequence(document, field, false, Node::Type::Mapping, [&](const Node& item) + { + auto configurationUnit = make_self<wil::details::module_count_wrapper<ConfigurationUnit>>(); + ParseConfigurationUnit(configurationUnit.get(), item); + result.emplace_back(*configurationUnit); + }); + } + + void ConfigurationSetParser_0_3::ParseConfigurationUnit(ConfigurationUnit* unit, const Node& unitNode) + { + // Set unknown intent as the new schema doesn't express it directly + unit->Intent(ConfigurationUnitIntent::Unknown); + + CHECK_ERROR(GetStringValueForUnit(unitNode, FieldName::Name, true, unit, &ConfigurationUnit::Identifier)); + CHECK_ERROR(GetStringValueForUnit(unitNode, FieldName::Type, true, unit, &ConfigurationUnit::Type)); + CHECK_ERROR(ParseValueSet(unitNode, FieldName::Metadata, false, unit->Metadata())); + CHECK_ERROR(ValidateType(unit, unitNode, FieldName::Type, false, true)); + CHECK_ERROR(GetStringArrayForUnit(unitNode, FieldName::DependsOn, false, unit, &ConfigurationUnit::Dependencies)); + + // Regardless of being a group or not, parse the settings. + CHECK_ERROR(ParseValueSet(unitNode, FieldName::Properties, false, unit->Settings())); + + if (ShouldConvertToGroup(unit)) + { + unit->IsGroup(true); + + // TODO: The PS DSC v3 POR looks like it supports each group defining a new schema to be used for its group items. + // Consider supporting that in the future; but for now just use the same schema for everything. + const Node& propertiesNode = GetAndEnsureField(unitNode, FieldName::Properties, false, Node::Type::Mapping); + if (propertiesNode) + { + std::vector<Configuration::ConfigurationUnit> units; + CHECK_ERROR(ParseConfigurationUnitsFromField(propertiesNode, FieldName::Resources, units)); + unit->Units(std::move(units)); + } + } + } + + bool ConfigurationSetParser_0_3::ShouldConvertToGroup(ConfigurationUnit* unit) + { + // Allow the metadata to inform us that we should treat it as a group, including preventing a known type from being treated as one. + auto isGroupObject = unit->Metadata().TryLookup(GetFieldNameHString(FieldName::IsGroupMetadata)); + if (isGroupObject) + { + auto isGroupProperty = isGroupObject.try_as<Windows::Foundation::IPropertyValue>(); + if (isGroupProperty && isGroupProperty.Type() == Windows::Foundation::PropertyType::Boolean) + { + return isGroupProperty.GetBoolean(); + } + } + + // TODO: Check for known types + + return false; + } +} diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_3.h b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_3.h @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ConfigurationSetParser.h" +#include <ConfigurationParameter.h> + +#include <winget/Yaml.h> + +namespace winrt::Microsoft::Management::Configuration::implementation +{ + // Parser for schema version 0.3 + struct ConfigurationSetParser_0_3 : public ConfigurationSetParser + { + ConfigurationSetParser_0_3(AppInstaller::YAML::Node&& document) : m_document(std::move(document)) {} + + virtual ~ConfigurationSetParser_0_3() noexcept = default; + + ConfigurationSetParser_0_3(const ConfigurationSetParser_0_3&) = delete; + ConfigurationSetParser_0_3& operator=(const ConfigurationSetParser_0_3&) = delete; + ConfigurationSetParser_0_3(ConfigurationSetParser_0_3&&) = default; + ConfigurationSetParser_0_3& operator=(ConfigurationSetParser_0_3&&) = default; + + // Retrieve the configuration units from the parser. + void Parse() override; + + // Retrieves the schema version of the parser. + hstring GetSchemaVersion() override; + + protected: + void ParseParameters(ConfigurationSetParser::ConfigurationSetPtr& set); + void ParseParameter(ConfigurationParameter* parameter, const AppInstaller::YAML::Node& node); + void ParseParameterType(ConfigurationParameter* parameter, const AppInstaller::YAML::Node& node); + void GetStringValueForParameter( + const AppInstaller::YAML::Node& node, + FieldName field, + ConfigurationParameter* parameter, + void(ConfigurationParameter::* propertyFunction)(const hstring& value)); + void GetUInt32ValueForParameter( + const AppInstaller::YAML::Node& node, + FieldName field, + ConfigurationParameter* parameter, + void(ConfigurationParameter::* propertyFunction)(uint32_t value)); + void ParseObjectValueForParameter( + const AppInstaller::YAML::Node& node, + FieldName field, + Windows::Foundation::PropertyType type, + ConfigurationParameter* parameter, + void(ConfigurationParameter::* propertyFunction)(const Windows::Foundation::IInspectable& value)); + + void ParseConfigurationUnitsFromField(const AppInstaller::YAML::Node& document, FieldName field, std::vector<Configuration::ConfigurationUnit>& result); + virtual void ParseConfigurationUnit(ConfigurationUnit* unit, const AppInstaller::YAML::Node& unitNode); + // Determines if the given unit should be converted to a group. + bool ShouldConvertToGroup(ConfigurationUnit* unit); + + AppInstaller::YAML::Node m_document; + }; +} diff --git a/src/Microsoft.Management.Configuration/ConfigurationStaticFunctions.cpp b/src/Microsoft.Management.Configuration/ConfigurationStaticFunctions.cpp @@ -6,6 +6,7 @@ #include "ConfigurationUnit.h" #include "ConfigurationSet.h" #include "ConfigurationProcessor.h" +#include "ConfigurationParameter.h" #include <AppInstallerStrings.h> #include <winget/ConfigurationSetProcessorHandlers.h> @@ -38,6 +39,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation { auto result = make_self<wil::details::module_count_wrapper<implementation::ConfigurationProcessor>>(); result->ConfigurationSetProcessorFactory(factory); + result->SetSupportsSchema03(WI_IsFlagSet(m_state, AppInstaller::WinRT::ConfigurationStaticsInternalsStateFlags::Configuration03)); return *result; } @@ -45,4 +47,15 @@ namespace winrt::Microsoft::Management::Configuration::implementation { THROW_HR(E_NOTIMPL); } + + Configuration::ConfigurationParameter ConfigurationStaticFunctions::CreateConfigurationParameter() + { + return *make_self<wil::details::module_count_wrapper<implementation::ConfigurationParameter>>(); + } + + HRESULT STDMETHODCALLTYPE ConfigurationStaticFunctions::SetExperimentalState(UINT32 state) + { + m_state = static_cast<AppInstaller::WinRT::ConfigurationStaticsInternalsStateFlags>(state); + return S_OK; + } } diff --git a/src/Microsoft.Management.Configuration/ConfigurationStaticFunctions.h b/src/Microsoft.Management.Configuration/ConfigurationStaticFunctions.h @@ -2,10 +2,11 @@ // Licensed under the MIT License. #pragma once #include "ConfigurationStaticFunctions.g.h" +#include <winget/IConfigurationStaticsInternals.h> namespace winrt::Microsoft::Management::Configuration::implementation { - struct ConfigurationStaticFunctions : ConfigurationStaticFunctionsT<ConfigurationStaticFunctions> + struct ConfigurationStaticFunctions : ConfigurationStaticFunctionsT<ConfigurationStaticFunctions, winrt::cloaked<AppInstaller::WinRT::IConfigurationStaticsInternals>> { ConfigurationStaticFunctions() = default; @@ -15,6 +16,14 @@ namespace winrt::Microsoft::Management::Configuration::implementation Configuration::ConfigurationProcessor CreateConfigurationProcessor(IConfigurationSetProcessorFactory const& factory); bool IsConfigurationAvailable() { return true; } Windows::Foundation::IAsyncActionWithProgress<uint32_t> EnsureConfigurationAvailableAsync(); + Configuration::ConfigurationParameter CreateConfigurationParameter(); + + // IConfigurationStaticsInternals + HRESULT STDMETHODCALLTYPE SetExperimentalState(UINT32 state); + + private: + // By default, enable all state so that in-proc usage contains it. + AppInstaller::WinRT::ConfigurationStaticsInternalsStateFlags m_state = AppInstaller::WinRT::ConfigurationStaticsInternalsStateFlags::All; }; } namespace winrt::Microsoft::Management::Configuration::factory_implementation diff --git a/src/Microsoft.Management.Configuration/ConfigurationUnit.cpp b/src/Microsoft.Management.Configuration/ConfigurationUnit.cpp @@ -84,7 +84,6 @@ namespace winrt::Microsoft::Management::Configuration::implementation void ConfigurationUnit::Intent(ConfigurationUnitIntent value) { - THROW_HR_IF(E_INVALIDARG, value != ConfigurationUnitIntent::Assert && value != ConfigurationUnitIntent::Inform && value != ConfigurationUnitIntent::Apply); m_intent = value; } @@ -156,11 +155,6 @@ namespace winrt::Microsoft::Management::Configuration::implementation m_isActive = value; } - HRESULT STDMETHODCALLTYPE ConfigurationUnit::SetLifetimeWatcher(IUnknown* watcher) - { - return AppInstaller::WinRT::LifetimeWatcherBase::SetLifetimeWatcher(watcher); - } - Configuration::ConfigurationUnit ConfigurationUnit::Copy() { auto result = make_self<wil::details::module_count_wrapper<ConfigurationUnit>>(); @@ -174,4 +168,51 @@ namespace winrt::Microsoft::Management::Configuration::implementation return *result; } + + bool ConfigurationUnit::IsGroup() + { + return m_isGroup; + } + + void ConfigurationUnit::IsGroup(bool value) + { + m_isGroup = value; + + if (value) + { + if (!m_units) + { + m_units = winrt::single_threaded_vector<Configuration::ConfigurationUnit>(); + } + } + } + + Windows::Foundation::Collections::IVector<Configuration::ConfigurationUnit> ConfigurationUnit::Units() + { + return m_units; + } + + void ConfigurationUnit::Units(const Windows::Foundation::Collections::IVector<Configuration::ConfigurationUnit>& value) + { + if (m_isGroup) + { + THROW_HR_IF(E_POINTER, !value); + } + else if (value) + { + m_isGroup = true; + } + + m_units = value; + } + + void ConfigurationUnit::Units(std::vector<Configuration::ConfigurationUnit>&& value) + { + m_units = winrt::single_threaded_vector<Configuration::ConfigurationUnit>(std::move(value)); + } + + HRESULT STDMETHODCALLTYPE ConfigurationUnit::SetLifetimeWatcher(IUnknown* watcher) + { + return AppInstaller::WinRT::LifetimeWatcherBase::SetLifetimeWatcher(watcher); + } } diff --git a/src/Microsoft.Management.Configuration/ConfigurationUnit.h b/src/Microsoft.Management.Configuration/ConfigurationUnit.h @@ -47,11 +47,18 @@ namespace winrt::Microsoft::Management::Configuration::implementation Configuration::ConfigurationUnit Copy(); + bool IsGroup(); + void IsGroup(bool value); + + Windows::Foundation::Collections::IVector<Configuration::ConfigurationUnit> Units(); + void Units(const Windows::Foundation::Collections::IVector<Configuration::ConfigurationUnit>& value); + HRESULT STDMETHODCALLTYPE SetLifetimeWatcher(IUnknown* watcher); #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) void Dependencies(std::vector<hstring>&& value); void Details(IConfigurationUnitProcessorDetails&& details); + void Units(std::vector<Configuration::ConfigurationUnit>&& value); private: hstring m_type; @@ -63,6 +70,8 @@ namespace winrt::Microsoft::Management::Configuration::implementation Windows::Foundation::Collections::ValueSet m_settings; IConfigurationUnitProcessorDetails m_details{ nullptr }; bool m_isActive = true; + bool m_isGroup = false; + Windows::Foundation::Collections::IVector<Configuration::ConfigurationUnit> m_units = nullptr; #endif }; } diff --git a/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.idl b/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.idl @@ -2,7 +2,7 @@ // Licensed under the MIT License. namespace Microsoft.Management.Configuration { - [contractversion(1)] + [contractversion(2)] apicontract Contract{}; // The current state of a configuration set. @@ -70,8 +70,7 @@ namespace Microsoft.Management.Configuration // The system state is causing the error. SystemState, // The configuration unit was not run due to a precondition not being met. - // For example, when an assert in the configuration set is not in the desired state, - // all of the units with Apply intent will have this set. + // For example, if a dependency fails to be applied, this will be set. Precondition, }; @@ -170,7 +169,14 @@ namespace Microsoft.Management.Configuration Windows.Foundation.Collections.IVectorView<IConfigurationUnitSettingDetails> Settings{ get; }; // Does it comes from a public repository Boolean IsPublic{ get; }; + } + // Provides information for a specific configuration unit within the runtime. + [contract(Microsoft.Management.Configuration.Contract, 2)] + interface IConfigurationUnitProcessorDetails2 requires IConfigurationUnitProcessorDetails + { + // Determines if this configuration unit should be treated as a group. + Boolean IsGroup{ get; }; } // Defines how the configuration unit is to be used within the configuration system. @@ -240,6 +246,16 @@ namespace Microsoft.Management.Configuration // Details will be the same value (not a copy, just another reference) // State, ResultInformation, and ShouldApply will be their default constructed state ConfigurationUnit Copy(); + + [contract(Microsoft.Management.Configuration.Contract, 2)] + { + // Determines if this configuration unit should be treated as a group. + // A configuration unit group treats its `Settings` as the definition of child units. + Boolean IsGroup; + + // The configuration units that are part of this unit (if IsGroup is true). + Windows.Foundation.Collections.IVector<ConfigurationUnit> Units; + } } // The change event type that has occurred for a configuration set change. @@ -273,6 +289,49 @@ namespace Microsoft.Management.Configuration ConfigurationUnit Unit{ get; }; } + // The definition of a configuration parameter; a value that may be provided to alter the processing of a configuration set. + [contract(Microsoft.Management.Configuration.Contract, 2)] + runtimeclass ConfigurationParameter + { + ConfigurationParameter(); + + // The name of the parameter. + String Name; + + // The description of the parameter. + String Description; + + // The metadata properties associated with the configuration parameter. + Windows.Foundation.Collections.ValueSet Metadata; + + // The value of the parameter should be treated as a secret; not logged our output. + Boolean IsSecure; + + // The type of the parameter. + Windows.Foundation.PropertyType Type; + + // The default value; may be null if no default is provided. + Object DefaultValue; + + // The set of allowed values; a null container indicates that the values are not restricted. + Windows.Foundation.Collections.IVector<Object> AllowedValues; + + // The minimum length for a parameter type with the concept (string, array, etc.). + UInt32 MinimumLength; + + // The maximum length for a parameter type with the concept (string, array, etc.). + UInt32 MaximumLength; + + // For comparable parameter types, the minimum value allowed (integrals, DateTime, etc.). + Object MinimumValue; + + // For comparable parameter types, the maximum value allowed (integrals, DateTime, etc.). + Object MaximumValue; + + // The input value; may be null if no value is provided. + Object ProvidedValue; + } + // A configuration set contains a collection of configuration units and details about the set. [contract(Microsoft.Management.Configuration.Contract, 1)] runtimeclass ConfigurationSet @@ -305,6 +364,7 @@ namespace Microsoft.Management.Configuration // The schema version to use for the set. // Will be set to the schema version when read in, and default to the latest if created manually. + // Setting SchemaVersion to a different value will change SchemaUri. String SchemaVersion; // Only changes for this set are sent to this event. @@ -316,6 +376,23 @@ namespace Microsoft.Management.Configuration // Removes the configuration set from the recorded history, if present. void Remove(); + + [contract(Microsoft.Management.Configuration.Contract, 2)] + { + // The metadata properties associated with the configuration set. + Windows.Foundation.Collections.ValueSet Metadata; + + // The parameters that this configuration set supports. + Windows.Foundation.Collections.IVector<ConfigurationParameter> Parameters; + + // The variables that this configuration set uses. + Windows.Foundation.Collections.ValueSet Variables; + + // The schema URI to use for the set. + // Will be set to the schema version when read in, and default to the latest if created manually. + // Setting SchemaUri to a different value will change SchemaVersion. + Windows.Foundation.Uri SchemaUri; + } } // The result of applying the settings with an IConfigurationUnitProcessor. @@ -677,7 +754,6 @@ namespace Microsoft.Management.Configuration event Windows.Foundation.TypedEventHandler<ConfigurationSet, ConfigurationChangeData> ConfigurationChange; // Gets the configuration sets that have already been applied or with the intent to be applied (this may include in progress sets or those that are waiting on others). - // These configuration sets will be marked as immutable. Windows.Foundation.Collections.IVector<ConfigurationSet> GetConfigurationHistory(); Windows.Foundation.IAsyncOperation< Windows.Foundation.Collections.IVector<ConfigurationSet> > GetConfigurationHistoryAsync(); @@ -734,8 +810,16 @@ namespace Microsoft.Management.Configuration } // Top level entry point for configuration, enabling easier usage in out-of-process scenarios. + [contract(Microsoft.Management.Configuration.Contract, 2)] + interface IConfigurationStatics2 requires IConfigurationStatics + { + // Creates an empty configuration parameter. + ConfigurationParameter CreateConfigurationParameter(); + } + + // Top level entry point for configuration, enabling easier usage in out-of-process scenarios. [contract(Microsoft.Management.Configuration.Contract, 1)] - runtimeclass ConfigurationStaticFunctions : [default]IConfigurationStatics + runtimeclass ConfigurationStaticFunctions : [default]IConfigurationStatics, IConfigurationStatics2 { ConfigurationStaticFunctions(); } diff --git a/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj b/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj @@ -194,10 +194,12 @@ <ItemGroup> <ClInclude Include="ApplyConfigurationSetResult.h" /> <ClInclude Include="ApplyConfigurationUnitResult.h" /> + <ClInclude Include="ArgumentValidation.h" /> <ClInclude Include="ConfigThreadGlobals.h" /> <ClInclude Include="ConfigurationChangeData.h" /> <ClInclude Include="ConfigurationConflict.h" /> <ClInclude Include="ConfigurationConflictSetting.h" /> + <ClInclude Include="ConfigurationParameter.h" /> <ClInclude Include="ConfigurationProcessor.h" /> <ClInclude Include="ConfigurationSet.h" /> <ClInclude Include="ConfigurationSetApplyProcessor.h" /> @@ -206,6 +208,7 @@ <ClInclude Include="ConfigurationSetParserError.h" /> <ClInclude Include="ConfigurationSetParser_0_1.h" /> <ClInclude Include="ConfigurationSetParser_0_2.h" /> + <ClInclude Include="ConfigurationSetParser_0_3.h" /> <ClInclude Include="ConfigurationStaticFunctions.h" /> <ClInclude Include="ConfigurationUnit.h" /> <ClInclude Include="ConfigurationUnitResultInformation.h" /> @@ -215,6 +218,7 @@ <ClInclude Include="GetConfigurationUnitDetailsResult.h" /> <ClInclude Include="GetConfigurationUnitSettingsResult.h" /> <ClInclude Include="OpenConfigurationSetResult.h" /> + <ClInclude Include="ParsingMacros.h" /> <ClInclude Include="pch.h" /> <ClInclude Include="Telemetry\Telemetry.h" /> <ClInclude Include="Telemetry\TraceLogging.h" /> @@ -224,10 +228,12 @@ <ItemGroup> <ClCompile Include="ApplyConfigurationSetResult.cpp" /> <ClCompile Include="ApplyConfigurationUnitResult.cpp" /> + <ClCompile Include="ArgumentValidation.cpp" /> <ClCompile Include="ConfigThreadGlobals.cpp" /> <ClCompile Include="ConfigurationChangeData.cpp" /> <ClCompile Include="ConfigurationConflict.cpp" /> <ClCompile Include="ConfigurationConflictSetting.cpp" /> + <ClCompile Include="ConfigurationParameter.cpp" /> <ClCompile Include="ConfigurationProcessor.cpp" /> <ClCompile Include="ConfigurationSet.cpp" /> <ClCompile Include="ConfigurationSetApplyProcessor.cpp" /> @@ -235,6 +241,7 @@ <ClCompile Include="ConfigurationSetParser.cpp" /> <ClCompile Include="ConfigurationSetParser_0_1.cpp" /> <ClCompile Include="ConfigurationSetParser_0_2.cpp" /> + <ClCompile Include="ConfigurationSetParser_0_3.cpp" /> <ClCompile Include="ConfigurationStaticFunctions.cpp" /> <ClCompile Include="ConfigurationUnit.cpp" /> <ClCompile Include="ConfigurationUnitResultInformation.cpp" /> diff --git a/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters b/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters @@ -78,6 +78,15 @@ <ClCompile Include="DiagnosticInformationInstance.cpp"> <Filter>Internals</Filter> </ClCompile> + <ClCompile Include="ConfigurationParameter.cpp"> + <Filter>API Source</Filter> + </ClCompile> + <ClCompile Include="ArgumentValidation.cpp"> + <Filter>Internals</Filter> + </ClCompile> + <ClCompile Include="ConfigurationSetParser_0_3.cpp"> + <Filter>Parser</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <ClInclude Include="pch.h" /> @@ -162,6 +171,18 @@ <ClInclude Include="DiagnosticInformationInstance.h"> <Filter>Internals</Filter> </ClInclude> + <ClInclude Include="ConfigurationParameter.h"> + <Filter>API Headers</Filter> + </ClInclude> + <ClInclude Include="ArgumentValidation.h"> + <Filter>Internals</Filter> + </ClInclude> + <ClInclude Include="ParsingMacros.h"> + <Filter>Parser</Filter> + </ClInclude> + <ClInclude Include="ConfigurationSetParser_0_3.h"> + <Filter>Parser</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <Midl Include="Microsoft.Management.Configuration.idl" /> diff --git a/src/Microsoft.Management.Configuration/ParsingMacros.h b/src/Microsoft.Management.Configuration/ParsingMacros.h @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#define CHECK_ERROR(_op_) (_op_); if (FAILED(m_result)) { return; } + +#define FIELD_TYPE_ERROR(_field_,_mark_) SetError(WINGET_CONFIG_ERROR_INVALID_FIELD_TYPE, (_field_), (_mark_)); return +#define FIELD_TYPE_ERROR_IF(_condition_,_field_,_mark_) if (_condition_) { FIELD_TYPE_ERROR(_field_,_mark_); } + +#define FIELD_MISSING_ERROR(_field_) SetError(WINGET_CONFIG_ERROR_MISSING_FIELD, (_field_)); return +#define FIELD_MISSING_ERROR_IF(_condition_,_field_) if (_condition_) { FIELD_MISSING_ERROR(_field_); } + +#define FIELD_VALUE_ERROR(_field_,_value_,_mark_) SetError(WINGET_CONFIG_ERROR_INVALID_FIELD_VALUE, (_field_), (_mark_), (_value_)); return +#define FIELD_VALUE_ERROR_IF(_condition_,_field_,_value_,_mark_) if (_condition_) { FIELD_VALUE_ERROR(_field_,_value_,_mark_); } diff --git a/src/Microsoft.Management.Configuration/Telemetry/Telemetry.cpp b/src/Microsoft.Management.Configuration/Telemetry/Telemetry.cpp @@ -131,6 +131,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation summaryItem = &result.InformSummary; break; case ConfigurationUnitIntent::Apply: + case ConfigurationUnitIntent::Unknown: summaryItem = &result.ApplySummary; break; default: @@ -215,6 +216,12 @@ namespace winrt::Microsoft::Management::Configuration::implementation ConfigurationUnitResultSource failurePoint, std::wstring_view settingNames) const noexcept try { + // Change unknown to Apply for telemetry, as it will have been treated that way + if (unitIntent == ConfigurationUnitIntent::Unknown) + { + unitIntent = ConfigurationUnitIntent::Apply; + } + if (IsTelemetryEnabled()) { AICLI_TraceLoggingWriteActivity( diff --git a/src/Microsoft.Management.Configuration/TestConfigurationSetResult.cpp b/src/Microsoft.Management.Configuration/TestConfigurationSetResult.cpp @@ -14,26 +14,29 @@ namespace winrt::Microsoft::Management::Configuration::implementation { m_unitResults.Append(unitResult); - ConfigurationTestResult unitValue = unitResult.TestResult(); - // Also aggregate the result of this incoming test into the overall result - switch (m_testResult) + m_testResult = FoldInTestResult(m_testResult, unitResult.TestResult()); + } + + ConfigurationTestResult TestConfigurationSetResult::FoldInTestResult(ConfigurationTestResult current, ConfigurationTestResult incoming) + { + switch (current) { case ConfigurationTestResult::Unknown: case ConfigurationTestResult::NotRun: // In these "default" cases, just take the unit result - m_testResult = unitValue; + return incoming; break; case ConfigurationTestResult::Positive: - if (unitValue == ConfigurationTestResult::Negative || unitValue == ConfigurationTestResult::Failed) + if (incoming == ConfigurationTestResult::Negative || incoming == ConfigurationTestResult::Failed) { - m_testResult = unitValue; + return incoming; } break; case ConfigurationTestResult::Negative: - if (unitValue == ConfigurationTestResult::Failed) + if (incoming == ConfigurationTestResult::Failed) { - m_testResult = unitValue; + return incoming; } break; case ConfigurationTestResult::Failed: @@ -42,6 +45,8 @@ namespace winrt::Microsoft::Management::Configuration::implementation default: THROW_HR(E_UNEXPECTED); } + + return current; } Windows::Foundation::Collections::IVectorView<TestConfigurationUnitResult> TestConfigurationSetResult::UnitResults() const diff --git a/src/Microsoft.Management.Configuration/TestConfigurationSetResult.h b/src/Microsoft.Management.Configuration/TestConfigurationSetResult.h @@ -13,6 +13,8 @@ namespace winrt::Microsoft::Management::Configuration::implementation #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) void AppendUnitResult(const TestConfigurationUnitResult& unitResult); void TestResult(ConfigurationTestResult value); + + static ConfigurationTestResult FoldInTestResult(ConfigurationTestResult current, ConfigurationTestResult incoming); #endif Windows::Foundation::Collections::IVectorView<TestConfigurationUnitResult> UnitResults() const; diff --git a/src/Microsoft.Management.Configuration/pch.h b/src/Microsoft.Management.Configuration/pch.h @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once + +#define NOMINMAX + #include <unknwn.h> #include <winrt/Windows.Foundation.h> #include <winrt/Windows.Foundation.Collections.h> @@ -18,6 +21,7 @@ #include <filesystem> #include <fstream> #include <functional> +#include <limits> #include <map> #include <memory> #include <mutex> diff --git a/src/PowerShell/tests/Microsoft.WinGet.Configuration.Tests.ps1 b/src/PowerShell/tests/Microsoft.WinGet.Configuration.Tests.ps1 @@ -302,7 +302,7 @@ Describe 'Get configuration' { It 'Missing property' { $testFile = GetConfigTestDataFile "NotConfig.yml" - { Get-WinGetConfiguration -File $testFile } | Should -Throw "*0x8A15C00E*properties*missing*" + { Get-WinGetConfiguration -File $testFile } | Should -Throw '*0x8A15C00E*$schema*missing*' } It 'Missing configurationVersion' { diff --git a/src/WindowsPackageManager/ConfigurationStaticFunctions.cpp b/src/WindowsPackageManager/ConfigurationStaticFunctions.cpp @@ -3,19 +3,22 @@ #include <Unknwn.h> #include <wil\cppwinrt_wrl.h> #include <winrt/Microsoft.Management.Configuration.h> +#include <winrt/Microsoft.Management.Deployment.h> #include <ComClsids.h> #include <AppInstallerErrors.h> #include <AppInstallerFileLogger.h> +#include <AppInstallerLanguageUtilities.h> #include <AppInstallerStrings.h> #include <winget/ConfigurationSetProcessorHandlers.h> #include <ConfigurationSetProcessorFactoryRemoting.h> #include <winget/ILifetimeWatcher.h> +#include <winget/IConfigurationStaticsInternals.h> #include <winget/GroupPolicy.h> #include <winget/Security.h> #include <winget/ThreadGlobals.h> #include <winget/SelfManagement.h> #include <winget/MSStore.h> -#include <winrt/Microsoft.Management.Deployment.h> +#include <winget/ExperimentalFeature.h> using namespace AppInstaller::SelfManagement; using namespace winrt::Microsoft::Management::Deployment; @@ -38,7 +41,9 @@ namespace ConfigurationShim struct DECLSPEC_UUID(WINGET_OUTOFPROC_COM_CLSID_ConfigurationStaticFunctions) - ConfigurationStaticFunctionsShim : winrt::implements<ConfigurationStaticFunctionsShim, winrt::Microsoft::Management::Configuration::IConfigurationStatics> + ConfigurationStaticFunctionsShim : winrt::implements<ConfigurationStaticFunctionsShim, + winrt::Microsoft::Management::Configuration::IConfigurationStatics, + winrt::Microsoft::Management::Configuration::IConfigurationStatics2> { ConfigurationStaticFunctionsShim() { @@ -50,7 +55,14 @@ namespace ConfigurationShim if (IsConfigurationAvailable()) { - m_statics = winrt::Microsoft::Management::Configuration::ConfigurationStaticFunctions(); + m_statics = winrt::Microsoft::Management::Configuration::ConfigurationStaticFunctions().as<winrt::Microsoft::Management::Configuration::IConfigurationStatics2>(); + + // Forward the current feature state to the internal statics + using namespace AppInstaller; + using Flags = WinRT::ConfigurationStaticsInternalsStateFlags; + + Flags flags = Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::Configuration03) ? Flags::Configuration03 : Flags::None; + m_statics.as<AppInstaller::WinRT::IConfigurationStaticsInternals>()->SetExperimentalState(ToIntegral(flags)); } } @@ -193,6 +205,20 @@ namespace ConfigurationShim s_canBeCreated = false; } + winrt::Microsoft::Management::Configuration::ConfigurationParameter CreateConfigurationParameter() + { + THROW_HR_IF(CO_E_CLASS_DISABLED, !s_canBeCreated); + + if (!m_statics) + { + THROW_HR(APPINSTALLER_CLI_ERROR_PACKAGE_IS_STUB); + } + + auto result = m_statics.CreateConfigurationParameter(); + result.as<AppInstaller::WinRT::ILifetimeWatcher>()->SetLifetimeWatcher(CreateLifetimeWatcher()); + return result; + } + private: // Returns a lifetime watcher object that is currently *unowned*. IUnknown* CreateLifetimeWatcher() @@ -204,7 +230,7 @@ namespace ConfigurationShim return out.detach(); } - winrt::Microsoft::Management::Configuration::ConfigurationStaticFunctions m_statics = nullptr; + winrt::Microsoft::Management::Configuration::IConfigurationStatics2 m_statics = nullptr; AppInstaller::ThreadLocalStorage::WingetThreadGlobals m_threadGlobals; }; @@ -218,7 +244,6 @@ namespace ConfigurationShim *object = nullptr; RETURN_HR_IF(APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY, !::AppInstaller::Settings::GroupPolicies().IsEnabled(::AppInstaller::Settings::TogglePolicy::Policy::WinGet)); RETURN_HR_IF(APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY, !::AppInstaller::Settings::GroupPolicies().IsEnabled(::AppInstaller::Settings::TogglePolicy::Policy::Configuration)); - // TODO: Review of security for configuration OOP RETURN_HR_IF(E_ACCESSDENIED, !::AppInstaller::Security::IsCOMCallerSameUserAndIntegrityLevel()); RETURN_HR_IF(CO_E_CLASS_DISABLED, !s_canBeCreated);