From 5cc670beba47010f8dc5c2d206ecdc3a037a1f9c Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Tue, 22 Sep 2026 08:20:12 -0500 Subject: [PATCH 1/7] Normalize nested installer paths Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/ReleaseNotes.md | 1 + .../Workflows/ArchiveFlow.cpp | 3 ++- .../Workflows/FontFlow.cpp | 4 +++- .../Workflows/PortableFlow.cpp | 3 ++- src/AppInstallerCLITests/InstallFlow.cpp | 23 +++++++++++++++++++ 5 files changed, 31 insertions(+), 3 deletions(-) diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md index c6cb18fc5c..0e1aeb3d45 100644 --- a/doc/ReleaseNotes.md +++ b/doc/ReleaseNotes.md @@ -27,3 +27,4 @@ Because the alias is now created as an executable hardlink in the install locati * Updated NUnit to v4 * Fixed a crash (`0x8000ffff`) when using `--disable-interactivity` with the Resume experimental feature enabled during install operations. * Fixed relative path handling for rooted paths. +* Fixed nested installer paths using forward slashes. diff --git a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp index 912b394ee2..e3ecefce28 100644 --- a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp @@ -90,7 +90,8 @@ namespace AppInstaller::CLI::Workflow for (const auto& nestedInstallerFile : installer.NestedInstallerFiles) { - const std::filesystem::path& nestedInstallerPath = targetInstallerPath / ConvertToUTF16(nestedInstallerFile.RelativeFilePath); + std::filesystem::path nestedInstallerPath = targetInstallerPath / ConvertToUTF16(nestedInstallerFile.RelativeFilePath); + nestedInstallerPath.make_preferred(); if (Filesystem::PathEscapesBaseDirectory(nestedInstallerFile.RelativeFilePath)) { diff --git a/src/AppInstallerCLICore/Workflows/FontFlow.cpp b/src/AppInstallerCLICore/Workflows/FontFlow.cpp index 87fb26682b..82aff00165 100644 --- a/src/AppInstallerCLICore/Workflows/FontFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/FontFlow.cpp @@ -223,7 +223,9 @@ namespace AppInstaller::CLI::Workflow const std::vector& nestedInstallerFiles = context.Get()->NestedInstallerFiles; for (const auto& nestedInstallerFile : nestedInstallerFiles) { - fontContext.AddPackageFile(installerPath / ConvertToUTF16(nestedInstallerFile.RelativeFilePath)); + std::filesystem::path relativeFilePath = ConvertToUTF16(nestedInstallerFile.RelativeFilePath); + relativeFilePath.make_preferred(); + fontContext.AddPackageFile(installerPath / relativeFilePath); } } else diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp index 8d0a0ddfd1..af06bc677d 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -228,7 +228,8 @@ namespace AppInstaller::CLI::Workflow EnsurePathIsRelative(context, nestedInstallerFile.PortableCommandAlias, "PortableCommandAlias", ManifestError::PortableCommandAliasEscapesDirectory); AICLI_RETURN_VALUE_IF_TERMINATED(context, {}); - const std::filesystem::path& relativeFilePath = ConvertToUTF16(nestedInstallerFile.RelativeFilePath); + std::filesystem::path relativeFilePath = ConvertToUTF16(nestedInstallerFile.RelativeFilePath); + relativeFilePath.make_preferred(); const std::filesystem::path& targetPath = targetInstallDirectory / relativeFilePath; std::filesystem::path originalFilename = targetPath.filename(); diff --git a/src/AppInstallerCLITests/InstallFlow.cpp b/src/AppInstallerCLITests/InstallFlow.cpp index f5eff1290c..4844cf8523 100644 --- a/src/AppInstallerCLITests/InstallFlow.cpp +++ b/src/AppInstallerCLITests/InstallFlow.cpp @@ -361,6 +361,29 @@ TEST_CASE("InstallFlow_Zip_BadRelativePath", "[InstallFlow][workflow]") REQUIRE(installOutput.str().find(Resource::LocString(expectedMessage).get()) != std::string::npos); } +TEST_CASE("VerifyAndSetNestedInstaller_NormalizesRelativePath", "[InstallFlow][workflow]") +{ + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Zip_Exe.yaml")); + auto installer = manifest.Installers.at(0); + installer.NestedInstallerFiles.at(0).RelativeFilePath = "redist/GameInputRedist.msi"; + + TestCommon::TempDirectory tempDirectory("NestedInstallerPath", false); + const auto archivePath = tempDirectory.GetPath() / "installer.zip"; + const auto nestedInstallerPath = tempDirectory.GetPath() / "extracted" / "redist" / "GameInputRedist.msi"; + // VerifyAndSetNestedInstaller checks that the normalized path exists before setting InstallerPath. + std::filesystem::create_directories(nestedInstallerPath.parent_path()); + std::ofstream(nestedInstallerPath).close(); + + context.Add(std::move(installer)); + context.Add(archivePath); + context << VerifyAndSetNestedInstaller; + + REQUIRE(context.Get() == nestedInstallerPath); +} + TEST_CASE("InstallFlow_Zip_MissingNestedInstaller", "[InstallFlow][workflow]") { TestCommon::TempFile installResultPath("TestExeInstalled.txt"); From 7a2cdd0f42ed6b8da033cee616c8e3553fd29569 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Tue, 22 Sep 2026 08:48:02 -0500 Subject: [PATCH 2/7] Spelling --- .github/actions/spelling/allow.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 7fe36d24d7..b385e360cd 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -273,6 +273,7 @@ QCol Qspectre rclsid REBOOTPROMPT +redist Redistributable REFCLSID removemanifest From 6359fa09619653972f88f122068bc6cad59b141c Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Tue, 29 Sep 2026 17:17:36 -0500 Subject: [PATCH 3/7] Add normalized filesystem path wrapper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Workflows/ArchiveFlow.cpp | 3 +- .../Workflows/FontFlow.cpp | 6 +- .../Workflows/PortableFlow.cpp | 4 +- src/AppInstallerCLITests/Strings.cpp | 14 +++ .../Public/winget/Filesystem.h | 93 +++++++++++++++++++ 5 files changed, 112 insertions(+), 8 deletions(-) diff --git a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp index e3ecefce28..61522e9e43 100644 --- a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp @@ -90,8 +90,7 @@ namespace AppInstaller::CLI::Workflow for (const auto& nestedInstallerFile : installer.NestedInstallerFiles) { - std::filesystem::path nestedInstallerPath = targetInstallerPath / ConvertToUTF16(nestedInstallerFile.RelativeFilePath); - nestedInstallerPath.make_preferred(); + Utility::NormalizedPath nestedInstallerPath{ targetInstallerPath / ConvertToUTF16(nestedInstallerFile.RelativeFilePath) }; if (Filesystem::PathEscapesBaseDirectory(nestedInstallerFile.RelativeFilePath)) { diff --git a/src/AppInstallerCLICore/Workflows/FontFlow.cpp b/src/AppInstallerCLICore/Workflows/FontFlow.cpp index 82aff00165..3a5106e82f 100644 --- a/src/AppInstallerCLICore/Workflows/FontFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/FontFlow.cpp @@ -4,6 +4,7 @@ #include "pch.h" #include "FontFlow.h" #include "TableOutput.h" +#include #include #include using namespace AppInstaller::Utility; @@ -223,9 +224,8 @@ namespace AppInstaller::CLI::Workflow const std::vector& nestedInstallerFiles = context.Get()->NestedInstallerFiles; for (const auto& nestedInstallerFile : nestedInstallerFiles) { - std::filesystem::path relativeFilePath = ConvertToUTF16(nestedInstallerFile.RelativeFilePath); - relativeFilePath.make_preferred(); - fontContext.AddPackageFile(installerPath / relativeFilePath); + Utility::NormalizedPath nestedInstallerPath{ installerPath / ConvertToUTF16(nestedInstallerFile.RelativeFilePath) }; + fontContext.AddPackageFile(nestedInstallerPath); } } else diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp index af06bc677d..84e96168ad 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -228,9 +228,7 @@ namespace AppInstaller::CLI::Workflow EnsurePathIsRelative(context, nestedInstallerFile.PortableCommandAlias, "PortableCommandAlias", ManifestError::PortableCommandAliasEscapesDirectory); AICLI_RETURN_VALUE_IF_TERMINATED(context, {}); - std::filesystem::path relativeFilePath = ConvertToUTF16(nestedInstallerFile.RelativeFilePath); - relativeFilePath.make_preferred(); - const std::filesystem::path& targetPath = targetInstallDirectory / relativeFilePath; + Utility::NormalizedPath targetPath{ targetInstallDirectory / ConvertToUTF16(nestedInstallerFile.RelativeFilePath) }; std::filesystem::path originalFilename = targetPath.filename(); std::filesystem::path commandAlias; diff --git a/src/AppInstallerCLITests/Strings.cpp b/src/AppInstallerCLITests/Strings.cpp index 86f052cb31..856c636b6c 100644 --- a/src/AppInstallerCLITests/Strings.cpp +++ b/src/AppInstallerCLITests/Strings.cpp @@ -5,6 +5,7 @@ #include #include #include +#include using namespace std::string_literals; using namespace std::string_view_literals; @@ -106,6 +107,19 @@ TEST_CASE("NormalizedString", "[strings]") REQUIRE(NormalizedString(input3) == "Test Case"); } +TEST_CASE("NormalizedPath", "[strings]") +{ + NormalizedPath path{ u8"directory/subdirectory/\xC3\xA4.exe" }; + + REQUIRE(path.native() == L"directory\\subdirectory\\\xE4.exe"); + + const std::filesystem::path& filesystemPath = path; + REQUIRE(filesystemPath == std::filesystem::path{ L"directory\\subdirectory\\\xE4.exe" }); + + path = std::string{ "other/path.exe" }; + REQUIRE(path.native() == L"other\\path.exe"); +} + TEST_CASE("Trim", "[strings]") { std::string str; diff --git a/src/AppInstallerSharedLib/Public/winget/Filesystem.h b/src/AppInstallerSharedLib/Public/winget/Filesystem.h index 865881938a..3d3b4445d2 100644 --- a/src/AppInstallerSharedLib/Public/winget/Filesystem.h +++ b/src/AppInstallerSharedLib/Public/winget/Filesystem.h @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once +#include #include #include #include @@ -10,6 +11,98 @@ using namespace std::chrono_literals; +namespace AppInstaller::Utility +{ + // Type to hold a UTF16 path with preferred directory separators. + // This type can be extended with additional path normalization as needed. + struct NormalizedPath : public std::filesystem::path + { + NormalizedPath() = default; + + template + explicit NormalizedPath(const char(&s)[Size]) { AssignValue(std::string_view{ s, (s[Size - 1] == '\0' ? Size - 1 : Size) }); } + + explicit NormalizedPath(std::string_view sv) { AssignValue(sv); } + explicit NormalizedPath(const std::string& s) { AssignValue(std::string_view{ s }); } + + template + explicit NormalizedPath(const wchar_t(&s)[Size]) { AssignValue(std::wstring_view{ s, (s[Size - 1] == L'\0' ? Size - 1 : Size) }); } + + explicit NormalizedPath(std::wstring_view sv) { AssignValue(sv); } + explicit NormalizedPath(const std::wstring& s) { AssignValue(std::wstring_view{ s }); } + + explicit NormalizedPath(const std::filesystem::path& path) { AssignValue(path); } + + NormalizedPath(const NormalizedPath& other) = default; + NormalizedPath& operator=(const NormalizedPath& other) = default; + + NormalizedPath(NormalizedPath&& other) = default; + NormalizedPath& operator=(NormalizedPath&& other) = default; + + template + NormalizedPath& operator=(const char(&s)[Size]) + { + AssignValue(std::string_view{ s, (s[Size - 1] == '\0' ? Size - 1 : Size) }); + return *this; + } + + NormalizedPath& operator=(std::string_view sv) + { + AssignValue(sv); + return *this; + } + + NormalizedPath& operator=(const std::string& s) + { + AssignValue(std::string_view{ s }); + return *this; + } + + template + NormalizedPath& operator=(const wchar_t(&s)[Size]) + { + AssignValue(std::wstring_view{ s, (s[Size - 1] == L'\0' ? Size - 1 : Size) }); + return *this; + } + + NormalizedPath& operator=(std::wstring_view sv) + { + AssignValue(sv); + return *this; + } + + NormalizedPath& operator=(const std::wstring& s) + { + AssignValue(std::wstring_view{ s }); + return *this; + } + + NormalizedPath& operator=(const std::filesystem::path& path) + { + AssignValue(path); + return *this; + } + + private: + void AssignValue(std::string_view sv) + { + AssignValue(std::filesystem::path{ ConvertToUTF16(sv) }); + } + + void AssignValue(std::wstring_view sv) + { + AssignValue(std::filesystem::path{ sv }); + } + + void AssignValue(const std::filesystem::path& path) + { + std::filesystem::path::operator=(path); + make_preferred(); + } + + }; +} + namespace AppInstaller::Filesystem { // Checks if the file system at path supports named streams/ADS From 4922980bbeb44b5180edf663ec2da1ddab9f89cd Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Fri, 9 Oct 2026 17:44:25 -0500 Subject: [PATCH 4/7] Normalize nested installer paths consistently Store nested installer file paths in a wrapper that normalizes separators on assignment and exposes UTF-8/UTF-16 path accessors. Update validation, serialization, workflow consumers, and tests for the wrapped path type. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Workflows/ArchiveFlow.cpp | 6 +- .../Workflows/FontFlow.cpp | 2 +- .../Workflows/PortableFlow.cpp | 4 +- src/AppInstallerCLITests/InstallFlow.cpp | 1 + .../RestInterface_1_10.cpp | 2 +- .../RestInterface_1_12.cpp | 2 +- .../RestInterface_1_28.cpp | 2 +- .../RestInterface_1_4.cpp | 2 +- .../RestInterface_1_5.cpp | 2 +- .../RestInterface_1_6.cpp | 2 +- .../RestInterface_1_7.cpp | 2 +- .../RestInterface_1_9.cpp | 2 +- src/AppInstallerCLITests/Strings.cpp | 11 ++- src/AppInstallerCLITests/YamlManifest.cpp | 16 ++-- .../Manifest/ManifestValidation.cpp | 14 +-- .../Manifest/YamlWriter.cpp | 2 +- .../Public/winget/ManifestCommon.h | 3 +- .../Public/winget/Filesystem.h | 86 +++---------------- 18 files changed, 52 insertions(+), 109 deletions(-) diff --git a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp index 61522e9e43..d7bfb6a796 100644 --- a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp @@ -90,9 +90,9 @@ namespace AppInstaller::CLI::Workflow for (const auto& nestedInstallerFile : installer.NestedInstallerFiles) { - Utility::NormalizedPath nestedInstallerPath{ targetInstallerPath / ConvertToUTF16(nestedInstallerFile.RelativeFilePath) }; - - if (Filesystem::PathEscapesBaseDirectory(nestedInstallerFile.RelativeFilePath)) + std::filesystem::path nestedInstallerPath = targetInstallerPath / nestedInstallerFile.RelativeFilePath.u16string(); + + if (Filesystem::PathEscapesBaseDirectory(nestedInstallerFile.RelativeFilePath.u8string())) { AICLI_LOG(CLI, Error, << "Path points to a location outside of the install directory: " << nestedInstallerPath); context.Reporter.Error() << Resource::String::InvalidPathToNestedInstaller << std::endl; diff --git a/src/AppInstallerCLICore/Workflows/FontFlow.cpp b/src/AppInstallerCLICore/Workflows/FontFlow.cpp index 3a5106e82f..4b829198ee 100644 --- a/src/AppInstallerCLICore/Workflows/FontFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/FontFlow.cpp @@ -224,7 +224,7 @@ namespace AppInstaller::CLI::Workflow const std::vector& nestedInstallerFiles = context.Get()->NestedInstallerFiles; for (const auto& nestedInstallerFile : nestedInstallerFiles) { - Utility::NormalizedPath nestedInstallerPath{ installerPath / ConvertToUTF16(nestedInstallerFile.RelativeFilePath) }; + std::filesystem::path nestedInstallerPath = installerPath / nestedInstallerFile.RelativeFilePath.u16string(); fontContext.AddPackageFile(nestedInstallerPath); } } diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp index 84e96168ad..6b6bedcd45 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -222,13 +222,13 @@ namespace AppInstaller::CLI::Workflow for (const auto& nestedInstallerFile : nestedInstallerFiles) { - EnsurePathIsRelative(context, nestedInstallerFile.RelativeFilePath, "RelativeFilePath", ManifestError::RelativeFilePathEscapesDirectory); + EnsurePathIsRelative(context, nestedInstallerFile.RelativeFilePath.u8string(), "RelativeFilePath", ManifestError::RelativeFilePathEscapesDirectory); AICLI_RETURN_VALUE_IF_TERMINATED(context, {}); EnsurePathIsRelative(context, nestedInstallerFile.PortableCommandAlias, "PortableCommandAlias", ManifestError::PortableCommandAliasEscapesDirectory); AICLI_RETURN_VALUE_IF_TERMINATED(context, {}); - Utility::NormalizedPath targetPath{ targetInstallDirectory / ConvertToUTF16(nestedInstallerFile.RelativeFilePath) }; + std::filesystem::path targetPath = targetInstallDirectory / nestedInstallerFile.RelativeFilePath.u16string(); std::filesystem::path originalFilename = targetPath.filename(); std::filesystem::path commandAlias; diff --git a/src/AppInstallerCLITests/InstallFlow.cpp b/src/AppInstallerCLITests/InstallFlow.cpp index 4844cf8523..317628c8ff 100644 --- a/src/AppInstallerCLITests/InstallFlow.cpp +++ b/src/AppInstallerCLITests/InstallFlow.cpp @@ -369,6 +369,7 @@ TEST_CASE("VerifyAndSetNestedInstaller_NormalizesRelativePath", "[InstallFlow][w auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Zip_Exe.yaml")); auto installer = manifest.Installers.at(0); installer.NestedInstallerFiles.at(0).RelativeFilePath = "redist/GameInputRedist.msi"; + REQUIRE(installer.NestedInstallerFiles.at(0).RelativeFilePath.u8string() == "redist\\GameInputRedist.msi"); TestCommon::TempDirectory tempDirectory("NestedInstallerPath", false); const auto archivePath = tempDirectory.GetPath() / "installer.zip"; diff --git a/src/AppInstallerCLITests/RestInterface_1_10.cpp b/src/AppInstallerCLITests/RestInterface_1_10.cpp index 8c002dcc31..341d4b348e 100644 --- a/src/AppInstallerCLITests/RestInterface_1_10.cpp +++ b/src/AppInstallerCLITests/RestInterface_1_10.cpp @@ -370,7 +370,7 @@ namespace REQUIRE(actualInstaller.UnsupportedArguments.size() == 1); REQUIRE(actualInstaller.UnsupportedArguments.at(0) == UnsupportedArgumentEnum::Log); REQUIRE(actualInstaller.NestedInstallerFiles.size() == 1); - REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath == "test\\app.exe"); + REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath.u8string() == "test\\app.exe"); REQUIRE(actualInstaller.NestedInstallerFiles.at(0).PortableCommandAlias == "test.exe"); REQUIRE(actualInstaller.InstallationMetadata.DefaultInstallLocation == "%TEMP%\\DefaultInstallLocation"); REQUIRE(actualInstaller.InstallationMetadata.Files.size() == 1); diff --git a/src/AppInstallerCLITests/RestInterface_1_12.cpp b/src/AppInstallerCLITests/RestInterface_1_12.cpp index fb55136924..7ec1b501ef 100644 --- a/src/AppInstallerCLITests/RestInterface_1_12.cpp +++ b/src/AppInstallerCLITests/RestInterface_1_12.cpp @@ -370,7 +370,7 @@ namespace REQUIRE(actualInstaller.UnsupportedArguments.size() == 1); REQUIRE(actualInstaller.UnsupportedArguments.at(0) == UnsupportedArgumentEnum::Log); REQUIRE(actualInstaller.NestedInstallerFiles.size() == 1); - REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath == "test\\app.exe"); + REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath.u8string() == "test\\app.exe"); REQUIRE(actualInstaller.NestedInstallerFiles.at(0).PortableCommandAlias == "test.exe"); REQUIRE(actualInstaller.InstallationMetadata.DefaultInstallLocation == "%TEMP%\\DefaultInstallLocation"); REQUIRE(actualInstaller.InstallationMetadata.Files.size() == 1); diff --git a/src/AppInstallerCLITests/RestInterface_1_28.cpp b/src/AppInstallerCLITests/RestInterface_1_28.cpp index 4286430f00..54d3bf019f 100644 --- a/src/AppInstallerCLITests/RestInterface_1_28.cpp +++ b/src/AppInstallerCLITests/RestInterface_1_28.cpp @@ -384,7 +384,7 @@ namespace REQUIRE(actualInstaller.UnsupportedArguments.size() == 1); REQUIRE(actualInstaller.UnsupportedArguments.at(0) == UnsupportedArgumentEnum::Log); REQUIRE(actualInstaller.NestedInstallerFiles.size() == 1); - REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath == "test\\app.exe"); + REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath.u8string() == "test\\app.exe"); REQUIRE(actualInstaller.NestedInstallerFiles.at(0).PortableCommandAlias == "test.exe"); REQUIRE(actualInstaller.InstallationMetadata.DefaultInstallLocation == "%TEMP%\\DefaultInstallLocation"); REQUIRE(actualInstaller.InstallationMetadata.Files.size() == 1); diff --git a/src/AppInstallerCLITests/RestInterface_1_4.cpp b/src/AppInstallerCLITests/RestInterface_1_4.cpp index fe5f2ecca0..2290e0bee7 100644 --- a/src/AppInstallerCLITests/RestInterface_1_4.cpp +++ b/src/AppInstallerCLITests/RestInterface_1_4.cpp @@ -344,7 +344,7 @@ namespace REQUIRE(actualInstaller.UnsupportedArguments.size() == 1); REQUIRE(actualInstaller.UnsupportedArguments.at(0) == UnsupportedArgumentEnum::Log); REQUIRE(actualInstaller.NestedInstallerFiles.size() == 1); - REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath == "test\\app.exe"); + REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath.u8string() == "test\\app.exe"); REQUIRE(actualInstaller.NestedInstallerFiles.at(0).PortableCommandAlias == "test.exe"); REQUIRE(actualInstaller.InstallationMetadata.DefaultInstallLocation == "%TEMP%\\DefaultInstallLocation"); REQUIRE(actualInstaller.InstallationMetadata.Files.size() == 1); diff --git a/src/AppInstallerCLITests/RestInterface_1_5.cpp b/src/AppInstallerCLITests/RestInterface_1_5.cpp index f8207dd20f..965c622fa6 100644 --- a/src/AppInstallerCLITests/RestInterface_1_5.cpp +++ b/src/AppInstallerCLITests/RestInterface_1_5.cpp @@ -370,7 +370,7 @@ namespace REQUIRE(actualInstaller.UnsupportedArguments.size() == 1); REQUIRE(actualInstaller.UnsupportedArguments.at(0) == UnsupportedArgumentEnum::Log); REQUIRE(actualInstaller.NestedInstallerFiles.size() == 1); - REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath == "test\\app.exe"); + REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath.u8string() == "test\\app.exe"); REQUIRE(actualInstaller.NestedInstallerFiles.at(0).PortableCommandAlias == "test.exe"); REQUIRE(actualInstaller.InstallationMetadata.DefaultInstallLocation == "%TEMP%\\DefaultInstallLocation"); REQUIRE(actualInstaller.InstallationMetadata.Files.size() == 1); diff --git a/src/AppInstallerCLITests/RestInterface_1_6.cpp b/src/AppInstallerCLITests/RestInterface_1_6.cpp index 17540a91e0..4cb7979b10 100644 --- a/src/AppInstallerCLITests/RestInterface_1_6.cpp +++ b/src/AppInstallerCLITests/RestInterface_1_6.cpp @@ -371,7 +371,7 @@ namespace REQUIRE(actualInstaller.UnsupportedArguments.size() == 1); REQUIRE(actualInstaller.UnsupportedArguments.at(0) == UnsupportedArgumentEnum::Log); REQUIRE(actualInstaller.NestedInstallerFiles.size() == 1); - REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath == "test\\app.exe"); + REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath.u8string() == "test\\app.exe"); REQUIRE(actualInstaller.NestedInstallerFiles.at(0).PortableCommandAlias == "test.exe"); REQUIRE(actualInstaller.InstallationMetadata.DefaultInstallLocation == "%TEMP%\\DefaultInstallLocation"); REQUIRE(actualInstaller.InstallationMetadata.Files.size() == 1); diff --git a/src/AppInstallerCLITests/RestInterface_1_7.cpp b/src/AppInstallerCLITests/RestInterface_1_7.cpp index 1d9da5af9a..457cabee26 100644 --- a/src/AppInstallerCLITests/RestInterface_1_7.cpp +++ b/src/AppInstallerCLITests/RestInterface_1_7.cpp @@ -425,7 +425,7 @@ namespace REQUIRE(actualInstaller.UnsupportedArguments.size() == 1); REQUIRE(actualInstaller.UnsupportedArguments.at(0) == UnsupportedArgumentEnum::Log); REQUIRE(actualInstaller.NestedInstallerFiles.size() == 1); - REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath == "test\\app.exe"); + REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath.u8string() == "test\\app.exe"); REQUIRE(actualInstaller.NestedInstallerFiles.at(0).PortableCommandAlias == "test.exe"); REQUIRE(actualInstaller.InstallationMetadata.DefaultInstallLocation == "%TEMP%\\DefaultInstallLocation"); REQUIRE(actualInstaller.InstallationMetadata.Files.size() == 1); diff --git a/src/AppInstallerCLITests/RestInterface_1_9.cpp b/src/AppInstallerCLITests/RestInterface_1_9.cpp index e9763ae2ba..0c516bd8b6 100644 --- a/src/AppInstallerCLITests/RestInterface_1_9.cpp +++ b/src/AppInstallerCLITests/RestInterface_1_9.cpp @@ -363,7 +363,7 @@ namespace REQUIRE(actualInstaller.UnsupportedArguments.size() == 1); REQUIRE(actualInstaller.UnsupportedArguments.at(0) == UnsupportedArgumentEnum::Log); REQUIRE(actualInstaller.NestedInstallerFiles.size() == 1); - REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath == "test\\app.exe"); + REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath.u8string() == "test\\app.exe"); REQUIRE(actualInstaller.NestedInstallerFiles.at(0).PortableCommandAlias == "test.exe"); REQUIRE(actualInstaller.InstallationMetadata.DefaultInstallLocation == "%TEMP%\\DefaultInstallLocation"); REQUIRE(actualInstaller.InstallationMetadata.Files.size() == 1); diff --git a/src/AppInstallerCLITests/Strings.cpp b/src/AppInstallerCLITests/Strings.cpp index 0c08957f76..d22e4ff785 100644 --- a/src/AppInstallerCLITests/Strings.cpp +++ b/src/AppInstallerCLITests/Strings.cpp @@ -111,13 +111,16 @@ TEST_CASE("NormalizedPath", "[strings]") { NormalizedPath path{ u8"directory/subdirectory/\xC3\xA4.exe" }; - REQUIRE(path.native() == L"directory\\subdirectory\\\xE4.exe"); + REQUIRE(path.u8string() == u8"directory\\subdirectory\\\xC3\xA4.exe"); + REQUIRE(path.u16string() == u"directory\\subdirectory\\\u00E4.exe"); - const std::filesystem::path& filesystemPath = path; - REQUIRE(filesystemPath == std::filesystem::path{ L"directory\\subdirectory\\\xE4.exe" }); + path = u8"directory/subdirectory/\U0001F600.exe"; + REQUIRE(path.u8string() == u8"directory\\subdirectory\\\U0001F600.exe"); + REQUIRE(path.u16string() == u"directory\\subdirectory\\\U0001F600.exe"); path = std::string{ "other/path.exe" }; - REQUIRE(path.native() == L"other\\path.exe"); + REQUIRE(path.u8string() == "other\\path.exe"); + REQUIRE(path.u16string() == u"other\\path.exe"); } TEST_CASE("Trim", "[strings]") diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp index f893881f83..5cbf6f9c52 100644 --- a/src/AppInstallerCLITests/YamlManifest.cpp +++ b/src/AppInstallerCLITests/YamlManifest.cpp @@ -299,7 +299,7 @@ namespace { REQUIRE(manifest.DefaultInstallerInfo.NestedInstallerType == InstallerTypeEnum::Msi); REQUIRE(manifest.DefaultInstallerInfo.NestedInstallerFiles.size() == 1); - REQUIRE(manifest.DefaultInstallerInfo.NestedInstallerFiles.at(0).RelativeFilePath == "RelativeFilePath"); + REQUIRE(manifest.DefaultInstallerInfo.NestedInstallerFiles.at(0).RelativeFilePath.u8string() == "RelativeFilePath"); REQUIRE(manifest.DefaultInstallerInfo.NestedInstallerFiles.at(0).PortableCommandAlias == "PortableCommandAlias"); REQUIRE(manifest.DefaultInstallerInfo.InstallationMetadata.DefaultInstallLocation == "%ProgramFiles%\\TestApp"); REQUIRE(manifest.DefaultInstallerInfo.InstallationMetadata.Files.size() == 1); @@ -517,9 +517,9 @@ namespace REQUIRE(installer4.ProductCode == "{Foo}"); REQUIRE(installer4.NestedInstallerType == InstallerTypeEnum::Portable); REQUIRE(installer4.NestedInstallerFiles.size() == 2); - REQUIRE(installer4.NestedInstallerFiles.at(0).RelativeFilePath == "relativeFilePath1"); + REQUIRE(installer4.NestedInstallerFiles.at(0).RelativeFilePath.u8string() == "relativeFilePath1"); REQUIRE(installer4.NestedInstallerFiles.at(0).PortableCommandAlias == "portableAlias1"); - REQUIRE(installer4.NestedInstallerFiles.at(1).RelativeFilePath == "relativeFilePath2"); + REQUIRE(installer4.NestedInstallerFiles.at(1).RelativeFilePath.u8string() == "relativeFilePath2"); REQUIRE(installer4.NestedInstallerFiles.at(1).PortableCommandAlias == "portableAlias2"); REQUIRE(installer4.InstallationMetadata.DefaultInstallLocation == "%ProgramFiles%\\TestApp2"); REQUIRE(installer4.InstallationMetadata.Files.size() == 1); @@ -566,11 +566,11 @@ namespace REQUIRE(installer6.Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); REQUIRE(installer6.NestedInstallerType == InstallerTypeEnum::Font); REQUIRE(installer6.NestedInstallerFiles.size() == 5); - REQUIRE(installer6.NestedInstallerFiles.at(0).RelativeFilePath == "relativeFilePath1.otf"); - REQUIRE(installer6.NestedInstallerFiles.at(1).RelativeFilePath == "relativeFilePath2.ttf"); - REQUIRE(installer6.NestedInstallerFiles.at(2).RelativeFilePath == "relativeFilePath3.fnt"); - REQUIRE(installer6.NestedInstallerFiles.at(3).RelativeFilePath == "relativeFilePath4.ttc"); - REQUIRE(installer6.NestedInstallerFiles.at(4).RelativeFilePath == "relativeFilePath5.otc"); + REQUIRE(installer6.NestedInstallerFiles.at(0).RelativeFilePath.u8string() == "relativeFilePath1.otf"); + REQUIRE(installer6.NestedInstallerFiles.at(1).RelativeFilePath.u8string() == "relativeFilePath2.ttf"); + REQUIRE(installer6.NestedInstallerFiles.at(2).RelativeFilePath.u8string() == "relativeFilePath3.fnt"); + REQUIRE(installer6.NestedInstallerFiles.at(3).RelativeFilePath.u8string() == "relativeFilePath4.ttc"); + REQUIRE(installer6.NestedInstallerFiles.at(4).RelativeFilePath.u8string() == "relativeFilePath5.otc"); const ManifestInstaller& installer7 = manifest.Installers.at(6); REQUIRE(installer7.BaseInstallerType == InstallerTypeEnum::Font); diff --git a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp index 71f946871a..92f0132055 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp @@ -455,20 +455,23 @@ namespace AppInstaller::Manifest for (const auto& nestedInstallerFile : installer.NestedInstallerFiles) { - if (nestedInstallerFile.RelativeFilePath.empty()) + const std::filesystem::path relativeFilePath{ nestedInstallerFile.RelativeFilePath.u16string() }; + const std::string relativeFilePathUtf8 = nestedInstallerFile.RelativeFilePath.u8string(); + + if (relativeFilePath.empty()) { resultErrors.emplace_back(ManifestError::RequiredFieldMissing, "RelativeFilePath"); break; } // Check that the relative file path does not escape base directory. - if (AppInstaller::Filesystem::PathEscapesBaseDirectory(nestedInstallerFile.RelativeFilePath)) + if (AppInstaller::Filesystem::PathEscapesBaseDirectory(relativeFilePathUtf8)) { resultErrors.emplace_back(ManifestError::RelativeFilePathEscapesDirectory, "RelativeFilePath"); } // Check for duplicate relative filepath values. - if (!relativeFilePathSet.insert(Utility::ToLower(nestedInstallerFile.RelativeFilePath)).second) + if (!relativeFilePathSet.insert(Utility::ToLower(relativeFilePathUtf8)).second) { resultErrors.emplace_back(ManifestError::DuplicateRelativeFilePath, "RelativeFilePath"); } @@ -493,7 +496,6 @@ namespace AppInstaller::Manifest // If running full validation, check filetype if (options.FullValidation) { - std::filesystem::path relativeFilePath{ Utility::ConvertToUTF16(nestedInstallerFile.RelativeFilePath) }; if (relativeFilePath.has_extension()) { const std::wstring lowerExtension = Utility::ToLower(relativeFilePath.extension().wstring()); @@ -502,7 +504,7 @@ namespace AppInstaller::Manifest { if (std::find(s_AllowedPortableFiletypes.begin(), s_AllowedPortableFiletypes.end(), lowerExtension) == s_AllowedPortableFiletypes.end()) { - resultErrors.emplace_back(ManifestError::InvalidPortableFiletype, "RelativeFilePath", nestedInstallerFile.RelativeFilePath); + resultErrors.emplace_back(ManifestError::InvalidPortableFiletype, "RelativeFilePath", relativeFilePathUtf8); } } @@ -510,7 +512,7 @@ namespace AppInstaller::Manifest { if (std::find(s_AllowedFontFiletypes.begin(), s_AllowedFontFiletypes.end(), lowerExtension) == s_AllowedFontFiletypes.end()) { - resultErrors.emplace_back(ManifestError::InvalidFontFiletype, "RelativeFilePath", nestedInstallerFile.RelativeFilePath); + resultErrors.emplace_back(ManifestError::InvalidFontFiletype, "RelativeFilePath", relativeFilePathUtf8); } } } diff --git a/src/AppInstallerCommonCore/Manifest/YamlWriter.cpp b/src/AppInstallerCommonCore/Manifest/YamlWriter.cpp index eb12738e01..f164042d5f 100644 --- a/src/AppInstallerCommonCore/Manifest/YamlWriter.cpp +++ b/src/AppInstallerCommonCore/Manifest/YamlWriter.cpp @@ -465,7 +465,7 @@ namespace AppInstaller::Manifest::YamlWriter for (const auto& nestedInstallerFile : nestedInstallerFiles) { out << YAML::BeginMap; - WRITE_PROPERTY(out, NestedInstallerFileRelativeFilePath, nestedInstallerFile.RelativeFilePath); + WRITE_PROPERTY(out, NestedInstallerFileRelativeFilePath, nestedInstallerFile.RelativeFilePath.u8string()); WRITE_PROPERTY_IF_EXISTS(out, PortableCommandAlias, nestedInstallerFile.PortableCommandAlias); out << YAML::EndMap; } diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h index c484da70fd..acd9ebb4b7 100644 --- a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h +++ b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h @@ -3,6 +3,7 @@ #pragma once #include #include +#include #include #include #include @@ -359,7 +360,7 @@ namespace AppInstaller::Manifest struct NestedInstallerFile { - string_t RelativeFilePath; + Utility::NormalizedPath RelativeFilePath; string_t PortableCommandAlias; }; diff --git a/src/AppInstallerSharedLib/Public/winget/Filesystem.h b/src/AppInstallerSharedLib/Public/winget/Filesystem.h index 3d3b4445d2..783afa161d 100644 --- a/src/AppInstallerSharedLib/Public/winget/Filesystem.h +++ b/src/AppInstallerSharedLib/Public/winget/Filesystem.h @@ -14,92 +14,28 @@ using namespace std::chrono_literals; namespace AppInstaller::Utility { // Type to hold a UTF16 path with preferred directory separators. - // This type can be extended with additional path normalization as needed. - struct NormalizedPath : public std::filesystem::path + struct NormalizedPath { NormalizedPath() = default; - template - explicit NormalizedPath(const char(&s)[Size]) { AssignValue(std::string_view{ s, (s[Size - 1] == '\0' ? Size - 1 : Size) }); } + NormalizedPath(const char* value) : NormalizedPath(std::string_view{ value }) {} + NormalizedPath(std::string_view value) { *this = value; } - explicit NormalizedPath(std::string_view sv) { AssignValue(sv); } - explicit NormalizedPath(const std::string& s) { AssignValue(std::string_view{ s }); } + NormalizedPath& operator=(const char* value) { return *this = std::string_view{ value }; } - template - explicit NormalizedPath(const wchar_t(&s)[Size]) { AssignValue(std::wstring_view{ s, (s[Size - 1] == L'\0' ? Size - 1 : Size) }); } - - explicit NormalizedPath(std::wstring_view sv) { AssignValue(sv); } - explicit NormalizedPath(const std::wstring& s) { AssignValue(std::wstring_view{ s }); } - - explicit NormalizedPath(const std::filesystem::path& path) { AssignValue(path); } - - NormalizedPath(const NormalizedPath& other) = default; - NormalizedPath& operator=(const NormalizedPath& other) = default; - - NormalizedPath(NormalizedPath&& other) = default; - NormalizedPath& operator=(NormalizedPath&& other) = default; - - template - NormalizedPath& operator=(const char(&s)[Size]) + NormalizedPath& operator=(std::string_view value) { - AssignValue(std::string_view{ s, (s[Size - 1] == '\0' ? Size - 1 : Size) }); + m_path = AppInstaller::Utility::ConvertToUTF16(value); + m_path.make_preferred(); return *this; } - NormalizedPath& operator=(std::string_view sv) - { - AssignValue(sv); - return *this; - } - - NormalizedPath& operator=(const std::string& s) - { - AssignValue(std::string_view{ s }); - return *this; - } - - template - NormalizedPath& operator=(const wchar_t(&s)[Size]) - { - AssignValue(std::wstring_view{ s, (s[Size - 1] == L'\0' ? Size - 1 : Size) }); - return *this; - } - - NormalizedPath& operator=(std::wstring_view sv) - { - AssignValue(sv); - return *this; - } - - NormalizedPath& operator=(const std::wstring& s) - { - AssignValue(std::wstring_view{ s }); - return *this; - } - - NormalizedPath& operator=(const std::filesystem::path& path) - { - AssignValue(path); - return *this; - } + std::string u8string() const { return m_path.u8string(); } + std::u16string u16string() const { return m_path.u16string(); } + const std::filesystem::path& get() const { return m_path; } private: - void AssignValue(std::string_view sv) - { - AssignValue(std::filesystem::path{ ConvertToUTF16(sv) }); - } - - void AssignValue(std::wstring_view sv) - { - AssignValue(std::filesystem::path{ sv }); - } - - void AssignValue(const std::filesystem::path& path) - { - std::filesystem::path::operator=(path); - make_preferred(); - } - + std::filesystem::path m_path; }; } From 4906d334e40b9803f652c74460c7627398deafb1 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Sun, 11 Oct 2026 20:49:53 -0500 Subject: [PATCH 5/7] Simplify normalized path string accessors Rename normalized path accessors to string and wstring and update their consumers. Correct Unicode test literals and cover overlong UTF-8 separator sequences. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Workflows/ArchiveFlow.cpp | 4 ++-- .../Workflows/FontFlow.cpp | 2 +- .../Workflows/PortableFlow.cpp | 4 ++-- src/AppInstallerCLITests/InstallFlow.cpp | 2 +- .../RestInterface_1_10.cpp | 2 +- .../RestInterface_1_12.cpp | 2 +- .../RestInterface_1_28.cpp | 2 +- .../RestInterface_1_4.cpp | 2 +- .../RestInterface_1_5.cpp | 2 +- .../RestInterface_1_6.cpp | 2 +- .../RestInterface_1_7.cpp | 2 +- .../RestInterface_1_9.cpp | 2 +- src/AppInstallerCLITests/Strings.cpp | 19 ++++++++++++------- src/AppInstallerCLITests/YamlManifest.cpp | 16 ++++++++-------- .../Manifest/ManifestValidation.cpp | 4 ++-- .../Manifest/YamlWriter.cpp | 2 +- .../Public/winget/Filesystem.h | 6 +++--- 17 files changed, 40 insertions(+), 35 deletions(-) diff --git a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp index d7bfb6a796..e0228f2de2 100644 --- a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp @@ -90,9 +90,9 @@ namespace AppInstaller::CLI::Workflow for (const auto& nestedInstallerFile : installer.NestedInstallerFiles) { - std::filesystem::path nestedInstallerPath = targetInstallerPath / nestedInstallerFile.RelativeFilePath.u16string(); + std::filesystem::path nestedInstallerPath = targetInstallerPath / nestedInstallerFile.RelativeFilePath.wstring(); - if (Filesystem::PathEscapesBaseDirectory(nestedInstallerFile.RelativeFilePath.u8string())) + if (Filesystem::PathEscapesBaseDirectory(nestedInstallerFile.RelativeFilePath.string())) { AICLI_LOG(CLI, Error, << "Path points to a location outside of the install directory: " << nestedInstallerPath); context.Reporter.Error() << Resource::String::InvalidPathToNestedInstaller << std::endl; diff --git a/src/AppInstallerCLICore/Workflows/FontFlow.cpp b/src/AppInstallerCLICore/Workflows/FontFlow.cpp index 4b829198ee..237c840f38 100644 --- a/src/AppInstallerCLICore/Workflows/FontFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/FontFlow.cpp @@ -224,7 +224,7 @@ namespace AppInstaller::CLI::Workflow const std::vector& nestedInstallerFiles = context.Get()->NestedInstallerFiles; for (const auto& nestedInstallerFile : nestedInstallerFiles) { - std::filesystem::path nestedInstallerPath = installerPath / nestedInstallerFile.RelativeFilePath.u16string(); + std::filesystem::path nestedInstallerPath = installerPath / nestedInstallerFile.RelativeFilePath.wstring(); fontContext.AddPackageFile(nestedInstallerPath); } } diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp index 6b6bedcd45..b7437ad0b1 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -222,13 +222,13 @@ namespace AppInstaller::CLI::Workflow for (const auto& nestedInstallerFile : nestedInstallerFiles) { - EnsurePathIsRelative(context, nestedInstallerFile.RelativeFilePath.u8string(), "RelativeFilePath", ManifestError::RelativeFilePathEscapesDirectory); + EnsurePathIsRelative(context, nestedInstallerFile.RelativeFilePath.string(), "RelativeFilePath", ManifestError::RelativeFilePathEscapesDirectory); AICLI_RETURN_VALUE_IF_TERMINATED(context, {}); EnsurePathIsRelative(context, nestedInstallerFile.PortableCommandAlias, "PortableCommandAlias", ManifestError::PortableCommandAliasEscapesDirectory); AICLI_RETURN_VALUE_IF_TERMINATED(context, {}); - std::filesystem::path targetPath = targetInstallDirectory / nestedInstallerFile.RelativeFilePath.u16string(); + std::filesystem::path targetPath = targetInstallDirectory / nestedInstallerFile.RelativeFilePath.wstring(); std::filesystem::path originalFilename = targetPath.filename(); std::filesystem::path commandAlias; diff --git a/src/AppInstallerCLITests/InstallFlow.cpp b/src/AppInstallerCLITests/InstallFlow.cpp index 317628c8ff..3d50ddb056 100644 --- a/src/AppInstallerCLITests/InstallFlow.cpp +++ b/src/AppInstallerCLITests/InstallFlow.cpp @@ -369,7 +369,7 @@ TEST_CASE("VerifyAndSetNestedInstaller_NormalizesRelativePath", "[InstallFlow][w auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Zip_Exe.yaml")); auto installer = manifest.Installers.at(0); installer.NestedInstallerFiles.at(0).RelativeFilePath = "redist/GameInputRedist.msi"; - REQUIRE(installer.NestedInstallerFiles.at(0).RelativeFilePath.u8string() == "redist\\GameInputRedist.msi"); + REQUIRE(installer.NestedInstallerFiles.at(0).RelativeFilePath.string() == "redist\\GameInputRedist.msi"); TestCommon::TempDirectory tempDirectory("NestedInstallerPath", false); const auto archivePath = tempDirectory.GetPath() / "installer.zip"; diff --git a/src/AppInstallerCLITests/RestInterface_1_10.cpp b/src/AppInstallerCLITests/RestInterface_1_10.cpp index 341d4b348e..6967746176 100644 --- a/src/AppInstallerCLITests/RestInterface_1_10.cpp +++ b/src/AppInstallerCLITests/RestInterface_1_10.cpp @@ -370,7 +370,7 @@ namespace REQUIRE(actualInstaller.UnsupportedArguments.size() == 1); REQUIRE(actualInstaller.UnsupportedArguments.at(0) == UnsupportedArgumentEnum::Log); REQUIRE(actualInstaller.NestedInstallerFiles.size() == 1); - REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath.u8string() == "test\\app.exe"); + REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath.string() == "test\\app.exe"); REQUIRE(actualInstaller.NestedInstallerFiles.at(0).PortableCommandAlias == "test.exe"); REQUIRE(actualInstaller.InstallationMetadata.DefaultInstallLocation == "%TEMP%\\DefaultInstallLocation"); REQUIRE(actualInstaller.InstallationMetadata.Files.size() == 1); diff --git a/src/AppInstallerCLITests/RestInterface_1_12.cpp b/src/AppInstallerCLITests/RestInterface_1_12.cpp index 7ec1b501ef..bd709c3fa5 100644 --- a/src/AppInstallerCLITests/RestInterface_1_12.cpp +++ b/src/AppInstallerCLITests/RestInterface_1_12.cpp @@ -370,7 +370,7 @@ namespace REQUIRE(actualInstaller.UnsupportedArguments.size() == 1); REQUIRE(actualInstaller.UnsupportedArguments.at(0) == UnsupportedArgumentEnum::Log); REQUIRE(actualInstaller.NestedInstallerFiles.size() == 1); - REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath.u8string() == "test\\app.exe"); + REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath.string() == "test\\app.exe"); REQUIRE(actualInstaller.NestedInstallerFiles.at(0).PortableCommandAlias == "test.exe"); REQUIRE(actualInstaller.InstallationMetadata.DefaultInstallLocation == "%TEMP%\\DefaultInstallLocation"); REQUIRE(actualInstaller.InstallationMetadata.Files.size() == 1); diff --git a/src/AppInstallerCLITests/RestInterface_1_28.cpp b/src/AppInstallerCLITests/RestInterface_1_28.cpp index 54d3bf019f..b048cebb39 100644 --- a/src/AppInstallerCLITests/RestInterface_1_28.cpp +++ b/src/AppInstallerCLITests/RestInterface_1_28.cpp @@ -384,7 +384,7 @@ namespace REQUIRE(actualInstaller.UnsupportedArguments.size() == 1); REQUIRE(actualInstaller.UnsupportedArguments.at(0) == UnsupportedArgumentEnum::Log); REQUIRE(actualInstaller.NestedInstallerFiles.size() == 1); - REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath.u8string() == "test\\app.exe"); + REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath.string() == "test\\app.exe"); REQUIRE(actualInstaller.NestedInstallerFiles.at(0).PortableCommandAlias == "test.exe"); REQUIRE(actualInstaller.InstallationMetadata.DefaultInstallLocation == "%TEMP%\\DefaultInstallLocation"); REQUIRE(actualInstaller.InstallationMetadata.Files.size() == 1); diff --git a/src/AppInstallerCLITests/RestInterface_1_4.cpp b/src/AppInstallerCLITests/RestInterface_1_4.cpp index 2290e0bee7..067f35e7d2 100644 --- a/src/AppInstallerCLITests/RestInterface_1_4.cpp +++ b/src/AppInstallerCLITests/RestInterface_1_4.cpp @@ -344,7 +344,7 @@ namespace REQUIRE(actualInstaller.UnsupportedArguments.size() == 1); REQUIRE(actualInstaller.UnsupportedArguments.at(0) == UnsupportedArgumentEnum::Log); REQUIRE(actualInstaller.NestedInstallerFiles.size() == 1); - REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath.u8string() == "test\\app.exe"); + REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath.string() == "test\\app.exe"); REQUIRE(actualInstaller.NestedInstallerFiles.at(0).PortableCommandAlias == "test.exe"); REQUIRE(actualInstaller.InstallationMetadata.DefaultInstallLocation == "%TEMP%\\DefaultInstallLocation"); REQUIRE(actualInstaller.InstallationMetadata.Files.size() == 1); diff --git a/src/AppInstallerCLITests/RestInterface_1_5.cpp b/src/AppInstallerCLITests/RestInterface_1_5.cpp index 965c622fa6..5864e80e58 100644 --- a/src/AppInstallerCLITests/RestInterface_1_5.cpp +++ b/src/AppInstallerCLITests/RestInterface_1_5.cpp @@ -370,7 +370,7 @@ namespace REQUIRE(actualInstaller.UnsupportedArguments.size() == 1); REQUIRE(actualInstaller.UnsupportedArguments.at(0) == UnsupportedArgumentEnum::Log); REQUIRE(actualInstaller.NestedInstallerFiles.size() == 1); - REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath.u8string() == "test\\app.exe"); + REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath.string() == "test\\app.exe"); REQUIRE(actualInstaller.NestedInstallerFiles.at(0).PortableCommandAlias == "test.exe"); REQUIRE(actualInstaller.InstallationMetadata.DefaultInstallLocation == "%TEMP%\\DefaultInstallLocation"); REQUIRE(actualInstaller.InstallationMetadata.Files.size() == 1); diff --git a/src/AppInstallerCLITests/RestInterface_1_6.cpp b/src/AppInstallerCLITests/RestInterface_1_6.cpp index 4cb7979b10..f61dc0161b 100644 --- a/src/AppInstallerCLITests/RestInterface_1_6.cpp +++ b/src/AppInstallerCLITests/RestInterface_1_6.cpp @@ -371,7 +371,7 @@ namespace REQUIRE(actualInstaller.UnsupportedArguments.size() == 1); REQUIRE(actualInstaller.UnsupportedArguments.at(0) == UnsupportedArgumentEnum::Log); REQUIRE(actualInstaller.NestedInstallerFiles.size() == 1); - REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath.u8string() == "test\\app.exe"); + REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath.string() == "test\\app.exe"); REQUIRE(actualInstaller.NestedInstallerFiles.at(0).PortableCommandAlias == "test.exe"); REQUIRE(actualInstaller.InstallationMetadata.DefaultInstallLocation == "%TEMP%\\DefaultInstallLocation"); REQUIRE(actualInstaller.InstallationMetadata.Files.size() == 1); diff --git a/src/AppInstallerCLITests/RestInterface_1_7.cpp b/src/AppInstallerCLITests/RestInterface_1_7.cpp index 457cabee26..b968f7617e 100644 --- a/src/AppInstallerCLITests/RestInterface_1_7.cpp +++ b/src/AppInstallerCLITests/RestInterface_1_7.cpp @@ -425,7 +425,7 @@ namespace REQUIRE(actualInstaller.UnsupportedArguments.size() == 1); REQUIRE(actualInstaller.UnsupportedArguments.at(0) == UnsupportedArgumentEnum::Log); REQUIRE(actualInstaller.NestedInstallerFiles.size() == 1); - REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath.u8string() == "test\\app.exe"); + REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath.string() == "test\\app.exe"); REQUIRE(actualInstaller.NestedInstallerFiles.at(0).PortableCommandAlias == "test.exe"); REQUIRE(actualInstaller.InstallationMetadata.DefaultInstallLocation == "%TEMP%\\DefaultInstallLocation"); REQUIRE(actualInstaller.InstallationMetadata.Files.size() == 1); diff --git a/src/AppInstallerCLITests/RestInterface_1_9.cpp b/src/AppInstallerCLITests/RestInterface_1_9.cpp index 0c516bd8b6..52d885df65 100644 --- a/src/AppInstallerCLITests/RestInterface_1_9.cpp +++ b/src/AppInstallerCLITests/RestInterface_1_9.cpp @@ -363,7 +363,7 @@ namespace REQUIRE(actualInstaller.UnsupportedArguments.size() == 1); REQUIRE(actualInstaller.UnsupportedArguments.at(0) == UnsupportedArgumentEnum::Log); REQUIRE(actualInstaller.NestedInstallerFiles.size() == 1); - REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath.u8string() == "test\\app.exe"); + REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath.string() == "test\\app.exe"); REQUIRE(actualInstaller.NestedInstallerFiles.at(0).PortableCommandAlias == "test.exe"); REQUIRE(actualInstaller.InstallationMetadata.DefaultInstallLocation == "%TEMP%\\DefaultInstallLocation"); REQUIRE(actualInstaller.InstallationMetadata.Files.size() == 1); diff --git a/src/AppInstallerCLITests/Strings.cpp b/src/AppInstallerCLITests/Strings.cpp index d22e4ff785..9f48b11b1b 100644 --- a/src/AppInstallerCLITests/Strings.cpp +++ b/src/AppInstallerCLITests/Strings.cpp @@ -109,18 +109,23 @@ TEST_CASE("NormalizedString", "[strings]") TEST_CASE("NormalizedPath", "[strings]") { - NormalizedPath path{ u8"directory/subdirectory/\xC3\xA4.exe" }; + NormalizedPath path{ u8"directory/subdirectory/\u00E4.exe" }; - REQUIRE(path.u8string() == u8"directory\\subdirectory\\\xC3\xA4.exe"); - REQUIRE(path.u16string() == u"directory\\subdirectory\\\u00E4.exe"); + REQUIRE(path.string() == "directory\\subdirectory\\\xC3\xA4.exe"); + REQUIRE(path.wstring() == L"directory\\subdirectory\\\u00E4.exe"s); path = u8"directory/subdirectory/\U0001F600.exe"; - REQUIRE(path.u8string() == u8"directory\\subdirectory\\\U0001F600.exe"); - REQUIRE(path.u16string() == u"directory\\subdirectory\\\U0001F600.exe"); + REQUIRE(path.string() == "directory\\subdirectory\\\xF0\x9F\x98\x80.exe"); + REQUIRE(path.wstring() == L"directory\\subdirectory\\\U0001F600.exe"s); + + // An overlong UTF8 encoding of '/' must not become a path separator. + path = "directory\xC0\xAF" "subdirectory/file.exe"; + REQUIRE(path.string() == "directory\xEF\xBF\xBD\xEF\xBF\xBD" "subdirectory\\file.exe"); + REQUIRE(path.wstring() == L"directory\uFFFD\uFFFDsubdirectory\\file.exe"s); path = std::string{ "other/path.exe" }; - REQUIRE(path.u8string() == "other\\path.exe"); - REQUIRE(path.u16string() == u"other\\path.exe"); + REQUIRE(path.string() == "other\\path.exe"); + REQUIRE(path.wstring() == L"other\\path.exe"s); } TEST_CASE("Trim", "[strings]") diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp index 5cbf6f9c52..7aa2bfc61d 100644 --- a/src/AppInstallerCLITests/YamlManifest.cpp +++ b/src/AppInstallerCLITests/YamlManifest.cpp @@ -299,7 +299,7 @@ namespace { REQUIRE(manifest.DefaultInstallerInfo.NestedInstallerType == InstallerTypeEnum::Msi); REQUIRE(manifest.DefaultInstallerInfo.NestedInstallerFiles.size() == 1); - REQUIRE(manifest.DefaultInstallerInfo.NestedInstallerFiles.at(0).RelativeFilePath.u8string() == "RelativeFilePath"); + REQUIRE(manifest.DefaultInstallerInfo.NestedInstallerFiles.at(0).RelativeFilePath.string() == "RelativeFilePath"); REQUIRE(manifest.DefaultInstallerInfo.NestedInstallerFiles.at(0).PortableCommandAlias == "PortableCommandAlias"); REQUIRE(manifest.DefaultInstallerInfo.InstallationMetadata.DefaultInstallLocation == "%ProgramFiles%\\TestApp"); REQUIRE(manifest.DefaultInstallerInfo.InstallationMetadata.Files.size() == 1); @@ -517,9 +517,9 @@ namespace REQUIRE(installer4.ProductCode == "{Foo}"); REQUIRE(installer4.NestedInstallerType == InstallerTypeEnum::Portable); REQUIRE(installer4.NestedInstallerFiles.size() == 2); - REQUIRE(installer4.NestedInstallerFiles.at(0).RelativeFilePath.u8string() == "relativeFilePath1"); + REQUIRE(installer4.NestedInstallerFiles.at(0).RelativeFilePath.string() == "relativeFilePath1"); REQUIRE(installer4.NestedInstallerFiles.at(0).PortableCommandAlias == "portableAlias1"); - REQUIRE(installer4.NestedInstallerFiles.at(1).RelativeFilePath.u8string() == "relativeFilePath2"); + REQUIRE(installer4.NestedInstallerFiles.at(1).RelativeFilePath.string() == "relativeFilePath2"); REQUIRE(installer4.NestedInstallerFiles.at(1).PortableCommandAlias == "portableAlias2"); REQUIRE(installer4.InstallationMetadata.DefaultInstallLocation == "%ProgramFiles%\\TestApp2"); REQUIRE(installer4.InstallationMetadata.Files.size() == 1); @@ -566,11 +566,11 @@ namespace REQUIRE(installer6.Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); REQUIRE(installer6.NestedInstallerType == InstallerTypeEnum::Font); REQUIRE(installer6.NestedInstallerFiles.size() == 5); - REQUIRE(installer6.NestedInstallerFiles.at(0).RelativeFilePath.u8string() == "relativeFilePath1.otf"); - REQUIRE(installer6.NestedInstallerFiles.at(1).RelativeFilePath.u8string() == "relativeFilePath2.ttf"); - REQUIRE(installer6.NestedInstallerFiles.at(2).RelativeFilePath.u8string() == "relativeFilePath3.fnt"); - REQUIRE(installer6.NestedInstallerFiles.at(3).RelativeFilePath.u8string() == "relativeFilePath4.ttc"); - REQUIRE(installer6.NestedInstallerFiles.at(4).RelativeFilePath.u8string() == "relativeFilePath5.otc"); + REQUIRE(installer6.NestedInstallerFiles.at(0).RelativeFilePath.string() == "relativeFilePath1.otf"); + REQUIRE(installer6.NestedInstallerFiles.at(1).RelativeFilePath.string() == "relativeFilePath2.ttf"); + REQUIRE(installer6.NestedInstallerFiles.at(2).RelativeFilePath.string() == "relativeFilePath3.fnt"); + REQUIRE(installer6.NestedInstallerFiles.at(3).RelativeFilePath.string() == "relativeFilePath4.ttc"); + REQUIRE(installer6.NestedInstallerFiles.at(4).RelativeFilePath.string() == "relativeFilePath5.otc"); const ManifestInstaller& installer7 = manifest.Installers.at(6); REQUIRE(installer7.BaseInstallerType == InstallerTypeEnum::Font); diff --git a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp index 92f0132055..08b28c255d 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp @@ -455,8 +455,8 @@ namespace AppInstaller::Manifest for (const auto& nestedInstallerFile : installer.NestedInstallerFiles) { - const std::filesystem::path relativeFilePath{ nestedInstallerFile.RelativeFilePath.u16string() }; - const std::string relativeFilePathUtf8 = nestedInstallerFile.RelativeFilePath.u8string(); + const std::filesystem::path relativeFilePath{ nestedInstallerFile.RelativeFilePath.wstring() }; + const std::string relativeFilePathUtf8 = nestedInstallerFile.RelativeFilePath.string(); if (relativeFilePath.empty()) { diff --git a/src/AppInstallerCommonCore/Manifest/YamlWriter.cpp b/src/AppInstallerCommonCore/Manifest/YamlWriter.cpp index f164042d5f..19ecad50ee 100644 --- a/src/AppInstallerCommonCore/Manifest/YamlWriter.cpp +++ b/src/AppInstallerCommonCore/Manifest/YamlWriter.cpp @@ -465,7 +465,7 @@ namespace AppInstaller::Manifest::YamlWriter for (const auto& nestedInstallerFile : nestedInstallerFiles) { out << YAML::BeginMap; - WRITE_PROPERTY(out, NestedInstallerFileRelativeFilePath, nestedInstallerFile.RelativeFilePath.u8string()); + WRITE_PROPERTY(out, NestedInstallerFileRelativeFilePath, nestedInstallerFile.RelativeFilePath.string()); WRITE_PROPERTY_IF_EXISTS(out, PortableCommandAlias, nestedInstallerFile.PortableCommandAlias); out << YAML::EndMap; } diff --git a/src/AppInstallerSharedLib/Public/winget/Filesystem.h b/src/AppInstallerSharedLib/Public/winget/Filesystem.h index 783afa161d..3358e3ef1d 100644 --- a/src/AppInstallerSharedLib/Public/winget/Filesystem.h +++ b/src/AppInstallerSharedLib/Public/winget/Filesystem.h @@ -13,7 +13,7 @@ using namespace std::chrono_literals; namespace AppInstaller::Utility { - // Type to hold a UTF16 path with preferred directory separators. + // Holds a UTF16 path with preferred directory separators; string() returns UTF8, wstring() returns UTF16. struct NormalizedPath { NormalizedPath() = default; @@ -30,8 +30,8 @@ namespace AppInstaller::Utility return *this; } - std::string u8string() const { return m_path.u8string(); } - std::u16string u16string() const { return m_path.u16string(); } + std::string string() const { return m_path.u8string(); } + std::wstring wstring() const { return m_path.wstring(); } const std::filesystem::path& get() const { return m_path; } private: From 183bb758b919f932e9589b434edcc19dfe5d0f1f Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Sun, 11 Oct 2026 21:12:10 -0500 Subject: [PATCH 6/7] Add E2E coverage for nested installer path validation Cover Unicode paths, normalized duplicate paths, and traversal rejection through WinGetUtil manifest creation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Manifests/NestedInstallerPaths.yaml | 20 +++++++ .../WinGetUtil/WinGetUtilManifest.cs | 55 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 src/AppInstallerCLIE2ETests/TestData/WinGetUtil/Manifests/NestedInstallerPaths.yaml diff --git a/src/AppInstallerCLIE2ETests/TestData/WinGetUtil/Manifests/NestedInstallerPaths.yaml b/src/AppInstallerCLIE2ETests/TestData/WinGetUtil/Manifests/NestedInstallerPaths.yaml new file mode 100644 index 0000000000..9704c82fbb --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/WinGetUtil/Manifests/NestedInstallerPaths.yaml @@ -0,0 +1,20 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.4.0.schema.json + +PackageIdentifier: AppInstallerTest.NestedInstallerPaths +PackageVersion: 1.0.0 +PackageName: NestedInstallerPaths +PackageLocale: en-US +Publisher: AppInstallerTest +License: Test +ShortDescription: E2E test for nested installer path validation. +Installers: + - Architecture: x64 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestZipInstaller/AppInstallerTestZipInstaller.zip + InstallerType: zip + InstallerSha256: 0000000000000000000000000000000000000000000000000000000000000000 + NestedInstallerType: portable + NestedInstallerFiles: + - RelativeFilePath: '' + - RelativeFilePath: 'bin\helper.exe' +ManifestType: singleton +ManifestVersion: 1.4.0 diff --git a/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilManifest.cs b/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilManifest.cs index ce41ff275b..09cc269f5d 100644 --- a/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilManifest.cs +++ b/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilManifest.cs @@ -82,6 +82,61 @@ public void WinGetUtil_ValidateManifest_Success(WinGetUtilWrapper.CreateManifest WinGetUtilWrapper.WinGetCloseManifest(manifestHandle); } + /// + /// Test normalization and semantic validation of nested installer paths through WinGetUtil. + /// + /// The first nested installer path. + /// The expected error, or null for a valid manifest. + [TestCase("bin/\u00E4\U0001F600.exe", null)] + [TestCase("../app.exe", "Relative file path must not point to a location outside of archive directory.")] + [TestCase(@"..\app.exe", "Relative file path must not point to a location outside of archive directory.")] + + // The test manifest specifies 'bin\helper.exe' - this case verifies duplicates are checked after normalization. + [TestCase("bin/helper.exe", "Duplicate relative file path found.")] + + public void WinGetUtil_ValidateManifest_NestedInstallerPaths(string relativeFilePath, string expectedError) + { + string manifestPath = TestCommon.GetRandomTestFile(".yaml"); + IntPtr manifestHandle = IntPtr.Zero; + + try + { + string manifest = File.ReadAllText(TestCommon.GetTestDataFile(@"WinGetUtil\Manifests\NestedInstallerPaths.yaml")); + File.WriteAllText(manifestPath, manifest.Replace("", relativeFilePath)); + + WinGetUtilWrapper.WinGetCreateManifest( + manifestPath, + out bool succeeded, + out manifestHandle, + out string createFailureMessage, + string.Empty, + WinGetUtilWrapper.CreateManifestOption.SchemaAndSemanticValidation); + + if (expectedError == null) + { + Assert.That(succeeded, Is.True); + Assert.That(manifestHandle, Is.Not.EqualTo(IntPtr.Zero)); + Assert.That(createFailureMessage, Is.Null); + } + else + { + Assert.That(succeeded, Is.False); + Assert.That(manifestHandle, Is.EqualTo(IntPtr.Zero)); + Assert.That(createFailureMessage, Does.Contain(expectedError)); + Assert.That(createFailureMessage, Does.Contain("RelativeFilePath")); + } + } + finally + { + if (manifestHandle != IntPtr.Zero) + { + WinGetUtilWrapper.WinGetCloseManifest(manifestHandle); + } + + File.Delete(manifestPath); + } + } + /// /// Test validate manifest with schema header. /// From 7e9cf7286ae380178cce02606454502ddac46b99 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Sun, 11 Oct 2026 21:20:24 -0500 Subject: [PATCH 7/7] Allow RELATIVEFILEPATH in spelling check Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/actions/spelling/allow.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 5532e50485..85194c75b0 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -279,6 +279,7 @@ REBOOTPROMPT redist Redistributable REFCLSID +RELATIVEFILEPATH removemanifest removepin removeportablefile