Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ namespace AppInstaller::CLI::ConfigurationRemoting
winrt::hstring pathIsAlias = m_dynamicFactory->Lookup(ToHString(PropertyName::FoundDscExecutablePathIsAlias));
if (!pathIsAlias.empty())
{
json["processorPathIsAlias"] = (pathIsAlias == L"true");
json["processorPathIsAlias"] = Utility::TryConvertStringToBool(std::wstring_view{ pathIsAlias }).value_or(false);
}
}
else
Expand All @@ -385,7 +385,7 @@ namespace AppInstaller::CLI::ConfigurationRemoting
auto pathIsAlias = m_dynamicFactory->GetFactoryMapValue(ToHString(PropertyName::DscExecutablePathIsAlias));
if (pathIsAlias)
{
json["processorPathIsAlias"] = (pathIsAlias.value() == L"true");
json["processorPathIsAlias"] = Utility::TryConvertStringToBool(std::wstring_view{ pathIsAlias.value() }).value_or(false);
}
}
}
Expand Down
21 changes: 18 additions & 3 deletions src/AppInstallerSharedLib/AppInstallerStrings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -981,16 +981,21 @@ namespace AppInstaller::Utility
return value ? "true"sv : "false"sv;
}

std::optional<bool> TryConvertStringToBool(const std::string_view& input)
template <typename Char>
std::optional<bool> TryConvertStringToBoolT(
const std::basic_string_view<Char>& input,
const std::basic_string_view<Char>& lowerFalse,
const std::basic_string_view<Char>& lowerTrue)
{
try
{
if (CaseInsensitiveEquals(input, "false"sv))
const auto lowerInput = ToLower(input);
if (lowerInput == lowerFalse)
{
return { false };
}

if (CaseInsensitiveEquals(input, "true"sv))
if (lowerInput == lowerTrue)
{
return { true };
}
Expand All @@ -1003,6 +1008,16 @@ namespace AppInstaller::Utility
}
}

std::optional<bool> TryConvertStringToBool(const std::string_view& input)
{
return TryConvertStringToBoolT(input, "false"sv, "true"sv);
}

std::optional<bool> TryConvertStringToBool(const std::wstring_view& input)
{
return TryConvertStringToBoolT(input, L"false"sv, L"true"sv);
}

