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
2 changes: 2 additions & 0 deletions .github/actions/spelling/allow.txt
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,10 @@ QCol
Qspectre
rclsid
REBOOTPROMPT
redist
Redistributable
REFCLSID
RELATIVEFILEPATH
removemanifest
removepin
removeportablefile
Expand Down
1 change: 1 addition & 0 deletions doc/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
6 changes: 3 additions & 3 deletions src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion src/AppInstallerCLICore/Workflows/FontFlow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "pch.h"
#include "FontFlow.h"
#include "TableOutput.h"
#include <winget/Filesystem.h>
#include <winget/Fonts.h>
#include <AppInstallerRuntime.h>
using namespace AppInstaller::Utility;
Expand Down Expand Up @@ -223,7 +224,8 @@ namespace AppInstaller::CLI::Workflow
const std::vector<Manifest::NestedInstallerFile>& nestedInstallerFiles = context.Get<Execution::Data::Installer>()->NestedInstallerFiles;
for (const auto& nestedInstallerFile : nestedInstallerFiles)
{
fontContext.AddPackageFile(installerPath / ConvertToUTF16(nestedInstallerFile.RelativeFilePath));
std::filesystem::path nestedInstallerPath = installerPath / nestedInstallerFile.RelativeFilePath.wstring();
fontContext.AddPackageFile(nestedInstallerPath);
}
}
else
Expand Down
5 changes: 2 additions & 3 deletions src/AppInstallerCLICore/Workflows/PortableFlow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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>'
- RelativeFilePath: 'bin\helper.exe'
ManifestType: singleton
ManifestVersion: 1.4.0
55 changes: 55 additions & 0 deletions src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilManifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,61 @@ public void WinGetUtil_ValidateManifest_Success(WinGetUtilWrapper.CreateManifest
WinGetUtilWrapper.WinGetCloseManifest(manifestHandle);
}

/// <summary>
/// Test normalization and semantic validation of nested installer paths through WinGetUtil.
/// </summary>
/// <param name="relativeFilePath">The first nested installer path.</param>
/// <param name="expectedError">The expected error, or null for a valid manifest.</param>
[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>", 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);
}
}

/// <summary>
/// Test validate manifest with schema header.
/// </summary>
Expand Down
24 changes: 24 additions & 0 deletions src/AppInstallerCLITests/InstallFlow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Data::Installer>(std::move(installer));
context.Add<Data::InstallerPath>(archivePath);
context << VerifyAndSetNestedInstaller;

REQUIRE(context.Get<Data::InstallerPath>() == nestedInstallerPath);
}

TEST_CASE("InstallFlow_Zip_MissingNestedInstaller", "[InstallFlow][workflow]")
{
TestCommon::TempFile installResultPath("TestExeInstalled.txt");
Expand Down
2 changes: 1 addition & 1 deletion src/AppInstallerCLITests/RestInterface_1_10.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/AppInstallerCLITests/RestInterface_1_12.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/AppInstallerCLITests/RestInterface_1_28.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/AppInstallerCLITests/RestInterface_1_4.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/AppInstallerCLITests/RestInterface_1_5.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/AppInstallerCLITests/RestInterface_1_6.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/AppInstallerCLITests/RestInterface_1_7.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/AppInstallerCLITests/RestInterface_1_9.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
22 changes: 22 additions & 0 deletions src/AppInstallerCLITests/Strings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <AppInstallerStrings.h>
#include <AppInstallerSHA256.h>
#include <ExecutionReporter.h>
#include <winget/Filesystem.h>

using namespace std::string_literals;
using namespace std::string_view_literals;
Expand Down Expand Up @@ -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;
Expand Down
16 changes: 8 additions & 8 deletions src/AppInstallerCLITests/YamlManifest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading