diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index ce8e06d60e..930e9f6146 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -431,6 +431,7 @@ winapifamily windir windowsdeveloper winerror +wingetconfigroot wingetcreate wingetdev wingetutil diff --git a/src/AppInstallerSharedLib/Public/AppInstallerErrors.h b/src/AppInstallerSharedLib/Public/AppInstallerErrors.h index 8e6fa30524..fdd0183654 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerErrors.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerErrors.h @@ -178,6 +178,7 @@ #define WINGET_CONFIG_ERROR_UNIT_MODULE_CONFLICT ((HRESULT)0x8A15C107) #define WINGET_CONFIG_ERROR_UNIT_IMPORT_MODULE ((HRESULT)0x8A15C108) #define WINGET_CONFIG_ERROR_UNIT_INVOKE_INVALID_RESULT ((HRESULT)0x8A15C109) +#define WINGET_CONFIG_ERROR_UNIT_SETTING_CONFIG_ROOT ((HRESULT)0x8A15C110) namespace AppInstaller { diff --git a/src/Microsoft.Management.Configuration.Processor/Exceptions/ErrorCodes.cs b/src/Microsoft.Management.Configuration.Processor/Exceptions/ErrorCodes.cs index 5431a296bf..89b32996c8 100644 --- a/src/Microsoft.Management.Configuration.Processor/Exceptions/ErrorCodes.cs +++ b/src/Microsoft.Management.Configuration.Processor/Exceptions/ErrorCodes.cs @@ -60,5 +60,10 @@ internal static class ErrorCodes /// The unit returned an invalid result. /// internal const int WinGetConfigUnitInvokeInvalidResult = unchecked((int)0x8A15C109); + + /// + /// The unit contains a setting that requires config root. + /// + internal const int WinGetConfigUnitSettingConfigRoot = unchecked((int)0x8A15C110); } } diff --git a/src/Microsoft.Management.Configuration.Processor/Exceptions/UnitSettingConfigRootException.cs b/src/Microsoft.Management.Configuration.Processor/Exceptions/UnitSettingConfigRootException.cs new file mode 100644 index 0000000000..54aa4c051f --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/Exceptions/UnitSettingConfigRootException.cs @@ -0,0 +1,39 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Exceptions +{ + using System; + + /// + /// A setting uses the config root variable and the Path was not set in the ConfigurationSet. + /// + internal class UnitSettingConfigRootException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// Unit name. + /// Setting. + public UnitSettingConfigRootException(string unitName, string setting) + : base($"Unit: {unitName} Setting {setting} requires the ConfigurationSet Path") + { + this.HResult = ErrorCodes.WinGetConfigUnitSettingConfigRoot; + this.UnitName = unitName; + this.Setting = setting; + } + + /// + /// Gets the resource name. + /// + public string UnitName { get; } + + /// + /// Gets the setting that reference the config root variable. + /// + public string Setting { get; } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitAndResource.cs b/src/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitAndResource.cs index 5afa2d4fcf..645d298499 100644 --- a/src/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitAndResource.cs +++ b/src/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitAndResource.cs @@ -99,14 +99,12 @@ public string ResourceName } /// - /// TODO: Implement. - /// I am so sad because rs.SessionStateProxy.InvokeCommand.ExpandString doesn't work as I wanted. - /// PowerShell assumes all code passed to ExpandString is trusted and we cannot assume that. + /// Gets the settings of the unit. /// /// ValueSet with settings. - public ValueSet GetExpandedSettings() + public ValueSet GetSettings() { - return this.Unit.Settings; + return this.UnitInternal.GetExpandedSettings(); } } } diff --git a/src/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitInternal.cs b/src/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitInternal.cs index 6a8b1dc654..1d91a4e0b2 100644 --- a/src/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitInternal.cs +++ b/src/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitInternal.cs @@ -6,9 +6,13 @@ namespace Microsoft.Management.Configuration.Processor.Helpers { + using System; using System.Collections.Generic; + using System.IO; using Microsoft.Management.Configuration.Processor.Constants; + using Microsoft.Management.Configuration.Processor.Exceptions; using Microsoft.PowerShell.Commands; + using Windows.Foundation.Collections; /// /// Wrapper around Configuration units and its directives. Creates a normalized directives map @@ -16,15 +20,20 @@ namespace Microsoft.Management.Configuration.Processor.Helpers /// internal class ConfigurationUnitInternal { + private const string ConfigRootVar = "${WinGetConfigRoot}"; + + private readonly string? configurationFileRootPath = null; private readonly Dictionary normalizedDirectives = new (); /// /// Initializes a new instance of the class. /// /// Configuration unit. + /// The configuration file path. /// Directives overlay. public ConfigurationUnitInternal( ConfigurationUnit unit, + string configurationFilePath, IReadOnlyDictionary? directivesOverlay = null) { this.Unit = unit; @@ -45,6 +54,16 @@ internal class ConfigurationUnitInternal this.GetDirective(DirectiveConstants.MaxVersion), this.GetDirective(DirectiveConstants.ModuleGuid)); } + + if (!string.IsNullOrEmpty(configurationFilePath)) + { + if (!File.Exists(configurationFilePath)) + { + throw new FileNotFoundException(configurationFilePath); + } + + this.configurationFileRootPath = Path.GetDirectoryName(configurationFilePath); + } } /// @@ -150,6 +169,57 @@ public string ToIdentifyingString() return null; } + /// + /// TODO: Implement for more variables. + /// I am so sad because rs.SessionStateProxy.InvokeCommand.ExpandString doesn't work as I wanted. + /// PowerShell assumes all code passed to ExpandString is trusted and we cannot assume that. + /// + /// ValueSet with settings. + public ValueSet GetExpandedSettings() + { + var valueSet = new ValueSet(); + foreach (var value in this.Unit.Settings) + { + if (value.Value is string) + { + // For now, we just expand config root. + valueSet.Add(value.Key, this.ExpandConfigRoot(value.Value as string, value.Key)); + } + else + { + valueSet.Add(value); + } + } + + return valueSet; + } + + private string? ExpandConfigRoot(string? value, string settingName) + { + if (!string.IsNullOrEmpty(value)) + { + // TODO: since we only support one variable, this only finds and replace + // ${WingetConfigRoot} if found in the string when the work of expanding + // string is done it should take into account other operators like the subexpression operator $() + if (value.Contains(ConfigRootVar, StringComparison.OrdinalIgnoreCase)) + { + if (string.IsNullOrEmpty(this.configurationFileRootPath)) + { + throw new UnitSettingConfigRootException(this.Unit.UnitName, settingName); + } + + if (this.configurationFileRootPath == null) + { + throw new ArgumentException(); + } + + return value.Replace(ConfigRootVar, this.configurationFileRootPath, StringComparison.OrdinalIgnoreCase); + } + } + + return value; + } + private void InitializeDirectives() { // Overlay directives have precedence. diff --git a/src/Microsoft.Management.Configuration.Processor/Set/ConfigurationSetProcessor.cs b/src/Microsoft.Management.Configuration.Processor/Set/ConfigurationSetProcessor.cs index 4a0cdc3a51..e11e0a3824 100644 --- a/src/Microsoft.Management.Configuration.Processor/Set/ConfigurationSetProcessor.cs +++ b/src/Microsoft.Management.Configuration.Processor/Set/ConfigurationSetProcessor.cs @@ -57,7 +57,7 @@ public ConfigurationSetProcessor(IProcessorEnvironment processorEnvironment, Con { try { - var configurationUnitInternal = new ConfigurationUnitInternal(unit, directivesOverlay); + var configurationUnitInternal = new ConfigurationUnitInternal(unit, this.configurationSet.Path, directivesOverlay); this.OnDiagnostics(DiagnosticLevel.Verbose, $"Creating unit processor for: {configurationUnitInternal.ToIdentifyingString()}..."); var dscResourceInfo = this.PrepareUnitForProcessing(configurationUnitInternal); @@ -87,7 +87,7 @@ public ConfigurationSetProcessor(IProcessorEnvironment processorEnvironment, Con { try { - var unitInternal = new ConfigurationUnitInternal(unit); + var unitInternal = new ConfigurationUnitInternal(unit, this.configurationSet.Path); this.OnDiagnostics(DiagnosticLevel.Verbose, $"Getting unit details [{detailLevel}] for: {unitInternal.ToIdentifyingString()}"); var dscResourceInfo = this.ProcessorEnvironment.GetDscResource(unitInternal); diff --git a/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessor.cs b/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessor.cs index 3fba8c04d8..ddc77a04f4 100644 --- a/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessor.cs +++ b/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessor.cs @@ -64,7 +64,7 @@ public GetSettingsResult GetSettings() try { result.Settings = this.processorEnvironment.InvokeGetResource( - this.unitResource.GetExpandedSettings(), + this.unitResource.GetSettings(), this.unitResource.ResourceName, this.unitResource.Module); } @@ -97,7 +97,7 @@ public TestSettingsResult TestSettings() try { bool testResult = this.processorEnvironment.InvokeTestResource( - this.unitResource.GetExpandedSettings(), + this.unitResource.GetSettings(), this.unitResource.ResourceName, this.unitResource.Module); @@ -132,7 +132,7 @@ public ApplySettingsResult ApplySettings() try { result.RebootRequired = this.processorEnvironment.InvokeSetResource( - this.unitResource.GetExpandedSettings(), + this.unitResource.GetSettings(), this.unitResource.ResourceName, this.unitResource.Module); } diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationDetailsTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationDetailsTests.cs index 25b674e2ca..7e8573352f 100644 --- a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationDetailsTests.cs +++ b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationDetailsTests.cs @@ -189,7 +189,7 @@ private ConfigurationUnit CreteConfigurationUnit() // This is easier than trying to mock sealed class from external code... var testEnv = this.fixture.PrepareTestProcessorEnvironment(true); - var dscResourceInfo = testEnv.GetDscResource(new ConfigurationUnitInternal(unit, null)); + var dscResourceInfo = testEnv.GetDscResource(new ConfigurationUnitInternal(unit, string.Empty, null)); var psModuleInfo = testEnv.GetAvailableModule(PowerShellHelpers.CreateModuleSpecification("xSimpleTestResource", "0.0.0.1")); if (dscResourceInfo is null || psModuleInfo is null) diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationSetProcessorTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationSetProcessorTests.cs index d1d18b7f71..2214f8c59e 100644 --- a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationSetProcessorTests.cs +++ b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationSetProcessorTests.cs @@ -694,7 +694,7 @@ private ConfigurationUnit CreteConfigurationUnit() { // This is easier than trying to mock sealed class from external code... var testEnv = this.fixture.PrepareTestProcessorEnvironment(true); - var dscResourceInfo = testEnv.GetDscResource(new ConfigurationUnitInternal(unit, null)); + var dscResourceInfo = testEnv.GetDscResource(new ConfigurationUnitInternal(unit, string.Empty, null)); var psModuleInfo = testEnv.GetAvailableModule(PowerShellHelpers.CreateModuleSpecification("xSimpleTestResource", "0.0.0.1")); if (dscResourceInfo is null || psModuleInfo is null) diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationUnitInternalTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationUnitInternalTests.cs index 7cf05f480f..e48f80b76b 100644 --- a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationUnitInternalTests.cs +++ b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationUnitInternalTests.cs @@ -8,10 +8,13 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests { using System; using System.Collections.Generic; + using System.IO; using System.Management.Automation; using Microsoft.Management.Configuration; + using Microsoft.Management.Configuration.Processor.Exceptions; using Microsoft.Management.Configuration.Processor.Helpers; using Microsoft.Management.Configuration.UnitTests.Fixtures; + using Microsoft.Management.Configuration.UnitTests.Helpers; using Xunit; using Xunit.Abstractions; @@ -73,7 +76,7 @@ public void GetDirectivesTest() { anotherDirective, overlayAnother }, }; - var unitInternal = new ConfigurationUnitInternal(unit, overlays); + var unitInternal = new ConfigurationUnitInternal(unit, string.Empty, overlays); var description = unitInternal.GetDirective(descriptionDirective); Assert.Equal(description, overlayDescription); @@ -107,7 +110,61 @@ public void GetVersion_BadVersion() unit.Directives.Add("version", "not a version"); Assert.Throws( - () => new ConfigurationUnitInternal(unit, null)); + () => new ConfigurationUnitInternal(unit, string.Empty, null)); + } + + /// + /// Verifies expansion of ConfigRoot. + /// + [Fact] + public void GetExpandedSettings_ConfigRoot() + { + using var tmpFile = new TempFile("fakeConfigFile.yml", content: "content"); + + var unit = new ConfigurationUnit(); + 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"); + unit.Settings.Add("var4", @"this\is\a\${WINGETCONFIGROOT}\path.txt"); + unit.Settings.Add("var5", @"this\is\a\path\$wingetconfigroot"); + unit.Settings.Add("var6", @"this\is\a\path\${wingetconfigroot}"); + + string configPath = tmpFile.FullFileName; + string? expectedPath = Path.GetDirectoryName(configPath); + var unitInternal = new ConfigurationUnitInternal(unit, configPath); + + var expandedSettings = unitInternal.GetExpandedSettings(); + + var var1 = expandedSettings["var1"]; + Assert.Equal(@"$WinGetConfigRoot\this\is\a\path.txt", var1 as string); + + var var2 = expandedSettings["var2"]; + Assert.Equal($@"{expectedPath}\this\is\a\path.txt", var2 as string); + + var var3 = expandedSettings["var3"]; + Assert.Equal(@"this\is\a\$WINGETCONFIGROOT\path.txt", var3 as string); + + var var4 = expandedSettings["var4"]; + Assert.Equal($@"this\is\a\{expectedPath}\path.txt", var4 as string); + + var var5 = expandedSettings["var5"]; + Assert.Equal(@"this\is\a\path\$wingetconfigroot", var5 as string); + + var var6 = expandedSettings["var6"]; + Assert.Equal($@"this\is\a\path\{expectedPath}", var6 as string); + } + + /// + /// Verifies throws when config root is not set. + /// + [Fact] + public void GetExpandedSetting_ConfigRoot_Throw() + { + var unit = new ConfigurationUnit(); + unit.Settings.Add("var2", @"${WinGetConfigRoot}\this\is\a\path.txt"); + + var unitInternal = new ConfigurationUnitInternal(unit, null!); + Assert.Throws(() => unitInternal.GetExpandedSettings()); } } } diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationUnitProcessorTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationUnitProcessorTests.cs index 0cdf6d9b87..467fa486a6 100644 --- a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationUnitProcessorTests.cs +++ b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationUnitProcessorTests.cs @@ -362,6 +362,7 @@ private ConfigurationUnitAndResource CreateUnitResource(ConfigurationUnitIntent UnitName = resourceName, Intent = intent, }, + string.Empty, new Dictionary()), new DscResourceInfoInternal(resourceName, null, null)); }