std::optional<int32_t> TryConvertStringToInt32(const std::string_view& input)
{
int32_t result = 0;
Expand Down
3 changes: 3 additions & 0 deletions src/AppInstallerSharedLib/Public/AppInstallerStrings.h
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,9 @@ namespace AppInstaller::Utility
// Converts the given string view into a bool.
std::optional<bool> TryConvertStringToBool(const std::string_view& value);

// Converts the given wide string view into a bool.
std::optional<bool> TryConvertStringToBool(const std::wstring_view& value);

// Converts the given string view into an int32.
std::optional<int32_t> TryConvertStringToInt32(const std::string_view& value);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ internal class ProcessorSettings : IDisposable
private bool processorPathVerified = false;
private bool disposed = false;

#if !AICLI_DISABLE_TEST_HOOKS
private string? testFoundPath = null;
#endif

private Dictionary<string, ResourceDetails> resourceDetailsDictionary = new ();

/// <summary>
Expand Down Expand Up @@ -136,6 +140,13 @@ public IDSCv3 DSCv3
/// <returns>The full path to the dsc.exe executable, or null if not found.</returns>
public string? GetFoundDscExecutablePath()
{
#if !AICLI_DISABLE_TEST_HOOKS
if (this.testFoundPath != null)
{
return this.testFoundPath;
}
#endif

string? result = this.dscPackageStateMachine.DscExecutablePath;

if (result != null)
Expand Down Expand Up @@ -210,11 +221,27 @@ public ProcessorSettings Clone()

#if !AICLI_DISABLE_TEST_HOOKS
result.dscV3 = this.DSCv3;
result.testFoundPath = this.testFoundPath;
#endif

return result;
}

#if !AICLI_DISABLE_TEST_HOOKS
/// <summary>
/// Injects a found DSC executable path for testing. The hash and alias flag are
/// computed automatically via the normal integrity helper so that the full
/// <c>TryGetValue → bool.ToString()</c> stringification path (including the bug
/// under test) is exercised end-to-end.
/// </summary>
/// <param name="path">The path to inject as the found DSC executable path.</param>
public void SetFoundDscExecutablePathForTest(string path)
{
this.EnsureFoundPathHashCached(path);
this.testFoundPath = path;
}
#endif

/// <summary>
/// Gets a string representation of this object.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ internal sealed partial class DSCv3ConfigurationSetProcessorFactory : Configurat
private const string FoundDscExecutablePathHashPropertyName = "FoundDscExecutablePathHash";
private const string FoundDscExecutablePathIsAliasPropertyName = "FoundDscExecutablePathIsAlias";

#if !AICLI_DISABLE_TEST_HOOKS
private const string TestFoundDscExecutablePathPropertyName = "TestFoundDscExecutablePath";
#endif

private ProcessorSettings processorSettings = new ();

/// <summary>
Expand Down Expand Up @@ -297,6 +301,11 @@ private void SetValue(string name, string value)
case DscExecutablePathIsAliasPropertyName:
this.DscExecutablePathIsAlias = bool.Parse(value);
break;
#if !AICLI_DISABLE_TEST_HOOKS
case TestFoundDscExecutablePathPropertyName:
this.processorSettings.SetFoundDscExecutablePathForTest(value);
break;
#endif
default:
throw new ArgumentOutOfRangeException($"Invalid property name: {name}");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,12 @@ protected ConfigurationProcessorTestBase(UnitTestFixture fixture, ITestOutputHel
/// Create a new <see cref="ConfigurationProcessor"/> with the diagnostics event hooked up.
/// </summary>
/// <param name="factory">The factory to use.</param>
/// <param name="captureVerbose">If true, verbose logs are captured into the log.</param>
/// <returns>The new <see cref="ConfigurationProcessor"/> object.</returns>
internal ConfigurationProcessor CreateConfigurationProcessorWithDiagnostics(IConfigurationSetProcessorFactory? factory = null)
internal ConfigurationProcessor CreateConfigurationProcessorWithDiagnostics(IConfigurationSetProcessorFactory? factory = null, bool captureVerbose = false)
{
ConfigurationProcessor result = this.Fixture.ConfigurationStatics.CreateConfigurationProcessor(factory);
result.Diagnostics += this.EventSink.DiagnosticsHandler;
result.Diagnostics += captureVerbose ? this.EventSink.DiagnosticsHandlerCapturesVerbose : this.EventSink.DiagnosticsHandler;
result.MinimumLevel = DiagnosticLevel.Verbose;
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,14 @@ public class Constants
/// Test guid for forcing units to have a high integrity level during the final routing of unit processor creation.
/// </summary>
public const string ForceHighIntegrityLevelUnitsTestGuid = "f698d20f-3584-4f28-bc75-28037e08e651";

/// <summary>
/// Dictionary key for the test-hook-only "found DSC executable path" property on
/// <c>DSCv3ConfigurationSetProcessorFactory</c>. Setting this key injects a path as the
/// auto-discovered DSC executable, computing its hash and alias flag automatically so that
/// the C++ Lookup → C# TryGetValue → bool.ToString() serialization path is exercised.
/// Only available when AICLI_DISABLE_TEST_HOOKS is not defined.
/// </summary>
public const string TestFoundDscExecutablePathPropertyName = "TestFoundDscExecutablePath";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,20 @@ internal void DiagnosticsHandler(object? sender, IDiagnosticInformation e)
this.log.WriteLine(e.Message);
}
}

/// <summary>
/// Handles diagnostic information from a <see cref="ConfigurationProcessor"/>.
/// </summary>
/// <param name="sender">The object sending the information.</param>
/// <param name="e">The diagnostic information.</param>
internal void DiagnosticsHandlerCapturesVerbose(object? sender, IDiagnosticInformation e)
{
if (e.Message.Contains(TelemetryEvent.Preamble))
{
this.Events.Add(new TelemetryEvent(e.Message));
}

this.log.WriteLine(e.Message);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Management.Configuration.UnitTests.Fixtures;
using Microsoft.Management.Configuration.UnitTests.Helpers;
Expand Down Expand Up @@ -104,5 +106,99 @@ await this.fixture.ConfigurationStatics.CreateConfigurationSetProcessorFactoryAs
ConfigurationProcessor processor = this.CreateConfigurationProcessorWithDiagnostics(dynamicFactory);
Assert.ThrowsAny<Exception>(() => processor.ApplySet(configurationSet, ApplyConfigurationSetFlags.None));
}

/// <summary>
/// Regression test for the case-sensitivity mismatch between C# and C++.
///
/// When the auto-discovered DSC path is an app execution alias (the "Found" code path),
/// the C# factory's <c>TryGetValue("FoundDscExecutablePathIsAlias")</c> used to return
/// <c>"True"</c> (PascalCase from <c>bool.ToString()</c>), while the C++ side in
/// <c>ConfigurationDynamicRuntimeFactory.cpp</c> compared it case-sensitively against
/// <c>L"true"</c>. The mismatch caused <c>processorPathIsAlias</c> to be serialized as
/// <c>false</c> in the JSON sent to the remoting server. The server then called
/// <c>VerifyAndOpen</c> with <c>isAlias:false</c> on an APPEXECLINK reparse point, which
/// fails with Win32 error 1920 (ERROR_CANT_ACCESS_FILE).
///
/// This test exercises the full C++ dynamic factory → C# TryGetValue → JSON
/// serialization → remoting server boundary, with wingetdev.exe as the injected
/// "found" path (an app execution alias that is always registered when the test
/// package is installed). The regression is detected as Win32 error 1920 on apply.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
[Fact]
public async Task Apply_FoundProcessorPath_IsAlias_DoesNotFailWithCantAccessFile()
{
// wingetdev.exe is an app execution alias registered when the test package is deployed.
string wingetdevPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
@"Microsoft\WindowsApps\wingetdev.exe");

Assert.True(File.Exists(wingetdevPath), $"wingetdev.exe not found at '{wingetdevPath}'. The test package must be deployed before running this test.");

// Obtain the factory via the C++ dynamic factory so that SerializeSetProperties,
// which contains the Lookup("FoundDscExecutablePathIsAlias") → TryGetValue →
// bool.ToString() path, is exercised.
IConfigurationSetProcessorFactory dynamicFactory =
await this.fixture.ConfigurationStatics.CreateConfigurationSetProcessorFactoryAsync(
Helpers.Constants.DSCv3DynamicRuntimeHandlerIdentifier);

var factoryMap = (IDictionary<string, string>)dynamicFactory;

// Inject wingetdev as the "found" path. This sets only the path in ProcessorSettings;
// the hash and isAlias flag are computed automatically by EnsureFoundPathHashCached so
// the real bool.ToString() stringification is exercised (not a hardcoded "true" string).
// Deliberately do NOT set "DscExecutablePath" so the C++ takes the usingFoundPath=true
// branch and calls Lookup("FoundDscExecutablePathIsAlias").
factoryMap[Helpers.Constants.TestFoundDscExecutablePathPropertyName] = wingetdevPath;

ConfigurationSet configurationSet = this.ConfigurationSet();
configurationSet.SchemaVersion = "0.3";
configurationSet.Metadata.Add(Helpers.Constants.EnableDynamicFactoryTestMode, true);

ConfigurationUnit unit = this.ConfigurationUnit();
unit.Identifier = "testUnit";
unit.Type = "Microsoft.WinGet.Dev/TestJSON";
unit.Intent = ConfigurationUnitIntent.Unknown;
unit.Environment.Context = SecurityContext.Elevated;
configurationSet.Units = new[] { unit };

ConfigurationProcessor processor = this.CreateConfigurationProcessorWithDiagnostics(dynamicFactory);
ApplyConfigurationSetResult result = processor.ApplySet(configurationSet, ApplyConfigurationSetFlags.None);

Assert.NotNull(result);
Assert.Equal(1, result.UnitResults.Count);

ApplyConfigurationUnitResult unitResult = result.UnitResults[0];
Assert.NotNull(unitResult);

string resultMessage = unitResult.ResultInformation?.ResultCode != null
? $"HResult=0x{unitResult.ResultInformation.ResultCode.HResult:X8}: {unitResult.ResultInformation.Description}"
: "no error";
this.log.WriteLine($"Unit result: {resultMessage}");

// Before the fix: isAlias is incorrectly false, so VerifyAndOpen opens the APPEXECLINK
// with GENERIC_READ which fails with Win32 error 1920, causing VerifyAndOpen to throw
// InvalidOperationException (HResult = 0x80131509).
// After the fix: the alias is opened correctly; any failure is unrelated to alias access
// and will have a different HResult.
const int invalidOperation = unchecked((int)0x80131509);
Assert.NotEqual(invalidOperation, unitResult.ResultInformation?.ResultCode?.HResult ?? 0);
}

/// <summary>
/// Returns all exception messages (including inner exceptions) as a single string.
/// </summary>
/// <param name="ex">The exception chain to inspect.</param>
/// <returns>Concatenated exception messages.</returns>
private static string GetFullExceptionMessage(Exception ex)
{
var sb = new StringBuilder();
for (Exception? current = ex; current != null; current = current.InnerException)
{
sb.AppendLine(current.Message);
}

return sb.ToString();
}
}
}
Loading