diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index cdf611bbdb..85194c75b0 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -276,8 +276,10 @@ QCol Qspectre rclsid REBOOTPROMPT +redist Redistributable REFCLSID +RELATIVEFILEPATH removemanifest removepin removeportablefile diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md index 151f337e5b..ef1b726315 100644 --- a/doc/ReleaseNotes.md +++ b/doc/ReleaseNotes.md @@ -55,3 +55,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..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) { - const std::filesystem::path& nestedInstallerPath = targetInstallerPath / ConvertToUTF16(nestedInstallerFile.RelativeFilePath); - - if (Filesystem::PathEscapesBaseDirectory(nestedInstallerFile.RelativeFilePath)) + std::filesystem::path nestedInstallerPath = targetInstallerPath / nestedInstallerFile.RelativeFilePath.wstring(); + + 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 87fb26682b..237c840f38 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,7 +224,8 @@ 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 nestedInstallerPath = installerPath / nestedInstallerFile.RelativeFilePath.wstring(); + fontContext.AddPackageFile(nestedInstallerPath); } } else diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp index 8d0a0ddfd1..b7437ad0b1 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -222,14 +222,13 @@ namespace AppInstaller::CLI::Workflow for (const auto& nestedInstallerFile : nestedInstallerFiles) { - EnsurePathIsRelative(context, nestedInstallerFile.RelativeFilePath, "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, {}); - const std::filesystem::path& relativeFilePath = ConvertToUTF16(nestedInstallerFile.RelativeFilePath); - const std::filesystem::path& targetPath = targetInstallDirectory / relativeFilePath; + std::filesystem::path targetPath = targetInstallDirectory / nestedInstallerFile.RelativeFilePath.wstring(); std::filesystem::path originalFilename = targetPath.filename(); std::filesystem::path commandAlias; 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. /// diff --git a/src/AppInstallerCLITests/InstallFlow.cpp b/src/AppInstallerCLITests/InstallFlow.cpp index f5eff1290c..3d50ddb056 100644 --- a/src/AppInstallerCLITests/InstallFlow.cpp +++ b/src/AppInstallerCLITests/InstallFlow.cpp @@ -361,6 +361,30 @@ 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"; + REQUIRE(installer.NestedInstallerFiles.at(0).RelativeFilePath.string() == "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"); diff --git a/src/AppInstallerCLITests/RestInterface_1_10.cpp b/src/AppInstallerCLITests/RestInterface_1_10.cpp index 8c002dcc31..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 == "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 fb55136924..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 == "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 4286430f00..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 == "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 fe5f2ecca0..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 == "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 f8207dd20f..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 == "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 17540a91e0..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 == "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 1d9da5af9a..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 == "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 e9763ae2ba..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 == "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 b131e6720e..9f48b11b1b 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,27 @@ TEST_CASE("NormalizedString", "[strings]") REQUIRE(NormalizedString(input3) == "Test Case"); } +TEST_CASE("NormalizedPath", "[strings]") +{ + NormalizedPath path{ u8"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.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.string() == "other\\path.exe"); + REQUIRE(path.wstring() == L"other\\path.exe"s); +} + TEST_CASE("Trim", "[strings]") { std::string str; diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp index f893881f83..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 == "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 == "relativeFilePath1"); + REQUIRE(installer4.NestedInstallerFiles.at(0).RelativeFilePath.string() == "relativeFilePath1"); REQUIRE(installer4.NestedInstallerFiles.at(0).PortableCommandAlias == "portableAlias1"); - REQUIRE(installer4.NestedInstallerFiles.at(1).RelativeFilePath == "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 == "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.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 71f946871a..08b28c255d 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.wstring() }; + const std::string relativeFilePathUtf8 = nestedInstallerFile.RelativeFilePath.string(); + + 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..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); + WRITE_PROPERTY(out, NestedInstallerFileRelativeFilePath, nestedInstallerFile.RelativeFilePath.string()); 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 865881938a..3358e3ef1d 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,34 @@ using namespace std::chrono_literals; +namespace AppInstaller::Utility +{ + // Holds a UTF16 path with preferred directory separators; string() returns UTF8, wstring() returns UTF16. + struct NormalizedPath + { + NormalizedPath() = default; + + NormalizedPath(const char* value) : NormalizedPath(std::string_view{ value }) {} + NormalizedPath(std::string_view value) { *this = value; } + + NormalizedPath& operator=(const char* value) { return *this = std::string_view{ value }; } + + NormalizedPath& operator=(std::string_view value) + { + m_path = AppInstaller::Utility::ConvertToUTF16(value); + m_path.make_preferred(); + return *this; + } + + 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: + std::filesystem::path m_path; + }; +} + namespace AppInstaller::Filesystem { // Checks if the file system at path supports named streams/ADS