diff --git a/doc/Settings.md b/doc/Settings.md index ae654a2aaf..3b2f014878 100644 --- a/doc/Settings.md +++ b/doc/Settings.md @@ -97,6 +97,16 @@ The 'skipDependencies' behavior affects whether dependencies are installed for a "installBehavior": { "skipDependencies": true }, +``` + +### Archive Extraction Method +The 'archiveExtractionMethod' behavior affects how installer archives are extracted. Currently there are two supported values: `Tar` or `ShellApi`. +`Tar` indicates that the archive should be extracted using the tar executable ('tar.exe') while `shellApi` indicates using the Windows Shell API. Defaults to `shellApi` if value is not set or is invalid. + +```json + "installBehavior": { + "archiveExtractionMethod": "tar" | "shellApi" + }, ``` ### Preferences and Requirements diff --git a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp index 069598e45b..fe5c3a5890 100644 --- a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp @@ -2,9 +2,10 @@ // Licensed under the MIT License. #include "pch.h" #include "ArchiveFlow.h" -#include "PortableFlow.h" +#include "PortableFlow.h" +#include "ShellExecuteInstallerHandler.h" #include -#include +#include #include using namespace AppInstaller::Manifest; @@ -42,30 +43,38 @@ namespace AppInstaller::CLI::Workflow } } } - } - - void ExtractFilesFromArchive(Execution::Context& context) - { + } + + void ExtractFilesFromArchive(Execution::Context& context) + { const auto& installerPath = context.Get(); std::filesystem::path destinationFolder = installerPath.parent_path() / s_Extracted; - std::filesystem::create_directory(destinationFolder); - + std::filesystem::create_directory(destinationFolder); + AICLI_LOG(CLI, Info, << "Extracting archive to: " << destinationFolder); - context.Reporter.Info() << Resource::String::ExtractingArchive << std::endl; - HRESULT result = AppInstaller::Archive::TryExtractArchive(installerPath, destinationFolder); - - if (SUCCEEDED(result)) - { - AICLI_LOG(CLI, Info, << "Successfully extracted archive"); - context.Reporter.Info() << Resource::String::ExtractArchiveSucceeded << std::endl; - } - else - { - AICLI_LOG(CLI, Info, << "Failed to extract archive with code " << result); - context.Reporter.Error() << Resource::String::ExtractArchiveFailed << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_EXTRACT_ARCHIVE_FAILED); - } - } + context.Reporter.Info() << Resource::String::ExtractingArchive << std::endl; + + if (Settings::User().Get() == Archive::ExtractionMethod::Tar) + { + context << ShellExecuteExtractArchive(installerPath, destinationFolder); + } + else + { + HRESULT result = AppInstaller::Archive::TryExtractArchive(installerPath, destinationFolder); + + if (SUCCEEDED(result)) + { + AICLI_LOG(CLI, Info, << "Successfully extracted archive"); + context.Reporter.Info() << Resource::String::ExtractArchiveSucceeded << std::endl; + } + else + { + AICLI_LOG(CLI, Info, << "Failed to extract archive with code " << result); + context.Reporter.Error() << Resource::String::ExtractArchiveFailed << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_EXTRACT_ARCHIVE_FAILED); + } + } + } void VerifyAndSetNestedInstaller(Execution::Context& context) { @@ -137,4 +146,4 @@ namespace AppInstaller::CLI::Workflow } } } -} \ No newline at end of file +} diff --git a/src/AppInstallerCLICore/Workflows/ArchiveFlow.h b/src/AppInstallerCLICore/Workflows/ArchiveFlow.h index 6fa3a09169..0bc8209d8f 100644 --- a/src/AppInstallerCLICore/Workflows/ArchiveFlow.h +++ b/src/AppInstallerCLICore/Workflows/ArchiveFlow.h @@ -10,12 +10,12 @@ namespace AppInstaller::CLI::Workflow // Inputs: InstallerPath // Outputs: None void ScanArchiveFromLocalManifest(Execution::Context& context); - + // Extracts the files from an archive // Required Args: None // Inputs: InstallerPath - // Outputs: None - void ExtractFilesFromArchive(Execution::Context& context); + // Outputs: None + void ExtractFilesFromArchive(Execution::Context& context); // Verifies that the NestedInstaller exists and sets the InstallerPath // Required Args: None @@ -28,4 +28,4 @@ namespace AppInstaller::CLI::Workflow // Inputs: Installer, InstallerPath // Outputs: None void EnsureValidNestedInstallerMetadataForArchiveInstall(Execution::Context& context); -} \ No newline at end of file +} diff --git a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp index a25ca30cd0..781984a47c 100644 --- a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp +++ b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp @@ -538,4 +538,55 @@ namespace AppInstaller::CLI::Workflow context.Add(enableFeatureResult.value()); } } -} \ No newline at end of file + +#ifndef AICLI_DISABLE_TEST_HOOKS + std::optional s_ExtractArchiveWithTarResult_Override{}; + + void TestHook_SetExtractArchiveWithTarResult_Override(std::optional&& result) + { + s_ExtractArchiveWithTarResult_Override = std::move(result); + } +#endif + + void ShellExecuteExtractArchive::operator()(Execution::Context& context) const + { + auto tarExecPath = AppInstaller::Filesystem::GetExpandedPath("%windir%\\system32\\tar.exe"); + + std::string args = "-xf \"" + m_archivePath.u8string() + "\" -C \"" + m_destPath.u8string() + "\""; + + std::optional extractArchiveResult; +#ifndef AICLI_DISABLE_TEST_HOOKS + if (s_ExtractArchiveWithTarResult_Override) + { + extractArchiveResult = *s_ExtractArchiveWithTarResult_Override; + } + else +#endif + { + extractArchiveResult = context.Reporter.ExecuteWithProgress( + std::bind(InvokeShellExecuteEx, + tarExecPath, + args, + false, + SW_HIDE, + std::placeholders::_1)); + } + + if (!extractArchiveResult) + { + AICLI_TERMINATE_CONTEXT(E_ABORT); + } + + if (extractArchiveResult.value() == ERROR_SUCCESS) + { + AICLI_LOG(CLI, Info, << "Successfully extracted archive"); + context.Reporter.Info() << Resource::String::ExtractArchiveSucceeded << std::endl; + } + else + { + AICLI_LOG(CLI, Info, << "Failed to extract archive with exit code " << extractArchiveResult.value()); + context.Reporter.Error() << Resource::String::ExtractArchiveFailed << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_EXTRACT_ARCHIVE_FAILED); + } + } +} diff --git a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.h b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.h index 87fac6db18..8dc3cfb70e 100644 --- a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.h +++ b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.h @@ -60,4 +60,19 @@ namespace AppInstaller::CLI::Workflow private: std::string_view m_featureName; }; -} \ No newline at end of file + + // Extracts the installer archive using the tar executable. + // Required Args: None + // Inputs: InstallerPath + // Outputs: None + struct ShellExecuteExtractArchive : public WorkflowTask + { + ShellExecuteExtractArchive(const std::filesystem::path& archivePath, const std::filesystem::path& destPath) : WorkflowTask("ShellExecuteExtractArchive"), m_archivePath(archivePath), m_destPath(destPath) {} + + void operator()(Execution::Context& context) const override; + + private: + std::filesystem::path m_archivePath; + std::filesystem::path m_destPath; + }; +} diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs index 5aa1c34efe..9a679492bd 100644 --- a/src/AppInstallerCLIE2ETests/Constants.cs +++ b/src/AppInstallerCLIE2ETests/Constants.cs @@ -119,6 +119,7 @@ public class Constants public const string PathSubKey_Machine = @"SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; // User settings + public const string ArchiveExtractionMethod = "archiveExtractionMethod"; public const string PortablePackageUserRoot = "portablePackageUserRoot"; public const string PortablePackageMachineRoot = "portablePackageMachineRoot"; public const string InstallBehaviorScope = "scope"; diff --git a/src/AppInstallerCLIE2ETests/InstallCommand.cs b/src/AppInstallerCLIE2ETests/InstallCommand.cs index be81f220d0..54efdef1c8 100644 --- a/src/AppInstallerCLIE2ETests/InstallCommand.cs +++ b/src/AppInstallerCLIE2ETests/InstallCommand.cs @@ -552,6 +552,21 @@ public void InstallZipWithMsix() Assert.True(TestCommon.VerifyTestMsixInstalledAndCleanup()); } + /// + /// Test install zip exe by extracting with tar. + /// + [Test] + public void InstallZip_ExtractWithTar() + { + WinGetSettingsHelper.ConfigureInstallBehavior(Constants.ArchiveExtractionMethod, "tar"); + var installDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestZipInstallerWithExe --silent -l {installDir}"); + WinGetSettingsHelper.ConfigureInstallBehavior(Constants.ArchiveExtractionMethod, string.Empty); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(installDir, "/execustom")); + } + /// /// Test install an installed package and convert to upgrade. /// @@ -764,4 +779,4 @@ public void InstallExeThatInstallsMSIX() TestCommon.RemoveARPEntry(fakeProductCode); } } -} \ No newline at end of file +} diff --git a/src/AppInstallerCLITests/Archive.cpp b/src/AppInstallerCLITests/Archive.cpp index 31070c7c39..4d415761f1 100644 --- a/src/AppInstallerCLITests/Archive.cpp +++ b/src/AppInstallerCLITests/Archive.cpp @@ -31,4 +31,4 @@ TEST_CASE("Scan_ZipArchive", "[archive]") const auto& testZipPath = testZip.GetPath(); bool result = ScanZipFile(testZipPath); REQUIRE(result); -} \ No newline at end of file +} diff --git a/src/AppInstallerCLITests/InstallFlow.cpp b/src/AppInstallerCLITests/InstallFlow.cpp index a52dd702a1..5cf6229805 100644 --- a/src/AppInstallerCLITests/InstallFlow.cpp +++ b/src/AppInstallerCLITests/InstallFlow.cpp @@ -507,6 +507,59 @@ TEST_CASE("ExtractInstallerFromArchive_InvalidZip", "[InstallFlow][workflow]") auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Zip_Exe.yaml")); context.Add(manifest); context.Add(manifest.Installers.at(0)); + + // Provide an invalid zip file which should be handled appropriately. + context.Add(TestDataFile("AppInstallerTestExeInstaller.exe")); + context << ExtractFilesFromArchive; + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_EXTRACT_ARCHIVE_FAILED); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::ExtractArchiveFailed).get()) != std::string::npos); +} + +TEST_CASE("ExtractInstallerFromArchiveWithTar", "[InstallFlow][workflow]") +{ + TestCommon::TestUserSettings testSettings; + testSettings.Set(AppInstaller::Archive::ExtractionMethod::Tar); + + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + + OverrideForShellExecute(context); + OverrideForVerifyAndSetNestedInstaller(context); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_Zip_Exe.yaml").GetPath().u8string()); + + TestHook::SetScanArchiveResult_Override scanArchiveResultOverride(true); + TestHook::SetExtractArchiveWithTarResult_Override setExtractArchiveWithTarResultOverride(ERROR_SUCCESS); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::ExtractArchiveSucceeded).get()) != std::string::npos); + + // Verify Installer is called and parameters are passed in. + REQUIRE(std::filesystem::exists(installResultPath.GetPath())); + std::ifstream installResultFile(installResultPath.GetPath()); + REQUIRE(installResultFile.is_open()); + std::string installResultStr; + std::getline(installResultFile, installResultStr); + REQUIRE(installResultStr.find("/custom") != std::string::npos); + REQUIRE(installResultStr.find("/silentwithprogress") != std::string::npos); +} + +TEST_CASE("ExtractInstallerFromArchiveWithTar_InvalidZip", "[InstallFlow][workflow]") +{ + TestCommon::TestUserSettings testSettings; + testSettings.Set(AppInstaller::Archive::ExtractionMethod::Tar); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Zip_Exe.yaml")); + context.Add(manifest); + context.Add(manifest.Installers.at(0)); + // Provide an invalid zip file which should be handled appropriately. context.Add(TestDataFile("AppInstallerTestExeInstaller.exe")); context << ExtractFilesFromArchive; diff --git a/src/AppInstallerCLITests/TestHooks.h b/src/AppInstallerCLITests/TestHooks.h index f27b1738bc..3917c8d1c2 100644 --- a/src/AppInstallerCLITests/TestHooks.h +++ b/src/AppInstallerCLITests/TestHooks.h @@ -71,6 +71,7 @@ namespace AppInstaller { void TestHook_SetEnableWindowsFeatureResult_Override(std::optional&& result); void TestHook_SetDoesWindowsFeatureExistResult_Override(std::optional&& result); + void TestHook_SetExtractArchiveWithTarResult_Override(std::optional&& result); } namespace Reboot @@ -183,6 +184,19 @@ namespace TestHook } }; + struct SetExtractArchiveWithTarResult_Override + { + SetExtractArchiveWithTarResult_Override(DWORD result) + { + AppInstaller::CLI::Workflow::TestHook_SetExtractArchiveWithTarResult_Override(result); + } + + ~SetExtractArchiveWithTarResult_Override() + { + AppInstaller::CLI::Workflow::TestHook_SetExtractArchiveWithTarResult_Override({}); + } + }; + struct SetInitiateRebootResult_Override { SetInitiateRebootResult_Override(bool status) : m_status(status) diff --git a/src/AppInstallerCLITests/UserSettings.cpp b/src/AppInstallerCLITests/UserSettings.cpp index b8f072e632..ed54c36bbc 100644 --- a/src/AppInstallerCLITests/UserSettings.cpp +++ b/src/AppInstallerCLITests/UserSettings.cpp @@ -524,6 +524,28 @@ TEST_CASE("SettingsDownloadDefaultDirectory", "[settings]") REQUIRE(userSettingTest.Get() == "C:/Foo/Bar"); REQUIRE(userSettingTest.GetWarnings().size() == 0); } +} + +TEST_CASE("SettingsArchiveExtractionMethod", "[settings]") +{ + auto again = DeleteUserSettingsFiles(); + + SECTION("Shell api") + { + std::string_view json = R"({ "installBehavior": { "archiveExtractionMethod": "shellApi" } })"; + SetSetting(Stream::PrimaryUserSettings, json); + UserSettingsTest userSettingTest; + + REQUIRE(userSettingTest.Get() == AppInstaller::Archive::ExtractionMethod::ShellApi); + } + SECTION("Shell api") + { + std::string_view json = R"({ "installBehavior": { "archiveExtractionMethod": "tar" } })"; + SetSetting(Stream::PrimaryUserSettings, json); + UserSettingsTest userSettingTest; + + REQUIRE(userSettingTest.Get() == AppInstaller::Archive::ExtractionMethod::Tar); + } } TEST_CASE("SettingsInstallScope", "[settings]") diff --git a/src/AppInstallerCommonCore/Archive.cpp b/src/AppInstallerCommonCore/Archive.cpp index 9d5f9c5896..95766519e9 100644 --- a/src/AppInstallerCommonCore/Archive.cpp +++ b/src/AppInstallerCommonCore/Archive.cpp @@ -49,7 +49,6 @@ namespace AppInstaller::Archive return S_OK; } - #ifndef AICLI_DISABLE_TEST_HOOKS static bool* s_ScanArchiveResult_TestHook_Override = nullptr; @@ -77,4 +76,4 @@ namespace AppInstaller::Archive return scanResult == 0; } -} \ No newline at end of file +} diff --git a/src/AppInstallerCommonCore/Public/winget/Archive.h b/src/AppInstallerCommonCore/Public/winget/Archive.h index 8855d42fc4..3ca16a3b3e 100644 --- a/src/AppInstallerCommonCore/Public/winget/Archive.h +++ b/src/AppInstallerCommonCore/Public/winget/Archive.h @@ -5,7 +5,14 @@ namespace AppInstaller::Archive { + enum class ExtractionMethod + { + // Default archive extraction method is ShellApi. + ShellApi, + Tar, + }; + HRESULT TryExtractArchive(const std::filesystem::path& archivePath, const std::filesystem::path& destPath); bool ScanZipFile(const std::filesystem::path& zipPath); -} \ No newline at end of file +} diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index 45bef7fe20..aceb9c611f 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -3,6 +3,7 @@ #pragma once #include "AppInstallerStrings.h" #include "AppInstallerLogging.h" +#include "winget/Archive.h" #include "winget/GroupPolicy.h" #include "winget/Resources.h" #include "winget/ManifestCommon.h" @@ -89,6 +90,7 @@ namespace AppInstaller::Settings InstallerTypeRequirement, InstallDefaultRoot, InstallSkipDependencies, + ArchiveExtractionMethod, DisableInstallNotes, PortablePackageUserRoot, PortablePackageMachineRoot, @@ -171,6 +173,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::InstallerTypePreference, std::vector, std::vector, {}, ".installBehavior.preferences.installerTypes"sv); SETTINGMAPPING_SPECIALIZATION(Setting::InstallerTypeRequirement, std::vector, std::vector, {}, ".installBehavior.requirements.installerTypes"sv); SETTINGMAPPING_SPECIALIZATION(Setting::InstallSkipDependencies, bool, bool, false, ".installBehavior.skipDependencies"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::ArchiveExtractionMethod, std::string, Archive::ExtractionMethod, Archive::ExtractionMethod::ShellApi, ".installBehavior.archiveExtractionMethod"sv); SETTINGMAPPING_SPECIALIZATION(Setting::DisableInstallNotes, bool, bool, false, ".installBehavior.disableInstallNotes"sv); SETTINGMAPPING_SPECIALIZATION(Setting::PortablePackageUserRoot, std::string, std::filesystem::path, {}, ".installBehavior.portablePackageUserRoot"sv); SETTINGMAPPING_SPECIALIZATION(Setting::PortablePackageMachineRoot, std::string, std::filesystem::path, {}, ".installBehavior.portablePackageMachineRoot"sv); diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index 95d39d3745..c299a37140 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -289,6 +289,23 @@ namespace AppInstaller::Settings return ValidatePathValue(value); } + WINGET_VALIDATE_SIGNATURE(ArchiveExtractionMethod) + { + static constexpr std::string_view s_archiveExtractionMethod_shellApi = "shellApi"; + static constexpr std::string_view s_archiveExtractionMethod_tar = "tar"; + + if (Utility::CaseInsensitiveEquals(value, s_archiveExtractionMethod_tar)) + { + return Archive::ExtractionMethod::Tar; + } + else if (Utility::CaseInsensitiveEquals(value, s_archiveExtractionMethod_shellApi)) + { + return Archive::ExtractionMethod::ShellApi; + } + + return {}; + } + WINGET_VALIDATE_SIGNATURE(InstallArchitecturePreference) { std::vector archs;