commit 662842c8cf9012eae840ab94c741f046e0b20526 parent 6d0930ed95789a91ae5491d4ea9cff5261328965 Author: Ryan Fu <69221034+ryfu-msft@users.noreply.github.com> Date: Wed, 13 Jul 2022 13:06:06 -0700 Implementation for Zip Install (Non-Portable) (#2320) Diffstat:
34 files changed, 634 insertions(+), 20 deletions(-)
diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -130,6 +130,7 @@ fintimes Fixfor flargle flexera +FOF foldc foldcase FOLDERID @@ -166,7 +167,9 @@ IAttachment IARP IConfiguration idx +IEnum IFACEMETHODIMP +IFile IGlobal IHelp IHost @@ -186,6 +189,7 @@ IPersist IRead IService ISettings +IShell ishelp ISQ issuecomment @@ -222,6 +226,7 @@ localhost localizationpriority LPBYTE LPDWORD +LPITEMIDLIST LPWSTR LSTATUS LTDA @@ -259,11 +264,13 @@ mylog mysilent mysilentwithprogress mytool +NESTEDINSTALLER NETFX netlify Newtonsoft NOEXPAND nonetwork +NONFOLDERS nonterminated normer NOSEPARATOR @@ -293,6 +300,8 @@ PEGI pfn pfxpath Pherson +pidl +PIDLIST pkgmgr pkindex PMS @@ -346,6 +355,8 @@ seof serializer setmetadatabymanifestid SETTINGMAPPING +SHCONTF +SHGDN Shlobj sid SIGNATUREHASH @@ -360,6 +371,7 @@ srs standalone startswith streambuf +STRRET strtoull subdir subkey @@ -453,4 +465,5 @@ yamlcreateps yao ype Zanollo +ZIPHASH zy diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -275,6 +275,7 @@ <ClInclude Include="TableOutput.h" /> <ClInclude Include="VTSupport.h" /> <ClInclude Include="PackageCollection.h" /> + <ClInclude Include="Workflows\ArchiveFlow.h" /> <ClInclude Include="Workflows\CompletionFlow.h" /> <ClInclude Include="Workflows\DependencyNodeProcessor.h" /> <ClInclude Include="Workflows\DownloadFlow.h" /> @@ -327,6 +328,7 @@ </ClCompile> <ClCompile Include="Resources.cpp" /> <ClCompile Include="VTSupport.cpp" /> + <ClCompile Include="Workflows\ArchiveFlow.cpp" /> <ClCompile Include="Workflows\CompletionFlow.cpp" /> <ClCompile Include="Workflows\DependencyNodeProcessor.cpp" /> <ClCompile Include="Workflows\DownloadFlow.cpp" /> diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -179,6 +179,9 @@ <ClInclude Include="Workflows\PortableFlow.h"> <Filter>Workflows</Filter> </ClInclude> + <ClInclude Include="Workflows\ArchiveFlow.h"> + <Filter>Workflows</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -325,6 +328,9 @@ <ClCompile Include="Workflows\PortableFlow.cpp"> <Filter>Workflows</Filter> </ClCompile> + <ClCompile Include="Workflows\ArchiveFlow.cpp"> + <Filter>Workflows</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -68,6 +68,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ExportIncludeVersionsArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExportSourceArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExternalDependencies); + WINGET_DEFINE_RESOURCE_STRINGID(ExtractArchiveFailed); WINGET_DEFINE_RESOURCE_STRINGID(ExtraPositionalError); WINGET_DEFINE_RESOURCE_STRINGID(FeatureDisabledByAdminSettingMessage); WINGET_DEFINE_RESOURCE_STRINGID(FeatureDisabledMessage); @@ -188,6 +189,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(MultipleInstalledPackagesFound); WINGET_DEFINE_RESOURCE_STRINGID(MultiplePackagesFound); WINGET_DEFINE_RESOURCE_STRINGID(NameArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(NestedInstallerNotFound); WINGET_DEFINE_RESOURCE_STRINGID(NoApplicableInstallers); WINGET_DEFINE_RESOURCE_STRINGID(NoExperimentalFeaturesMessage); WINGET_DEFINE_RESOURCE_STRINGID(NoInstalledPackageFound); @@ -216,6 +218,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(PortableHashMismatchOverridden); WINGET_DEFINE_RESOURCE_STRINGID(PortableHashMismatchOverrideRequired); WINGET_DEFINE_RESOURCE_STRINGID(PortableInstallFailed); + WINGET_DEFINE_RESOURCE_STRINGID(PortableInstallFromArchiveNotSupported); WINGET_DEFINE_RESOURCE_STRINGID(PortableRegistryCollisionOverridden); WINGET_DEFINE_RESOURCE_STRINGID(PositionArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(PreserveArgumentDescription); diff --git a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "ArchiveFlow.h" +#include "winget/Archive.h" + +namespace AppInstaller::CLI::Workflow +{ + void ExtractFilesFromArchive(Execution::Context& context) + { + const auto& installerPath = context.Get<Execution::Data::InstallerPath>(); + const auto& installerParentPath = installerPath.parent_path(); + + // TODO: For portables, extract portables to final install location and log to local database. + HRESULT hr = AppInstaller::Archive::TryExtractArchive(installerPath, installerParentPath); + AICLI_LOG(CLI, Info, << "Extracting archive to: " << installerParentPath); + + if (SUCCEEDED(hr)) + { + AICLI_LOG(CLI, Info, << "Successfully extracted archive"); + } + else + { + AICLI_LOG(CLI, Info, << "Failed to extract archive with code " << hr); + context.Reporter.Error() << Resource::String::ExtractArchiveFailed << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_EXTRACT_ARCHIVE_FAILED); + } + } + + void VerifyAndSetNestedInstaller(Execution::Context& context) + { + const auto& installer = context.Get<Execution::Data::Installer>().value(); + if (installer.NestedInstallerFiles.empty()) + { + // Manifest validation should prevent this from happening + AICLI_LOG(CLI, Error, << "No entries specified for NestedInstallerFiles"); + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INVALID_MANIFEST); + } + + const auto& installerPath = context.Get<Execution::Data::InstallerPath>(); + const auto& installerParentPath = installerPath.parent_path(); + + const auto& relativeFilePath = ConvertToUTF16(installer.NestedInstallerFiles[0].RelativeFilePath); + + std::filesystem::path nestedInstallerPath = installerParentPath / relativeFilePath; + + if (!std::filesystem::exists(nestedInstallerPath)) + { + AICLI_LOG(CLI, Error, << "Unable to locate nested installer at: " << nestedInstallerPath); + context.Reporter.Error() << Resource::String::NestedInstallerNotFound << ' ' << nestedInstallerPath << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NESTEDINSTALLER_NOT_FOUND); + } + else + { + AICLI_LOG(CLI, Info, << "Setting installerPath to: " << nestedInstallerPath); + context.Add<Execution::Data::InstallerPath>(nestedInstallerPath); + } + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/ArchiveFlow.h b/src/AppInstallerCLICore/Workflows/ArchiveFlow.h @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ExecutionContext.h" + +namespace AppInstaller::CLI::Workflow +{ + // Extracts the files from an archive + // Required Args: None + // Inputs: InstallerPath + // Outputs: None + void ExtractFilesFromArchive(Execution::Context& context); + + // Verifies that the NestedInstaller exists and sets the InstallerPath + // Required Args: None + // Inputs: Installer, InstallerPath + // Outputs: None + void VerifyAndSetNestedInstaller(Execution::Context& context); +}+ \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp b/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp @@ -152,6 +152,7 @@ namespace AppInstaller::CLI::Workflow case InstallerTypeEnum::Nullsoft: case InstallerTypeEnum::Portable: case InstallerTypeEnum::Wix: + case InstallerTypeEnum::Zip: context << DownloadInstallerFile; break; case InstallerTypeEnum::Msix: diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -9,11 +9,13 @@ #include "ShellExecuteInstallerHandler.h" #include "MSStoreInstallerHandler.h" #include "MsiInstallFlow.h" +#include "ArchiveFlow.h" #include "PortableFlow.h" #include "WorkflowBase.h" #include "Workflows/DependenciesFlow.h" #include <AppInstallerDeployment.h> #include <winget/ARPCorrelation.h> +#include <winget/Archive.h> #include <Argument.h> #include <Command.h> @@ -316,13 +318,12 @@ namespace AppInstaller::CLI::Workflow } } - void ExecuteInstaller(Execution::Context& context) + void ExecuteInstallerForType::operator()(Execution::Context& context) const { - const auto& installer = context.Get<Execution::Data::Installer>().value(); - bool isUpdate = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate); + UpdateBehaviorEnum updateBehavior = context.Get<Execution::Data::Installer>().value().UpdateBehavior; - switch (installer.InstallerType) + switch (m_installerType) { case InstallerTypeEnum::Exe: case InstallerTypeEnum::Burn: @@ -330,14 +331,14 @@ namespace AppInstaller::CLI::Workflow case InstallerTypeEnum::Msi: case InstallerTypeEnum::Nullsoft: case InstallerTypeEnum::Wix: - if (isUpdate && installer.UpdateBehavior == UpdateBehaviorEnum::UninstallPrevious) + if (isUpdate && updateBehavior == UpdateBehaviorEnum::UninstallPrevious) { context << GetUninstallInfo << ExecuteUninstaller; context.ClearFlags(Execution::ContextFlag::InstallerExecutionUseUpdate); } - if (ShouldUseDirectMSIInstall(installer.InstallerType, context.Args.Contains(Execution::Args::Type::Silent))) + if (ShouldUseDirectMSIInstall(m_installerType, context.Args.Contains(Execution::Args::Type::Silent))) { context << DirectMSIInstall; } @@ -355,7 +356,7 @@ namespace AppInstaller::CLI::Workflow (isUpdate ? MSStoreUpdate : MSStoreInstall); break; case InstallerTypeEnum::Portable: - if (isUpdate && installer.UpdateBehavior == UpdateBehaviorEnum::UninstallPrevious) + if (isUpdate && updateBehavior == UpdateBehaviorEnum::UninstallPrevious) { context << GetUninstallInfo << @@ -364,11 +365,27 @@ namespace AppInstaller::CLI::Workflow } context << PortableInstall; break; + case InstallerTypeEnum::Zip: + context << ArchiveInstall; + break; default: THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); } } + void ExecuteInstaller(Execution::Context& context) + { + context << Workflow::ExecuteInstallerForType(context.Get<Execution::Data::Installer>().value().InstallerType); + } + + void ArchiveInstall(Execution::Context& context) + { + context << + ExtractFilesFromArchive << + VerifyAndSetNestedInstaller << + ExecuteInstallerForType(context.Get<Execution::Data::Installer>().value().NestedInstallerType); + } + void ShellExecuteInstall(Execution::Context& context) { context << @@ -524,7 +541,8 @@ namespace AppInstaller::CLI::Workflow void EnsureSupportForInstall(Execution::Context& context) { context << - Workflow::EnsureSupportForPortableInstall; + Workflow::EnsureSupportForPortableInstall << + Workflow::EnsureNonPortableTypeForArchiveInstall; } void InstallMultiplePackages::operator()(Execution::Context& context) const diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.h b/src/AppInstallerCLICore/Workflows/InstallFlow.h @@ -72,12 +72,26 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void EnsurePackageAgreementsAcceptanceForMultipleInstallers(Execution::Context& context); - // Composite flow that chooses what to do based on the installer type. + // Starts execution of the installer. // Required Args: None // Inputs: Installer, InstallerPath // Outputs: None void ExecuteInstaller(Execution::Context& context); + // Composite flow that chooses what to do based on the installer type. + // Required Args: None + // Inputs: Installer, InstallerPath + // Outputs: None + struct ExecuteInstallerForType : public WorkflowTask + { + ExecuteInstallerForType(Manifest::InstallerTypeEnum installerType) : WorkflowTask("ExecuteInstallerForType"), m_installerType(installerType) {} + + void operator()(Execution::Context& context) const override; + + private: + Manifest::InstallerTypeEnum m_installerType; + }; + // Runs the installer via ShellExecute. // Required Args: None // Inputs: Installer, InstallerPath @@ -102,6 +116,12 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void PortableInstall(Execution::Context& context); + // Runs the flow for installing a package from an archive. + // Required Args: None + // Inputs: Installer, InstallerPath, Manifest + // Outputs: None + void ArchiveInstall(Execution::Context& context); + // Verifies parameters for install to ensure success. // Required Args: None // Inputs: diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -621,4 +621,16 @@ namespace AppInstaller::CLI::Workflow EnsureVolumeSupportsReparsePoints; } } + + // TODO: remove this check once support for portable in archive has been implemented + void EnsureNonPortableTypeForArchiveInstall(Execution::Context& context) + { + auto nestedInstallerType = context.Get<Execution::Data::Installer>().value().NestedInstallerType; + + if (nestedInstallerType == InstallerTypeEnum::Portable) + { + context.Reporter.Error() << Resource::String::PortableInstallFromArchiveNotSupported << std::endl; + AICLI_TERMINATE_CONTEXT(ERROR_NOT_SUPPORTED); + } + } } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.h b/src/AppInstallerCLICore/Workflows/PortableFlow.h @@ -18,4 +18,6 @@ namespace AppInstaller::CLI::Workflow void PortableUninstallImpl(Execution::Context& context); void EnsureSupportForPortableInstall(Execution::Context& context); + + void EnsureNonPortableTypeForArchiveInstall(Execution::Context& context); } \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs @@ -37,6 +37,11 @@ namespace AppInstallerCLIE2ETests public const string ExeInstaller = "AppInstallerTestExeInstaller"; public const string MsiInstaller = "AppInstallerTestMsiInstaller"; public const string MsixInstaller = "AppInstallerTestMsixInstaller"; + public const string ZipInstaller = "AppInstallerTestZipInstaller"; + public const string ExeInstallerFileName = "AppInstallerTestExeInstaller.exe"; + public const string MsiInstallerFileName = "AppInstallerTestMsiInstaller.msi"; + public const string MsixInstallerFileName = "AppInstallerTestMsixInstaller.msix"; + public const string ZipInstallerFileName = "AppInstallerTestZipInstaller.zip"; public const string IndexPackage = "source.msix"; public const string MakeAppx = "makeappx.exe"; public const string SignTool = "signtool.exe"; diff --git a/src/AppInstallerCLIE2ETests/InstallCommand.cs b/src/AppInstallerCLIE2ETests/InstallCommand.cs @@ -271,6 +271,40 @@ namespace AppInstallerCLIE2ETests TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, false); } + [Test] + public void InstallZipWithExe() + { + var installDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestZipInstallerWithExe --silent -l {installDir}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + Assert.True(VerifyTestExeInstalled(installDir, "/execustom")); + } + + [Test] + public void InstallZipWithMsi() + { + if (string.IsNullOrEmpty(TestCommon.MsiInstallerPath)) + { + Assert.Ignore("MSI installer not available"); + } + + var installDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestZipInstallerWithMsi --silent -l {installDir}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + Assert.True(VerifyTestMsiInstalledAndCleanup(installDir)); + } + + [Test] + public void InstallZipWithMsix() + { + var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestZipInstallerWithMsix"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + Assert.True(VerifyTestMsixInstalledAndCleanup()); + } + private bool VerifyTestExeInstalled(string installDir, string expectedContent = null) { if (!File.Exists(Path.Combine(installDir, Constants.TestExeInstalledFileName))) diff --git a/src/AppInstallerCLIE2ETests/TestCommon.cs b/src/AppInstallerCLIE2ETests/TestCommon.cs @@ -32,6 +32,8 @@ namespace AppInstallerCLIE2ETests public static string MsiInstallerPath { get; set; } public static string MsixInstallerPath { get; set; } + + public static string ZipInstallerPath { get; set; } public static string PackageCertificatePath { get; set; } diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Exe.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Exe.yaml @@ -0,0 +1,26 @@ +PackageIdentifier: AppInstallerTest.TestZipInstallerWithExe +PackageVersion: 1.0.0.0 +PackageName: TestZipInstallerWithExe +PackageLocale: en-US +Publisher: AppInstallerTest +License: Test +ShortDescription: E2E test for installing a zip with exe. +Installers: + - Architecture: x64 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestZipInstaller/AppInstallerTestZipInstaller.zip + InstallerType: zip + ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' + InstallerSha256: <ZIPHASH> + NestedInstallerType: exe + NestedInstallerFiles: + - RelativeFilePath: AppInstallerTestExeInstaller.exe + InstallerSwitches: + Custom: /execustom + SilentWithProgress: /exeswp + Silent: /exesilent + Interactive: /exeinteractive + Language: /exeenus + Log: /LogFile <LOGPATH> + InstallLocation: /InstallDir <INSTALLPATH> +ManifestType: singleton +ManifestVersion: 1.3.0+ \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Msi.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Msi.yaml @@ -0,0 +1,18 @@ +PackageIdentifier: AppInstallerTest.TestZipInstallerWithMsi +PackageVersion: 1.0.0.0 +PackageName: TestZipInstallerWithMsi +PackageLocale: en-US +Publisher: AppInstallerTest +License: Test +ShortDescription: E2E test for installing a zip with msi. +Installers: + - Architecture: x64 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestZipInstaller/AppInstallerTestZipInstaller.zip + InstallerType: zip + ProductCode: '{A5D36CF1-1993-4F63-BFB4-3ACD910D36A1}' + InstallerSha256: <ZIPHASH> + NestedInstallerType: msi + NestedInstallerFiles: + - RelativeFilePath: AppInstallerTestMsiInstaller.msi +ManifestType: singleton +ManifestVersion: 1.3.0+ \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Msix.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Msix.yaml @@ -0,0 +1,18 @@ +PackageIdentifier: AppInstallerTest.TestZipInstallerWithMsix +PackageVersion: 1.0.0.0 +PackageName: TestZipInstallerWithMsix +PackageLocale: en-US +Publisher: AppInstallerTest +License: Test +ShortDescription: E2E test for installing a zip with msix. +Installers: + - Architecture: x64 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestZipInstaller/AppInstallerTestZipInstaller.zip + InstallerType: zip + ProductCode: '{A5D36CF1-1993-4F63-BFB4-3ACD910D36A1}' + InstallerSha256: <ZIPHASH> + NestedInstallerType: msix + NestedInstallerFiles: + - RelativeFilePath: AppInstallerTestMsixInstaller.msix +ManifestType: singleton +ManifestVersion: 1.3.0+ \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestHashHelper.cs b/src/AppInstallerCLIE2ETests/TestHashHelper.cs @@ -15,6 +15,8 @@ namespace AppInstallerCLIE2ETests public static string MsixInstallerHashValue { get; set; } + public static string ZipInstallerHashValue { get; set; } + public static string SignatureHashValue { get; set; } public static void HashInstallers() @@ -34,6 +36,11 @@ namespace AppInstallerCLIE2ETests MsixInstallerHashValue = HashFile(TestCommon.MsixInstallerPath); SignatureHashValue = HashSignatureFromMSIX(TestCommon.MsixInstallerPath); } + + if (!string.IsNullOrEmpty(TestCommon.ZipInstallerPath)) + { + ZipInstallerHashValue = HashFile(TestCommon.ZipInstallerPath); + } } /// <summary> @@ -71,6 +78,11 @@ namespace AppInstallerCLIE2ETests File.WriteAllText(file.FullName, text); } + else if (text.Contains("<ZIPHASH>")) + { + text = text.Replace("<ZIPHASH>", ZipInstallerHashValue); + File.WriteAllText(file.FullName, text); + } } } diff --git a/src/AppInstallerCLIE2ETests/TestIndexSetup.cs b/src/AppInstallerCLIE2ETests/TestIndexSetup.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; using System.IO; +using System.IO.Compression; using Microsoft.Msix.Utils.ProcessRunner; namespace AppInstallerCLIE2ETests @@ -41,6 +42,8 @@ namespace AppInstallerCLIE2ETests CopyMsixInstallerToTestDirectory(); } + CreateZipInstallerInTestDirectory(); + TestHashHelper.HashInstallers(); string manifestDirectoryPath = Path.Combine(TestCommon.StaticFileRootPath, ManifestsName); @@ -90,7 +93,7 @@ namespace AppInstallerCLIE2ETests // Set Exe Test Installer Path string exeInstallerDestPath = Path.Combine(TestCommon.StaticFileRootPath, Constants.ExeInstaller); DirectoryInfo exeInstallerDestDir = Directory.CreateDirectory(exeInstallerDestPath); - string exeInstallerFullName = Path.Combine(exeInstallerDestDir.FullName, "AppInstallerTestExeInstaller.exe"); + string exeInstallerFullName = Path.Combine(exeInstallerDestDir.FullName, Constants.ExeInstallerFileName); // Copy Exe Test Installer to Destination Path File.Copy(TestCommon.ExeInstallerPath, exeInstallerFullName, true); @@ -107,7 +110,7 @@ namespace AppInstallerCLIE2ETests DirectoryInfo msiInstallerDestDir = Directory.CreateDirectory(msiInstallerDestPath); // Copy MSI Test Installer to Destination Path - string msiInstallerFullName = Path.Combine(msiInstallerDestDir.FullName, "AppInstallerTestMsiInstaller.msi"); + string msiInstallerFullName = Path.Combine(msiInstallerDestDir.FullName, Constants.MsiInstallerFileName); File.Copy(TestCommon.MsiInstallerPath, msiInstallerFullName, true); TestCommon.MsiInstallerPath = msiInstallerFullName; @@ -123,7 +126,7 @@ namespace AppInstallerCLIE2ETests DirectoryInfo msixInstallerDestDir = Directory.CreateDirectory(msixInstallerDestPath); // Copy Msix Test Installer to Destination Path - string msixInstallerFullName = Path.Combine(msixInstallerDestDir.FullName, "AppInstallerTestMsixInstaller.msix"); + string msixInstallerFullName = Path.Combine(msixInstallerDestDir.FullName, Constants.MsixInstallerFileName); File.Copy(TestCommon.MsixInstallerPath, msixInstallerFullName, true); TestCommon.MsixInstallerPath = msixInstallerFullName; @@ -132,6 +135,35 @@ namespace AppInstallerCLIE2ETests SignFile(TestCommon.MsixInstallerPath); } + private static void CreateZipInstallerInTestDirectory() + { + DirectoryInfo zipInstallerDir = Directory.CreateDirectory(Path.Combine(TestCommon.StaticFileRootPath, Constants.ZipInstaller)); + string zipSourceDirFullPath = Directory.CreateDirectory(TestCommon.GetRandomTestDir()).FullName; + + string exeInstallerSourceDestPath = Path.Combine(zipSourceDirFullPath, Constants.ExeInstallerFileName); + string msiInstallerSourceDestPath = Path.Combine(zipSourceDirFullPath, Constants.MsiInstallerFileName); + string msixInstallerSourceDestPath = Path.Combine(zipSourceDirFullPath, Constants.MsixInstallerFileName); + + if (File.Exists(TestCommon.ExeInstallerPath)) + { + File.Copy(TestCommon.ExeInstallerPath, exeInstallerSourceDestPath, true); + } + + if (File.Exists(TestCommon.MsiInstallerPath)) + { + File.Copy(TestCommon.MsiInstallerPath, msiInstallerSourceDestPath, true); + } + + if (File.Exists(TestCommon.MsixInstallerPath)) + { + File.Copy(TestCommon.MsixInstallerPath, msixInstallerSourceDestPath, true); + } + + string destArchiveFullPath = Path.Combine(zipInstallerDir.FullName, Constants.ZipInstallerFileName); ; + ZipFile.CreateFromDirectory(zipSourceDirFullPath, destArchiveFullPath); + TestCommon.ZipInstallerPath = destArchiveFullPath; + } + private static void SetupLocalTestDirectory(string staticFileRootPath) { DirectoryInfo staticFileRootDir = Directory.CreateDirectory(staticFileRootPath); diff --git a/src/AppInstallerCLIE2ETests/UpgradeCommand.cs b/src/AppInstallerCLIE2ETests/UpgradeCommand.cs @@ -13,12 +13,6 @@ namespace AppInstallerCLIE2ETests private const string WinGetPackageIdentifier = "WinGetPackageIdentifier"; private const string WinGetSourceIdentifier = "WinGetSourceIdentifier"; - [OneTimeSetUp] - public void OneTimeSetup() - { - ConfigureFeature("portableInstall", true); - } - [Test] public void UpgradePortable() { diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -1361,4 +1361,13 @@ Please specify one of them using the `--source` option to proceed.</value> <data name="UnsupportedArgument" xml:space="preserve"> <value>A provided argument is not supported for this package</value> </data> + <data name="PortableInstallFromArchiveNotSupported" xml:space="preserve"> + <value>Installing a portable package from an archive is not yet supported</value> + </data> + <data name="ExtractArchiveFailed" xml:space="preserve"> + <value>Failed to extract the contents of the archive</value> + </data> + <data name="NestedInstallerNotFound" xml:space="preserve"> + <value>Nested installer file does not exist. Ensure the specified relative path of the nested installer matches: </value> + </data> </root> \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -186,6 +186,7 @@ <ClInclude Include="TestSource.h" /> </ItemGroup> <ItemGroup> + <ClCompile Include="Archive.cpp" /> <ClCompile Include="ARPChanges.cpp" /> <ClCompile Include="Command.cpp" /> <ClCompile Include="Completion.cpp" /> @@ -283,6 +284,9 @@ <CopyFileToFolders Include="TestData\InstallFlowTest_UnsupportedArguments.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\InstallFlowTest_ZipWithExe.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\ImportFile-Bad-Invalid.json"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> @@ -342,6 +346,9 @@ <CopyFileToFolders Include="TestData\TestSignedApp.msix"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\TestZip.zip"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> <None Include="Run-TestsInPackage.ps1" /> <CopyFileToFolders Include="TestData\Manifest-Bad-ArchInvalid.yaml"> <DeploymentContent>true</DeploymentContent> @@ -601,6 +608,9 @@ <CopyFileToFolders Include="TestData\UpdateFlowTest_Portable.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\UpdateFlowTest_ZipWithExe.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\InputNames.txt"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -209,6 +209,9 @@ <ClCompile Include="YamlManifest.cpp"> <Filter>Source Files\Common</Filter> </ClCompile> + <ClCompile Include="Archive.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> @@ -465,6 +468,9 @@ <CopyFileToFolders Include="TestData\TestSignedApp.msix"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\TestZip.zip"> + <Filter>TestData</Filter> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\InstallFlowTest_NoApplicableArchitecture.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> @@ -501,6 +507,9 @@ <CopyFileToFolders Include="TestData\InstallFlowTest_UnsupportedArguments.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\InstallFlowTest_ZipWithExe.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\InstallerArgTest_Msi_WithSwitches.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> @@ -540,6 +549,9 @@ <CopyFileToFolders Include="TestData\UpdateFlowTest_Portable.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\UpdateFlowTest_ZipWithExe.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\ImportFile-Good.json"> <Filter>TestData</Filter> </CopyFileToFolders> diff --git a/src/AppInstallerCLITests/Archive.cpp b/src/AppInstallerCLITests/Archive.cpp @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include <winget/Archive.h> + +using namespace AppInstaller::Archive; +using namespace TestCommon; + +constexpr std::string_view s_ZipFile = "TestZip.zip"; + +TEST_CASE("Extract_ZipArchive", "[archive]") +{ + TestCommon::TempDirectory tempDirectory("TempDirectory"); + TestDataFile testZip(s_ZipFile); + + const auto& testZipPath = testZip.GetPath(); + const auto& tempDirectoryPath = tempDirectory.GetPath(); + + HRESULT hr = TryExtractArchive(testZipPath, tempDirectoryPath); + + REQUIRE(SUCCEEDED(hr)); + REQUIRE(std::filesystem::exists(tempDirectoryPath / "test.txt")); +}+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_ZipWithExe.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_ZipWithExe.yaml @@ -0,0 +1,23 @@ +PackageIdentifier: AppInstallerCliTest.TestZipInstaller +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: AppInstaller Test Zip Installer +ShortDescription: AppInstaller Test Zip Installer with exe +Publisher: Microsoft Corporation +Moniker: AICLITestZip +License: Test +Installers: + - Architecture: x86 + InstallerUrl: https://ThisIsNotUsed + InstallerType: zip + InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B + NestedInstallerType: exe + NestedInstallerFiles: + - RelativeFilePath: relativeFilePath + InstallerSwitches: + Custom: /custom /scope=machine + SilentWithProgress: /silentwithprogress + Silent: /silence + Update: /update +ManifestType: singleton +ManifestVersion: 1.3.0+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/TestZip.zip b/src/AppInstallerCLITests/TestData/TestZip.zip Binary files differ. diff --git a/src/AppInstallerCLITests/TestData/UpdateFlowTest_ZipWithExe.yaml b/src/AppInstallerCLITests/TestData/UpdateFlowTest_ZipWithExe.yaml @@ -0,0 +1,24 @@ +# Same content with InstallFlowTest_ZipWithExe.yaml but with higher version +PackageIdentifier: AppInstallerCliTest.TestZipInstaller +PackageVersion: 2.0.0.0 +PackageLocale: en-US +PackageName: AppInstaller Test Zip Installer +ShortDescription: AppInstaller Test Zip Installer with exe +Publisher: Microsoft Corporation +Moniker: AICLITestZip +License: Test +Installers: + - Architecture: x86 + InstallerUrl: https://ThisIsNotUsed + InstallerType: zip + InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B + NestedInstallerType: exe + NestedInstallerFiles: + - RelativeFilePath: relativeFilePath + InstallerSwitches: + Custom: /custom /ver2.0.0.0 + SilentWithProgress: /silentwithprogress + Silent: /silence + Update: /update +ManifestType: singleton +ManifestVersion: 1.3.0+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp @@ -15,6 +15,7 @@ #include <Workflows/InstallFlow.h> #include <Workflows/MsiInstallFlow.h> #include <Workflows/PortableFlow.h> +#include <Workflows/ArchiveFlow.h> #include <Workflows/UninstallFlow.h> #include <Workflows/UpdateFlow.h> #include <Workflows/DependenciesFlow.h> @@ -182,6 +183,21 @@ namespace PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestMsixInstaller"))); } + if (input.empty() || input == "AppInstallerCliTest.TestZipInstaller") + { + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_ZipWithExe.yaml")); + auto manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_ZipWithExe.yaml")); + result.Matches.emplace_back( + ResultMatch( + TestPackage::Make( + manifest, + TestPackage::MetadataMap{ { PackageVersionMetadata::InstalledType, "Zip" } }, + std::vector<Manifest>{ manifest2, manifest }, + shared_from_this() + ), + PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestZipInstaller"))); + } + if (input.empty() || input == "AppInstallerCliTest.TestMSStoreInstaller") { auto installed = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_MSStore.yaml")); @@ -606,6 +622,31 @@ void OverrideForEnsureSupportForPortable(TestContext& context) } }); } +void OverrideForArchiveInstall(TestContext& context) +{ + context.Override({ ExtractFilesFromArchive, [](TestContext&) + { + } }); + + context.Override({ VerifyAndSetNestedInstaller, [](TestContext&) + { + } }); +} + +void OverrideForExtractInstallerFromArchive(TestContext& context) +{ + context.Override({ ExtractFilesFromArchive, [](TestContext&) + { + } }); +} + +void OverrideForVerifyAndSetNestedInstaller(TestContext& context) +{ + context.Override({ VerifyAndSetNestedInstaller, [](TestContext&) + { + } }); +} + void OverrideForDirectMsi(TestContext& context) { OverrideForCheckExistingInstaller(context); @@ -944,6 +985,69 @@ TEST_CASE("InstallFlowWithNonApplicableArchitecture", "[InstallFlow][workflow]") REQUIRE(!std::filesystem::exists(installResultPath.GetPath())); } +TEST_CASE("InstallFlow_ZipWithExe", "[InstallFlow][workflow]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + OverrideForShellExecute(context); + OverrideForExtractInstallerFromArchive(context); + OverrideForVerifyAndSetNestedInstaller(context); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_ZipWithExe.yaml").GetPath().u8string()); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + // 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("InstallFlow_Zip_BadRelativePath", "[InstallFlow][workflow]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + OverrideForShellExecute(context); + OverrideForExtractInstallerFromArchive(context); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_ZipWithExe.yaml").GetPath().u8string()); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_NESTEDINSTALLER_NOT_FOUND); + + // Verify Installer was not called + REQUIRE(!std::filesystem::exists(installResultPath.GetPath())); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::NestedInstallerNotFound).get()) != std::string::npos); +} + +TEST_CASE("ExtractInstallerFromArchive_InvalidZip", "[InstallFlow][workflow]") +{ + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_ZipWithExe.yaml")); + context.Add<Data::Manifest>(manifest); + context.Add<Data::Installer>(manifest.Installers.at(0)); + // Provide an invalid zip file which should be handled appropriately. + context.Add<Data::InstallerPath>(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("MSStoreInstallFlowWithTestManifest", "[InstallFlow][workflow]") { TestCommon::TempFile installResultPath("TestMSStoreInstalled.txt"); @@ -1601,6 +1705,35 @@ TEST_CASE("UpdateFlow_UpdateExe", "[UpdateFlow][workflow]") REQUIRE(updateResultStr.find("/ver3.0.0.0") != std::string::npos); } +TEST_CASE("UpdateFlow_UpdateZipWithExe", "[UpdateFlow][workflow]") +{ + TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); + + std::ostringstream updateOutput; + TestContext context{ updateOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + OverrideForCompositeInstalledSource(context); + OverrideForShellExecute(context); + OverrideForExtractInstallerFromArchive(context); + OverrideForVerifyAndSetNestedInstaller(context); + context.Args.AddArg(Execution::Args::Type::Query, "AppInstallerCliTest.TestZipInstaller"sv); + context.Args.AddArg(Execution::Args::Type::Silent); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + + // Verify Installer is called and parameters are passed in. + REQUIRE(std::filesystem::exists(updateResultPath.GetPath())); + std::ifstream updateResultFile(updateResultPath.GetPath()); + REQUIRE(updateResultFile.is_open()); + std::string updateResultStr; + std::getline(updateResultFile, updateResultStr); + REQUIRE(updateResultStr.find("/custom") != std::string::npos); + REQUIRE(updateResultStr.find("/silence") != std::string::npos); + REQUIRE(updateResultStr.find("/ver2.0.0.0") != std::string::npos); +} + TEST_CASE("UpdateFlow_UpdatePortable", "[UpdateFlow][workflow]") { TestCommon::TempFile updateResultPath("TestPortableInstalled.txt"); @@ -2152,7 +2285,7 @@ TEST_CASE("ExportFlow_ExportAll", "[ExportFlow][workflow]") REQUIRE(exportedCollection.Sources[0].Details.Identifier == "*TestSource"); const auto& exportedPackages = exportedCollection.Sources[0].Packages; - REQUIRE(exportedPackages.size() == 4); + REQUIRE(exportedPackages.size() == 5); REQUIRE(exportedPackages.end() != std::find_if(exportedPackages.begin(), exportedPackages.end(), [](const auto& p) { return p.Id == "AppInstallerCliTest.TestExeInstaller" && p.VersionAndChannel.GetVersion().ToString().empty(); @@ -2169,6 +2302,10 @@ TEST_CASE("ExportFlow_ExportAll", "[ExportFlow][workflow]") { return p.Id == "AppInstallerCliTest.TestPortableInstaller" && p.VersionAndChannel.GetVersion().ToString().empty(); })); + REQUIRE(exportedPackages.end() != std::find_if(exportedPackages.begin(), exportedPackages.end(), [](const auto& p) + { + return p.Id == "AppInstallerCliTest.TestZipInstaller" && p.VersionAndChannel.GetVersion().ToString().empty(); + })); } TEST_CASE("ExportFlow_ExportAll_WithVersions", "[ExportFlow][workflow]") @@ -2192,7 +2329,7 @@ TEST_CASE("ExportFlow_ExportAll_WithVersions", "[ExportFlow][workflow]") REQUIRE(exportedCollection.Sources[0].Details.Identifier == "*TestSource"); const auto& exportedPackages = exportedCollection.Sources[0].Packages; - REQUIRE(exportedPackages.size() == 4); + REQUIRE(exportedPackages.size() == 5); REQUIRE(exportedPackages.end() != std::find_if(exportedPackages.begin(), exportedPackages.end(), [](const auto& p) { return p.Id == "AppInstallerCliTest.TestExeInstaller" && p.VersionAndChannel.GetVersion().ToString() == "1.0.0.0"; @@ -2209,6 +2346,10 @@ TEST_CASE("ExportFlow_ExportAll_WithVersions", "[ExportFlow][workflow]") { return p.Id == "AppInstallerCliTest.TestPortableInstaller" && p.VersionAndChannel.GetVersion().ToString() == "1.0.0.0"; })); + REQUIRE(exportedPackages.end() != std::find_if(exportedPackages.begin(), exportedPackages.end(), [](const auto& p) + { + return p.Id == "AppInstallerCliTest.TestZipInstaller" && p.VersionAndChannel.GetVersion().ToString() == "1.0.0.0"; + })); } TEST_CASE("ImportFlow_Successful", "[ImportFlow][workflow]") diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -303,6 +303,7 @@ <ClInclude Include="Public\AppInstallerLogging.h" /> <ClInclude Include="Public\AppInstallerArchitecture.h" /> <ClInclude Include="Public\AppInstallerVersions.h" /> + <ClInclude Include="Public\winget\Archive.h" /> <ClInclude Include="Public\winget\ExperimentalFeature.h" /> <ClInclude Include="Public\winget\ExtensionCatalog.h" /> <ClInclude Include="Public\winget\JsonSchemaValidation.h" /> @@ -389,6 +390,7 @@ <PrecompiledHeader>Create</PrecompiledHeader> </ClCompile> <ClCompile Include="AppInstallerTelemetry.cpp" /> + <ClCompile Include="Archive.cpp" /> <ClCompile Include="Settings.cpp" /> <ClCompile Include="SHA256.cpp" /> <ClCompile Include="PortableARPEntry.cpp" /> diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -201,6 +201,9 @@ <ClInclude Include="Public\winget\MsixManifest.h"> <Filter>Header Files</Filter> </ClInclude> + <ClInclude Include="Public\winget\Archive.h"> + <Filter>Public\winget</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -350,6 +353,9 @@ <ClCompile Include="MsixManifest.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="Archive.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCommonCore/Archive.cpp b/src/AppInstallerCommonCore/Archive.cpp @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" + +namespace AppInstaller::Archive +{ + using unique_pidlist_absolute = wil::unique_any<PIDLIST_ABSOLUTE, decltype(&::CoTaskMemFree), ::CoTaskMemFree>; + using unique_lpitemidlist = wil::unique_any<LPITEMIDLIST, decltype(&::CoTaskMemFree), ::CoTaskMemFree>; + + HRESULT TryExtractArchive(const std::filesystem::path& archivePath, const std::filesystem::path& destPath) + { + wil::com_ptr<IFileOperation> pFileOperation; + RETURN_IF_FAILED(CoCreateInstance(CLSID_FileOperation, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pFileOperation))); + RETURN_IF_FAILED(pFileOperation->SetOperationFlags(FOF_NO_UI)); + + wil::com_ptr<IShellItem> pShellItemTo; + RETURN_IF_FAILED(SHCreateItemFromParsingName(destPath.c_str(), NULL, IID_PPV_ARGS(&pShellItemTo))); + + unique_pidlist_absolute pidlFull; + RETURN_IF_FAILED(SHParseDisplayName(archivePath.c_str(), NULL, &pidlFull, 0, NULL)); + + wil::com_ptr<IShellFolder> pArchiveShellFolder; + RETURN_IF_FAILED(SHBindToObject(NULL, pidlFull.get(), NULL, IID_PPV_ARGS(&pArchiveShellFolder))); + + wil::com_ptr<IEnumIDList> pEnumIdList; + RETURN_IF_FAILED(pArchiveShellFolder->EnumObjects(nullptr, SHCONTF_FOLDERS | SHCONTF_NONFOLDERS, &pEnumIdList)); + + unique_lpitemidlist pidlChild; + ULONG nFetched; + while (pEnumIdList->Next(1, wil::out_param_ptr<LPITEMIDLIST*>(pidlChild), &nFetched) == S_OK && nFetched == 1) + { + wil::com_ptr<IShellItem> pShellItemFrom; + STRRET strFolderName; + WCHAR szFolderName[MAX_PATH]; + RETURN_IF_FAILED(pArchiveShellFolder->GetDisplayNameOf(pidlChild.get(), SHGDN_INFOLDER, &strFolderName)); + RETURN_IF_FAILED(StrRetToBuf(&strFolderName, pidlChild.get(), szFolderName, MAX_PATH)); + RETURN_IF_FAILED(SHCreateItemWithParent(pidlFull.get(), pArchiveShellFolder.get(), pidlChild.get(), IID_PPV_ARGS(&pShellItemFrom))); + RETURN_IF_FAILED(pFileOperation->CopyItem(pShellItemFrom.get(), pShellItemTo.get(), NULL, NULL)); + } + + RETURN_IF_FAILED(pFileOperation->PerformOperations()); + return S_OK; + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Errors.cpp b/src/AppInstallerCommonCore/Errors.cpp @@ -222,6 +222,8 @@ namespace AppInstaller return "Failed to install package dependencies."; case APPINSTALLER_CLI_ERROR_BIND_WITH_EMBEDDED_NULL: return "Embedded null characters are disallowed for SQLite"; + case APPINSTALLER_CLI_ERROR_NESTEDINSTALLER_NOT_FOUND: + return "Failed to find the nested installer in the archive."; default: return "Unknown Error Code"; } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -103,6 +103,8 @@ #define APPINSTALLER_CLI_ERROR_ARP_VERSION_VALIDATION_FAILED ((HRESULT)0x8A150058) #define APPINSTALLER_CLI_ERROR_UNSUPPORTED_ARGUMENT ((HRESULT)0x8A150059) #define APPINSTALLER_CLI_ERROR_BIND_WITH_EMBEDDED_NULL ((HRESULT)0x8A15005A) +#define APPINSTALLER_CLI_ERROR_NESTEDINSTALLER_NOT_FOUND ((HRESULT)0x8A15005B) +#define APPINSTALLER_CLI_ERROR_EXTRACT_ARCHIVE_FAILED ((HRESULT)0x8A15005C) // Install errors. #define APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE ((HRESULT)0x8A150101) diff --git a/src/AppInstallerCommonCore/Public/winget/Archive.h b/src/AppInstallerCommonCore/Public/winget/Archive.h @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" + +namespace AppInstaller::Archive +{ + HRESULT TryExtractArchive(const std::filesystem::path& archivePath, const std::filesystem::path& destPath); +}+ \ No newline at end of file