From b0eae014154e09ed5208abc526214fc23bb2bd86 Mon Sep 17 00:00:00 2001 From: Ryan Fu Date: Mon, 29 Aug 2022 18:31:04 -0700 Subject: [PATCH 01/22] Refactor install for portable --- .../ExecutionContextData.h | 7 + .../Workflows/ArchiveFlow.cpp | 22 +- .../Workflows/InstallFlow.cpp | 2 + .../Workflows/PortableFlow.cpp | 307 +++++++----------- .../Workflows/PortableFlow.h | 10 + .../Workflows/UninstallFlow.cpp | 3 +- src/AppInstallerCLITests/Archive.cpp | 4 +- src/AppInstallerCLITests/PortableEntry.cpp | 52 ++- src/AppInstallerCommonCore/Archive.cpp | 3 +- src/AppInstallerCommonCore/PortableEntry.cpp | 158 ++++++++- .../Public/winget/Archive.h | 2 +- .../Public/winget/PortableEntry.h | 50 ++- 12 files changed, 373 insertions(+), 247 deletions(-) diff --git a/src/AppInstallerCLICore/ExecutionContextData.h b/src/AppInstallerCLICore/ExecutionContextData.h index becf945155..1cffc15ead 100644 --- a/src/AppInstallerCLICore/ExecutionContextData.h +++ b/src/AppInstallerCLICore/ExecutionContextData.h @@ -56,6 +56,7 @@ namespace AppInstaller::CLI::Execution AllowedArchitectures, PortableEntry, AllowUnknownScope, + ExtractedItems, Max }; @@ -230,5 +231,11 @@ namespace AppInstaller::CLI::Execution { using value_t = bool; }; + + template <> + struct DataMapping + { + using value_t = std::vector; + }; } } diff --git a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp index 3f11a0430d..53133832bf 100644 --- a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp @@ -4,6 +4,7 @@ #include "ArchiveFlow.h" #include "winget/Archive.h" #include "winget/Filesystem.h" +#include "PortableFlow.h" using namespace AppInstaller::Manifest; @@ -12,11 +13,24 @@ namespace AppInstaller::CLI::Workflow void ExtractFilesFromArchive(Execution::Context& context) { const auto& installerPath = context.Get(); - const auto& installerParentPath = installerPath.parent_path(); + std::filesystem::path destinationFolder; + std::vector extractedItems; + + HRESULT hr; + if (context.Get()->NestedInstallerType == InstallerTypeEnum::Portable) + { + destinationFolder = GetPortableTargetDirectory(context); + hr = AppInstaller::Archive::TryExtractArchive(installerPath, destinationFolder, extractedItems); + context.Add(extractedItems); + } + else + { + destinationFolder = installerPath.parent_path(); + hr = AppInstaller::Archive::TryExtractArchive(installerPath, destinationFolder, extractedItems); + + } - // 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); + AICLI_LOG(CLI, Info, << "Extracting archive to: " << destinationFolder); if (SUCCEEDED(hr)) { diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp index d0af3ef715..75a00f2d1b 100644 --- a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -334,6 +334,8 @@ namespace AppInstaller::CLI::Workflow void PortableInstall(Execution::Context& context) { context << + GetPortableInstallInfo << + VerifyPortableRegistryMatch << PortableInstallImpl << ReportInstallerResult("Portable"sv, APPINSTALLER_CLI_ERROR_PORTABLE_INSTALL_FAILED, true); } diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp index 85ae72b71a..732117035e 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -24,14 +24,6 @@ namespace AppInstaller::CLI::Workflow { constexpr std::string_view s_DefaultSource = "*DefaultSource"sv; - void AppendExeExtension(std::filesystem::path& value) - { - if (value.extension() != ".exe") - { - value += ".exe"; - } - } - std::string GetPortableProductCode(Execution::Context& context) { const std::string& packageId = context.Get().Id; @@ -80,74 +72,7 @@ namespace AppInstaller::CLI::Workflow } } - std::filesystem::path GetPortableTargetDirectory(Execution::Context& context) - { - Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); - Utility::Architecture arch = context.Get()->Arch; - std::string_view locationArg = context.Args.GetArg(Execution::Args::Type::InstallLocation); - std::filesystem::path targetInstallDirectory; - - if (!locationArg.empty()) - { - targetInstallDirectory = std::filesystem::path{ ConvertToUTF16(locationArg) }; - } - else - { - const std::string& productCode = GetPortableProductCode(context); - targetInstallDirectory = GetPortableInstallRoot(scope, arch); - targetInstallDirectory /= ConvertToUTF16(productCode); - } - - return targetInstallDirectory; - } - std::filesystem::path GetPortableTargetFullPath(Execution::Context& context) - { - const std::filesystem::path& installerPath = context.Get(); - const std::filesystem::path& targetInstallDirectory = GetPortableTargetDirectory(context); - std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); - - std::filesystem::path fileName; - if (!renameArg.empty()) - { - fileName = ConvertToUTF16(renameArg); - } - else - { - fileName = installerPath.filename(); - } - - AppendExeExtension(fileName); - return targetInstallDirectory / fileName; - } - - std::filesystem::path GetPortableSymlinkFullPath(Execution::Context& context) - { - const std::filesystem::path& installerPath = context.Get(); - const std::vector& commands = context.Get()->Commands; - Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); - std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); - - std::filesystem::path commandAlias; - if (!renameArg.empty()) - { - commandAlias = ConvertToUTF16(renameArg); - } - else - { - if (!commands.empty()) - { - commandAlias = ConvertToUTF16(commands[0]); - } - else - { - commandAlias = installerPath.filename(); - } - } - - AppendExeExtension(commandAlias); - return GetPortableLinksLocation(scope) / commandAlias; - } Manifest::AppsAndFeaturesEntry GetAppsAndFeaturesEntryForPortableInstall( const std::vector& appsAndFeaturesEntries, @@ -175,56 +100,6 @@ namespace AppInstaller::CLI::Workflow return appsAndFeaturesEntry; } - void InitializePortableARPEntry(Execution::Context& context) - { - const std::string& packageIdentifier = context.Get().Id; - - std::string sourceIdentifier; - if (context.Contains(Execution::Data::PackageVersion)) - { - sourceIdentifier = context.Get()->GetSource().GetIdentifier(); - } - else - { - sourceIdentifier = s_DefaultSource; - } - - Portable::PortableEntry& portableEntry = context.Get(); - - if (portableEntry.Exists()) - { - if (packageIdentifier != portableEntry.WinGetPackageIdentifier || sourceIdentifier != portableEntry.WinGetSourceIdentifier) - { - // TODO: Replace HashOverride with --Force when argument behavior gets updated. - if (!context.Args.Contains(Execution::Args::Type::HashOverride)) - { - AICLI_LOG(CLI, Error, << "Registry match failed, skipping write to uninstall registry"); - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS); - } - else - { - AICLI_LOG(CLI, Info, << "Overriding registry match check..."); - context.Reporter.Warn() << Resource::String::PortableRegistryCollisionOverridden << std::endl; - } - } - } - - portableEntry.Commit(PortableValueName::WinGetPackageIdentifier, portableEntry.WinGetPackageIdentifier = packageIdentifier); - portableEntry.Commit(PortableValueName::WinGetSourceIdentifier, portableEntry.WinGetSourceIdentifier = sourceIdentifier); - portableEntry.Commit(PortableValueName::UninstallString, portableEntry.UninstallString = "winget uninstall --product-code " + GetPortableProductCode(context)); - portableEntry.Commit(PortableValueName::WinGetInstallerType, portableEntry.WinGetInstallerType = InstallerTypeToString(InstallerTypeEnum::Portable)); - } - - void MovePortableExe(Execution::Context& context) - { - Portable::PortableEntry& portableEntry = context.Get(); - const std::filesystem::path& installerPath = context.Get(); - portableEntry.Commit(PortableValueName::PortableTargetFullPath, portableEntry.PortableTargetFullPath = GetPortableTargetFullPath(context)); - portableEntry.Commit(PortableValueName::InstallLocation, portableEntry.InstallLocation = GetPortableTargetDirectory(context)); - portableEntry.Commit(PortableValueName::SHA256, portableEntry.SHA256 = SHA256::ConvertToString(context.Get().second)); - portableEntry.MovePortableExe(installerPath); - } - void RemovePortableExe(Execution::Context& context) { Portable::PortableEntry& portableEntry = context.Get(); @@ -299,49 +174,6 @@ namespace AppInstaller::CLI::Workflow } } - void CreatePortableSymlink(Execution::Context& context) - { - Portable::PortableEntry& portableEntry = context.Get(); - if (portableEntry.InstallDirectoryAddedToPath) - { - AICLI_LOG(CLI, Info, << "Package directory was previously added to PATH. Skipping symlink creation."); - return; - } - - const std::filesystem::path& symlinkFullPath = GetPortableSymlinkFullPath(context); - portableEntry.Commit(PortableValueName::PortableSymlinkFullPath, portableEntry.PortableSymlinkFullPath = symlinkFullPath); - - std::filesystem::file_status status = std::filesystem::status(symlinkFullPath); - if (std::filesystem::is_directory(status)) - { - AICLI_LOG(CLI, Info, << "Unable to create symlink. '" << symlinkFullPath << "points to an existing directory."); - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY); - } - - if (std::filesystem::remove(symlinkFullPath)) - { - AICLI_LOG(CLI, Info, << "Removed existing file at " << symlinkFullPath); - context.Reporter.Warn() << Resource::String::OverwritingExistingFileAtMessage << ' ' << symlinkFullPath.u8string() << std::endl; - } - - portableEntry.CreatePortableSymlink(); - } - - void AddToPathVariable(Execution::Context& context) - { - Portable::PortableEntry& portableEntry = context.Get(); - const std::filesystem::path& pathValue = portableEntry.GetPathValue(); - if (portableEntry.AddToPathVariable()) - { - AICLI_LOG(CLI, Info, << "Appended target directory to PATH registry: " << pathValue); - context.Reporter.Warn() << Resource::String::ModifiedPathRequiresShellRestart << std::endl; - } - else - { - AICLI_LOG(CLI, Info, << "Target directory already exists in PATH registry: " << pathValue); - } - } - void RemoveFromPathVariable(Execution::Context& context) { Portable::PortableEntry& portableEntry = context.Get(); @@ -386,20 +218,6 @@ namespace AppInstaller::CLI::Workflow portableEntry.RemoveARPEntry(); AICLI_LOG(CLI, Info, << "PortableARPEntry deleted."); } - - void CommitPortableMetadataToRegistry(Execution::Context& context) - { - Portable::PortableEntry& portableEntry = context.Get(); - const AppInstaller::Manifest::Manifest& manifest = context.Get(); - const Manifest::AppsAndFeaturesEntry& entry = GetAppsAndFeaturesEntryForPortableInstall(context.Get()->AppsAndFeaturesEntries, manifest); - - portableEntry.Commit(PortableValueName::DisplayName, portableEntry.DisplayName = entry.DisplayName); - portableEntry.Commit(PortableValueName::DisplayVersion, portableEntry.DisplayVersion = entry.DisplayVersion); - portableEntry.Commit(PortableValueName::Publisher, portableEntry.Publisher = entry.Publisher); - portableEntry.Commit(PortableValueName::InstallDate, portableEntry.InstallDate = Utility::GetCurrentDateForARP()); - portableEntry.Commit(PortableValueName::URLInfoAbout, portableEntry.URLInfoAbout = manifest.CurrentLocalization.Get()); - portableEntry.Commit(PortableValueName::HelpLink, portableEntry.HelpLink = manifest.CurrentLocalization.Get < Manifest::Localization::PublisherSupportUrl>()); - } void EnsureValidArgsForPortableInstall(Execution::Context& context) { @@ -442,9 +260,69 @@ namespace AppInstaller::CLI::Workflow } } } + + std::filesystem::path GetPortableTargetDirectory(Execution::Context& context) + { + Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); + Utility::Architecture arch = context.Get()->Arch; + std::string_view locationArg = context.Args.GetArg(Execution::Args::Type::InstallLocation); + std::filesystem::path targetInstallDirectory; + + if (!locationArg.empty()) + { + targetInstallDirectory = std::filesystem::path{ ConvertToUTF16(locationArg) }; + } + else + { + const std::string& productCode = GetPortableProductCode(context); + targetInstallDirectory = GetPortableInstallRoot(scope, arch); + targetInstallDirectory /= ConvertToUTF16(productCode); + } + + return targetInstallDirectory; + } + + void VerifyPortableRegistryMatch(Execution::Context& context) + { + const std::string& packageIdentifier = context.Get().Id; + + std::string sourceIdentifier; + if (context.Contains(Execution::Data::PackageVersion)) + { + sourceIdentifier = context.Get()->GetSource().GetIdentifier(); + } + else + { + sourceIdentifier = s_DefaultSource; + } - void PortableInstallImpl(Execution::Context& context) - { + Portable::PortableEntry& portableEntry = context.Get(); + + if (portableEntry.Exists()) + { + if (packageIdentifier != portableEntry.WinGetPackageIdentifier || sourceIdentifier != portableEntry.WinGetSourceIdentifier) + { + // TODO: Replace HashOverride with --Force when argument behavior gets updated. + if (!context.Args.Contains(Execution::Args::Type::HashOverride)) + { + AICLI_LOG(CLI, Error, << "Registry match failed, skipping write to uninstall registry"); + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS); + } + else + { + AICLI_LOG(CLI, Info, << "Overriding registry match check..."); + context.Reporter.Warn() << Resource::String::PortableRegistryCollisionOverridden << std::endl; + } + } + } + + portableEntry.WinGetPackageIdentifier = packageIdentifier; + portableEntry.WinGetSourceIdentifier = sourceIdentifier; + } + + + void GetPortableInstallInfo(Execution::Context& context) + { Manifest::ScopeEnum scope = Manifest::ScopeEnum::Unknown; bool isUpdate = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate); if (isUpdate) @@ -461,29 +339,62 @@ namespace AppInstaller::CLI::Workflow scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); } - PortableARPEntry uninstallEntry = PortableARPEntry( - scope, - context.Get()->Arch, - GetPortableProductCode(context)); + PortableEntry portableEntry = PortableEntry(scope, context.Get()->Arch, GetPortableProductCode(context), isUpdate); + portableEntry.InstallLocation = GetPortableTargetDirectory(context); + portableEntry.SHA256 = SHA256::ConvertToString(context.Get().second); - PortableEntry portableEntry = PortableEntry(uninstallEntry); - context.Add(std::move(portableEntry)); + const AppInstaller::Manifest::Manifest& manifest = context.Get(); + const Manifest::AppsAndFeaturesEntry& entry = GetAppsAndFeaturesEntryForPortableInstall(context.Get()->AppsAndFeaturesEntries, manifest); + portableEntry.SetAppsAndFeaturesMetadata(entry, manifest); + context.Add(std::move(portableEntry)); + } + + void PortableInstallImpl(Execution::Context& context) + { + Portable::PortableEntry& portableEntry = context.Get(); try { context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl; - context << - InitializePortableARPEntry << - MovePortableExe << - CreatePortableSymlink << - AddToPathVariable << - CommitPortableMetadataToRegistry; + HRESULT result; + const std::filesystem::path& installerPath = context.Get(); - context.Add(context.GetTerminationHR()); + if (IsArchiveType(context.Get()->BaseInstallerType)) + { + const std::vector& extractedItems = context.Get(); + const std::vector& nestedInstallerFiles = context.Get()->NestedInstallerFiles; + result = portableEntry.MultipleInstall(nestedInstallerFiles, extractedItems); + } + else + { + std::filesystem::path commandAlias; + bool rename = false; + std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); + const std::vector& commands = context.Get()->Commands; + + if (!renameArg.empty()) + { + rename = true; + commandAlias = ConvertToUTF16(renameArg); + } + else if (!commands.empty()) + { + commandAlias = ConvertToUTF16(commands[0]); + } + else + { + commandAlias = installerPath.filename(); + } + + result = portableEntry.SingleInstall(installerPath, commandAlias, rename); + } + + context.Add(result); } catch (...) { + // fix this to handle. context.Add(Workflow::HandleException(context, std::current_exception())); } @@ -493,7 +404,7 @@ namespace AppInstaller::CLI::Workflow // Perform cleanup only if the install fails and is not an update. const auto& installReturnCode = context.Get(); - if (installReturnCode != 0 && installReturnCode != APPINSTALLER_CLI_ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS && !isUpdate) + if (installReturnCode != 0 && installReturnCode != APPINSTALLER_CLI_ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS && !portableEntry.IsUpdate()) { context.Reporter.Warn() << Resource::String::PortableInstallFailed << std::endl; auto uninstallPortableContextPtr = context.CreateSubContext(); diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.h b/src/AppInstallerCLICore/Workflows/PortableFlow.h index 1c2b9d8105..7526021791 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.h +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.h @@ -22,4 +22,14 @@ namespace AppInstaller::CLI::Workflow void EnsureSupportForPortableUninstall(Execution::Context& context); void EnsureNonPortableTypeForArchiveInstall(Execution::Context& context); + + // Gets the portable install info from the context. + // Required Args: None + // Inputs: Manifest?, Installer, InstallerPath + // Outputs: InstallerArgs + void GetPortableInstallInfo(Execution::Context& context); + + void VerifyPortableRegistryMatch(Execution::Context& context); + + std::filesystem::path GetPortableTargetDirectory(Execution::Context& context); } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp b/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp index 26aa633ed5..d28d1d4de4 100644 --- a/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp @@ -141,11 +141,10 @@ namespace AppInstaller::CLI::Workflow const std::string installedScope = context.Get()->GetMetadata()[Repository::PackageVersionMetadata::InstalledScope]; const std::string installedArch = context.Get()->GetMetadata()[Repository::PackageVersionMetadata::InstalledArchitecture]; - Registry::Portable::PortableARPEntry uninstallEntry = Registry::Portable::PortableARPEntry( + Portable::PortableEntry portableEntry = Portable::PortableEntry( ConvertToScopeEnum(installedScope), Utility::ConvertToArchitectureEnum(installedArch), productCodes[0]); - Portable::PortableEntry portableEntry = Portable::PortableEntry(uninstallEntry); context.Add(portableEntry); break; diff --git a/src/AppInstallerCLITests/Archive.cpp b/src/AppInstallerCLITests/Archive.cpp index 304f27965b..e1ac99688b 100644 --- a/src/AppInstallerCLITests/Archive.cpp +++ b/src/AppInstallerCLITests/Archive.cpp @@ -17,8 +17,10 @@ TEST_CASE("Extract_ZipArchive", "[archive]") const auto& testZipPath = testZip.GetPath(); const auto& tempDirectoryPath = tempDirectory.GetPath(); - HRESULT hr = TryExtractArchive(testZipPath, tempDirectoryPath); + std::vector extractedItems; + HRESULT hr = TryExtractArchive(testZipPath, tempDirectoryPath, extractedItems); REQUIRE(SUCCEEDED(hr)); REQUIRE(std::filesystem::exists(tempDirectoryPath / "test.txt")); + REQUIRE(extractedItems[0] == "test.txt"); } \ No newline at end of file diff --git a/src/AppInstallerCLITests/PortableEntry.cpp b/src/AppInstallerCLITests/PortableEntry.cpp index df18a69bab..e172fa29b1 100644 --- a/src/AppInstallerCLITests/PortableEntry.cpp +++ b/src/AppInstallerCLITests/PortableEntry.cpp @@ -13,12 +13,11 @@ using namespace TestCommon; TEST_CASE("VerifyPortableMove", "[PortableEntry]") { - PortableARPEntry testARPEntry = PortableARPEntry( + PortableEntry testEntry = PortableEntry( AppInstaller::Manifest::ScopeEnum::User, Architecture::X64, "testProductCode"); - PortableEntry testEntry = PortableEntry(testARPEntry); TestCommon::TempDirectory tempDirectory("TempDirectory", false); testEntry.InstallLocation = tempDirectory.GetPath(); testEntry.PortableTargetFullPath = tempDirectory.GetPath() / "output.txt"; @@ -32,12 +31,10 @@ TEST_CASE("VerifyPortableMove", "[PortableEntry]") REQUIRE(testEntry.InstallDirectoryCreated); // Create a second PortableEntry instance to emulate installing for a second time. (ARP entry should already exist) - PortableARPEntry testARPEntry2 = PortableARPEntry( + PortableEntry testEntry2 = PortableEntry( AppInstaller::Manifest::ScopeEnum::User, Architecture::X64, "testProductCode"); - - PortableEntry testEntry2 = PortableEntry(testARPEntry2); REQUIRE(testEntry2.InstallDirectoryCreated); // InstallDirectoryCreated should already be initialized as true. testEntry2.InstallLocation = tempDirectory.GetPath(); @@ -56,13 +53,11 @@ TEST_CASE("VerifyPortableMove", "[PortableEntry]") TEST_CASE("VerifySymlinkCheck", "[PortableEntry]") { - PortableARPEntry testARPEntry = PortableARPEntry( + PortableEntry testEntry = PortableEntry( AppInstaller::Manifest::ScopeEnum::User, Architecture::X64, "testProductCode"); - PortableEntry testEntry = PortableEntry(testARPEntry); - TestCommon::TempFile testFile("target.txt"); std::ofstream file(testFile.GetPath(), std::ofstream::out); file.close(); @@ -81,24 +76,23 @@ TEST_CASE("VerifySymlinkCheck", "[PortableEntry]") testEntry.RemoveARPEntry(); } -TEST_CASE("VerifyPathVariableModified", "[PortableEntry]") -{ - PortableARPEntry testARPEntry = PortableARPEntry( - AppInstaller::Manifest::ScopeEnum::User, - Architecture::X64, - "testProductCode"); - - PortableEntry testEntry = PortableEntry(testARPEntry); - testEntry.InstallDirectoryAddedToPath = true; - TestCommon::TempDirectory tempDirectory("TempDirectory", false); - const std::filesystem::path& pathValue = tempDirectory.GetPath(); - testEntry.InstallLocation = pathValue; - testEntry.AddToPathVariable(); - - AppInstaller::Registry::Environment::PathVariable pathVariable(AppInstaller::Manifest::ScopeEnum::User); - REQUIRE(pathVariable.Contains(pathValue)); - - testEntry.RemoveFromPathVariable(); - REQUIRE_FALSE(pathVariable.Contains(pathValue)); - testEntry.RemoveARPEntry(); -} \ No newline at end of file +//TEST_CASE("VerifyPathVariableModified", "[PortableEntry]") +//{ +// PortableEntry testEntry = PortableEntry( +// AppInstaller::Manifest::ScopeEnum::User, +// Architecture::X64, +// "testProductCode"); +// +// testEntry.InstallDirectoryAddedToPath = true; +// TestCommon::TempDirectory tempDirectory("TempDirectory", false); +// const std::filesystem::path& pathValue = tempDirectory.GetPath(); +// testEntry.InstallLocation = pathValue; +// testEntry.AddToPathVariable(); +// +// AppInstaller::Registry::Environment::PathVariable pathVariable(AppInstaller::Manifest::ScopeEnum::User); +// REQUIRE(pathVariable.Contains(pathValue)); +// +// testEntry.RemoveFromPathVariable(); +// REQUIRE_FALSE(pathVariable.Contains(pathValue)); +// testEntry.RemoveARPEntry(); +//} \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Archive.cpp b/src/AppInstallerCommonCore/Archive.cpp index 41cbde0a12..57e9af1905 100644 --- a/src/AppInstallerCommonCore/Archive.cpp +++ b/src/AppInstallerCommonCore/Archive.cpp @@ -7,7 +7,7 @@ namespace AppInstaller::Archive using unique_pidlist_absolute = wil::unique_any; using unique_lpitemidlist = wil::unique_any; - HRESULT TryExtractArchive(const std::filesystem::path& archivePath, const std::filesystem::path& destPath) + HRESULT TryExtractArchive(const std::filesystem::path& archivePath, const std::filesystem::path& destPath, std::vector& extractedItems) { wil::com_ptr pFileOperation; RETURN_IF_FAILED(CoCreateInstance(CLSID_FileOperation, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pFileOperation))); @@ -36,6 +36,7 @@ namespace AppInstaller::Archive 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)); + extractedItems.emplace_back(szFolderName); } RETURN_IF_FAILED(pFileOperation->PerformOperations()); diff --git a/src/AppInstallerCommonCore/PortableEntry.cpp b/src/AppInstallerCommonCore/PortableEntry.cpp index 32d18d42aa..129b00daf4 100644 --- a/src/AppInstallerCommonCore/PortableEntry.cpp +++ b/src/AppInstallerCommonCore/PortableEntry.cpp @@ -7,6 +7,8 @@ #include "winget/Filesystem.h" #include "winget/PathVariable.h" #include "Public/AppInstallerLogging.h" +#include +#include using namespace AppInstaller::Registry; using namespace AppInstaller::Registry::Portable; @@ -14,10 +16,22 @@ using namespace AppInstaller::Registry::Environment; namespace AppInstaller::Portable { - PortableEntry::PortableEntry(PortableARPEntry& portableARPEntry) : - m_portableARPEntry(portableARPEntry) + namespace + { + constexpr std::string_view c_PortableIndexFileName = "portable.db"; + + void AppendExeExtension(std::filesystem::path& value) + { + if (value.extension() != ".exe") + { + value += ".exe"; + } + } + } + + PortableEntry::PortableEntry(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode, bool isUpdate) : + m_productCode(productCode), m_portableARPEntry(PortableARPEntry(scope, arch, productCode)), m_isUpdate(isUpdate) { - // Initialize all values if present if (Exists()) { DisplayName = GetStringValue(PortableValueName::DisplayName); @@ -42,6 +56,139 @@ namespace AppInstaller::Portable } } + HRESULT PortableEntry::MultipleInstall(const std::vector& nestedInstallerFiles, const std::vector& extractedItems) + { + // Add each file/ directory to database. + if (extractedItems.size() == 0) + { + + } + + for (const auto& nestedInstallerFile : nestedInstallerFiles) + { + // This is supposed to be where the portable exe is located. + PortableTargetFullPath = InstallLocation / ConvertToUTF16(nestedInstallerFile.RelativeFilePath); + + // Create symlink + if (!InstallDirectoryAddedToPath) + { + PortableSymlinkFullPath = GetPortableLinksLocation(GetScope()) / ConvertToUTF16(nestedInstallerFile.PortableCommandAlias); + Commit(PortableValueName::PortableSymlinkFullPath, PortableSymlinkFullPath); + + std::filesystem::file_status status = std::filesystem::status(PortableSymlinkFullPath); + if (std::filesystem::is_directory(status)) + { + AICLI_LOG(CLI, Info, << "Unable to create symlink. '" << PortableSymlinkFullPath << "points to an existing directory."); + return APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY; + } + + if (std::filesystem::remove(PortableSymlinkFullPath)) + { + AICLI_LOG(CLI, Info, << "Removed existing file at " << PortableSymlinkFullPath); + //context.Reporter.Warn() << Resource::String::OverwritingExistingFileAtMessage << ' ' << PortableSymlinkFullPath.u8string() << std::endl; + } + + CreatePortableSymlink(); + } + else + { + AICLI_LOG(CLI, Info, << "Package directory was previously added to PATH. Skipping symlink creation."); + } + } + + // Add to path + const std::filesystem::path& pathValue = GetPathValue(); + if (PathVariable(GetScope()).Append(pathValue)) + { + AICLI_LOG(Core, Info, << "Appended target directory to PATH registry: " << pathValue); + //context.Reporter.Warn() << Resource::String::ModifiedPathRequiresShellRestart << std::endl; + m_addedToPath = true; + } + else + { + AICLI_LOG(CLI, Info, << "Target directory already exists in PATH registry: " << pathValue); + } + + return ERROR_SUCCESS; + } + + HRESULT PortableEntry::SingleInstall(const std::filesystem::path& installerPath, const std::filesystem::path& commandAlias, bool rename) + { + // Initial registration. + Commit(PortableValueName::WinGetPackageIdentifier, WinGetPackageIdentifier); + Commit(PortableValueName::WinGetSourceIdentifier, WinGetSourceIdentifier); + Commit(PortableValueName::UninstallString, UninstallString = "winget uninstall --product-code " + m_productCode); + Commit(PortableValueName::WinGetInstallerType, WinGetInstallerType = InstallerTypeToString(Manifest::InstallerTypeEnum::Portable)); + Commit(PortableValueName::SHA256, SHA256); + + // Move portable exe. + if (rename) + { + PortableTargetFullPath = InstallLocation / commandAlias; + } + else + { + PortableTargetFullPath = InstallLocation / installerPath.filename(); + } + + AppendExeExtension(PortableTargetFullPath); + Commit(PortableValueName::PortableTargetFullPath, PortableTargetFullPath); + Commit(PortableValueName::InstallLocation, InstallLocation); + MovePortableExe(installerPath); + + + // Create symlink + if (!InstallDirectoryAddedToPath) + { + PortableSymlinkFullPath = GetPortableLinksLocation(GetScope()) / commandAlias; + Commit(PortableValueName::PortableSymlinkFullPath, PortableSymlinkFullPath); + + std::filesystem::file_status status = std::filesystem::status(PortableSymlinkFullPath); + if (std::filesystem::is_directory(status)) + { + AICLI_LOG(CLI, Info, << "Unable to create symlink. '" << PortableSymlinkFullPath << "points to an existing directory."); + return APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY; + } + + if (std::filesystem::remove(PortableSymlinkFullPath)) + { + AICLI_LOG(CLI, Info, << "Removed existing file at " << PortableSymlinkFullPath); + //context.Reporter.Warn() << Resource::String::OverwritingExistingFileAtMessage << ' ' << PortableSymlinkFullPath.u8string() << std::endl; + } + + CreatePortableSymlink(); + } + else + { + AICLI_LOG(CLI, Info, << "Package directory was previously added to PATH. Skipping symlink creation."); + } + + // Add to path + const std::filesystem::path& pathValue = GetPathValue(); + if (PathVariable(GetScope()).Append(pathValue)) + { + AICLI_LOG(Core, Info, << "Appended target directory to PATH registry: " << pathValue); + //context.Reporter.Warn() << Resource::String::ModifiedPathRequiresShellRestart << std::endl; + m_addedToPath = true; + } + else + { + AICLI_LOG(CLI, Info, << "Target directory already exists in PATH registry: " << pathValue); + } + + return ERROR_SUCCESS; + } + + void PortableEntry::SetAppsAndFeaturesMetadata(const Manifest::AppsAndFeaturesEntry& entry, const Manifest::Manifest& manifest) + { + Commit(PortableValueName::DisplayName, DisplayName = entry.DisplayName); + Commit(PortableValueName::DisplayVersion, DisplayVersion = entry.DisplayVersion); + Commit(PortableValueName::Publisher, Publisher = entry.Publisher); + Commit(PortableValueName::InstallDate, InstallDate = AppInstaller::Utility::GetCurrentDateForARP()); + Commit(PortableValueName::URLInfoAbout, URLInfoAbout = manifest.CurrentLocalization.Get()); + Commit(PortableValueName::HelpLink, HelpLink = manifest.CurrentLocalization.Get < Manifest::Localization::PublisherSupportUrl>()); + } + void PortableEntry::MovePortableExe(const std::filesystem::path& installerPath) { bool isDirectoryCreated = false; @@ -109,11 +256,6 @@ namespace AppInstaller::Portable } } - bool PortableEntry::AddToPathVariable() - { - return PathVariable(GetScope()).Append(GetPathValue()); - } - bool PortableEntry::RemoveFromPathVariable() { bool removeFromPath = true; diff --git a/src/AppInstallerCommonCore/Public/winget/Archive.h b/src/AppInstallerCommonCore/Public/winget/Archive.h index bee409e6f3..2b75fbff37 100644 --- a/src/AppInstallerCommonCore/Public/winget/Archive.h +++ b/src/AppInstallerCommonCore/Public/winget/Archive.h @@ -5,5 +5,5 @@ namespace AppInstaller::Archive { - HRESULT TryExtractArchive(const std::filesystem::path& archivePath, const std::filesystem::path& destPath); + HRESULT TryExtractArchive(const std::filesystem::path& archivePath, const std::filesystem::path& destPath, std::vector& extractedItems); } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/PortableEntry.h b/src/AppInstallerCommonCore/Public/winget/PortableEntry.h index 67fc4c1b33..8a08a7819e 100644 --- a/src/AppInstallerCommonCore/Public/winget/PortableEntry.h +++ b/src/AppInstallerCommonCore/Public/winget/PortableEntry.h @@ -4,6 +4,7 @@ #include #include #include "winget/PortableARPEntry.h" +#include #include #include @@ -31,6 +32,10 @@ namespace AppInstaller::Portable std::string WinGetSourceIdentifier; bool InstallDirectoryAddedToPath = false; + HRESULT SingleInstall(const std::filesystem::path& installerPath, const std::filesystem::path& alias, bool rename = false); + + HRESULT MultipleInstall(const std::vector& nestedInstallerFiles, const std::vector& extractedItems); + template void Commit(PortableValueName valueName, T value) { @@ -41,13 +46,47 @@ namespace AppInstaller::Portable bool Exists() { return m_portableARPEntry.Exists(); }; - PortableEntry(PortableARPEntry& portableARPEntry); + PortableEntry(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode, bool isUpdate = false); std::filesystem::path GetPathValue() const { return InstallDirectoryAddedToPath ? InstallLocation : PortableSymlinkFullPath.parent_path(); } + static std::filesystem::path GetPortableLinksLocation(Manifest::ScopeEnum scope) + { + if (scope == Manifest::ScopeEnum::Machine) + { + return Runtime::GetPathTo(Runtime::PathName::PortableLinksMachineLocation); + } + else + { + return Runtime::GetPathTo(Runtime::PathName::PortableLinksUserLocation); + } + } + + static std::filesystem::path GetPortableInstallLocation(Manifest::ScopeEnum scope, Utility::Architecture arch) + { + if (scope == Manifest::ScopeEnum::Machine) + { + if (arch == Utility::Architecture::X86) + { + return Runtime::GetPathTo(Runtime::PathName::PortablePackageMachineRootX86); + } + else + { + return Runtime::GetPathTo(Runtime::PathName::PortablePackageMachineRootX64); + } + } + else + { + return Runtime::GetPathTo(Runtime::PathName::PortablePackageUserRoot); + } + } + + + void SetAppsAndFeaturesMetadata(const Manifest::AppsAndFeaturesEntry& entry, const Manifest::Manifest& manifest); + bool VerifyPortableExeHash(); bool VerifySymlinkTarget(); @@ -56,14 +95,19 @@ namespace AppInstaller::Portable void CreatePortableSymlink(); - bool AddToPathVariable(); - bool RemoveFromPathVariable(); void RemoveARPEntry(); + bool IsUpdate() { return m_isUpdate; }; + private: + //std::unique_ptr m_portableIndex; + bool m_isUpdate = false; + bool m_addedToPath = false; + std::string m_productCode; PortableARPEntry m_portableARPEntry; + std::string GetStringValue(PortableValueName valueName); std::filesystem::path GetPathValue(PortableValueName valueName); bool GetBoolValue(PortableValueName valueName); From 50b6a494720d33c30a87d85ae22a012a3226c446 Mon Sep 17 00:00:00 2001 From: Ryan Fu Date: Tue, 30 Aug 2022 00:51:05 -0700 Subject: [PATCH 02/22] save work --- .../Workflows/PortableFlow.cpp | 143 +---------- .../Workflows/UninstallFlow.cpp | 6 +- src/AppInstallerCLITests/PortableEntry.cpp | 10 +- src/AppInstallerCommonCore/Filesystem.cpp | 4 +- src/AppInstallerCommonCore/PortableEntry.cpp | 231 ++++++++++++------ .../Public/winget/Filesystem.h | 2 +- .../Public/winget/PortableEntry.h | 28 ++- 7 files changed, 194 insertions(+), 230 deletions(-) diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp index 732117035e..8b24e4840a 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -7,7 +7,6 @@ #include "winget/PortableARPEntry.h" #include "winget/PortableEntry.h" #include "winget/PathVariable.h" -#include "AppInstallerStrings.h" using namespace AppInstaller::Manifest; using namespace AppInstaller::Portable; @@ -100,125 +99,6 @@ namespace AppInstaller::CLI::Workflow return appsAndFeaturesEntry; } - void RemovePortableExe(Execution::Context& context) - { - Portable::PortableEntry& portableEntry = context.Get(); - const auto& targetPath = portableEntry.PortableTargetFullPath; - - if (std::filesystem::exists(targetPath)) - { - if (!portableEntry.VerifyPortableExeHash()) - { - bool overrideHashMismatch = context.Args.Contains(Execution::Args::Type::HashOverride); - if (overrideHashMismatch) - { - context.Reporter.Warn() << Resource::String::PortableHashMismatchOverridden << std::endl; - } - else - { - context.Reporter.Warn() << Resource::String::PortableHashMismatchOverrideRequired << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED); - } - } - - std::filesystem::remove(targetPath); - AICLI_LOG(CLI, Info, << "Successfully deleted portable exe:" << targetPath); - } - else - { - AICLI_LOG(CLI, Info, << "Portable exe not found; Unable to delete portable exe: " << targetPath); - } - } - - void RemoveInstallDirectory(Execution::Context& context) - { - Portable::PortableEntry& portableEntry = context.Get(); - const auto& installDirectory = portableEntry.InstallLocation; - - if (std::filesystem::exists(installDirectory)) - { - const auto& isCreated = portableEntry.InstallDirectoryCreated; - bool isUpdate = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate); - - if (context.Args.Contains(Execution::Args::Type::Purge) || - (!isUpdate && Settings::User().Get() && !context.Args.Contains(Execution::Args::Type::Preserve))) - { - if (isCreated) - { - context.Reporter.Warn() << Resource::String::PurgeInstallDirectory << std::endl; - const auto& removedFilesCount = std::filesystem::remove_all(installDirectory); - AICLI_LOG(CLI, Info, << "Purged install location directory. Deleted " << removedFilesCount << " files or directories"); - } - else - { - context.Reporter.Warn() << Resource::String::UnableToPurgeInstallDirectory << std::endl; - } - - } - else if (std::filesystem::is_empty(installDirectory)) - { - if (isCreated) - { - std::filesystem::remove(installDirectory); - AICLI_LOG(CLI, Info, << "Install directory deleted: " << installDirectory); - } - } - else - { - context.Reporter.Warn() << Resource::String::FilesRemainInInstallDirectory << installDirectory << std::endl; - } - } - else - { - AICLI_LOG(CLI, Info, << "Install directory does not exist: " << installDirectory); - } - } - - void RemoveFromPathVariable(Execution::Context& context) - { - Portable::PortableEntry& portableEntry = context.Get(); - const std::filesystem::path& pathValue = portableEntry.GetPathValue(); - if (portableEntry.RemoveFromPathVariable()) - { - AICLI_LOG(CLI, Info, << "Removed target directory from PATH registry: " << pathValue); - } - else - { - AICLI_LOG(CLI, Info, << "Target directory not removed from PATH registry: " << pathValue); - } - } - - void RemovePortableSymlink(Execution::Context& context) - { - Portable::PortableEntry& portableEntry = context.Get(); - const auto& symlinkPath = portableEntry.PortableSymlinkFullPath; - - if (!std::filesystem::is_symlink(std::filesystem::symlink_status(symlinkPath))) - { - AICLI_LOG(Core, Info, << "The registry value for [PortableSymlinkFullPath] does not point to a valid symlink file."); - return; - } - - if (portableEntry.VerifySymlinkTarget()) - { - if (!std::filesystem::remove(symlinkPath)) - { - AICLI_LOG(CLI, Info, << "Portable symlink not found; Unable to delete portable symlink: " << symlinkPath); - } - } - else - { - context.Reporter.Warn() << Resource::String::SymlinkModified << std::endl; - } - } - - void RemovePortableARPEntry(Execution::Context& context) - { - Portable::PortableEntry& portableEntry = context.Get(); - portableEntry.RemoveARPEntry(); - AICLI_LOG(CLI, Info, << "PortableARPEntry deleted."); - } - void EnsureValidArgsForPortableInstall(Execution::Context& context) { std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); @@ -398,9 +278,6 @@ namespace AppInstaller::CLI::Workflow context.Add(Workflow::HandleException(context, std::current_exception())); } - // Reset termination to allow for ReportInstallResult to process return code. - context.ResetTermination(); - // Perform cleanup only if the install fails and is not an update. const auto& installReturnCode = context.Get(); @@ -418,26 +295,24 @@ namespace AppInstaller::CLI::Workflow void PortableUninstallImpl(Execution::Context& context) { + HRESULT result; + try { context.Reporter.Info() << Resource::String::UninstallFlowStartingPackageUninstall << std::endl; - context << - RemovePortableExe << - RemoveInstallDirectory << - RemovePortableSymlink << - RemoveFromPathVariable << - RemovePortableARPEntry; + PortableEntry& portableEntry = context.Get(); + bool shouldPurge = context.Args.Contains(Execution::Args::Type::Purge) || + (!portableEntry.IsUpdate() && Settings::User().Get() && !context.Args.Contains(Execution::Args::Type::Preserve)); - context.Add(context.GetTerminationHR()); + result = portableEntry.Uninstall(shouldPurge); } - catch (...) + catch (HRESULT error) { - context.Add(Workflow::HandleException(context, std::current_exception())); + result = error; } - // Reset termination to allow for ReportUninstallResult to process return code. - context.ResetTermination(); + context.Add(result); } void EnsureSupportForPortableInstall(Execution::Context& context) diff --git a/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp b/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp index d28d1d4de4..ff0abadb65 100644 --- a/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp @@ -141,12 +141,14 @@ namespace AppInstaller::CLI::Workflow const std::string installedScope = context.Get()->GetMetadata()[Repository::PackageVersionMetadata::InstalledScope]; const std::string installedArch = context.Get()->GetMetadata()[Repository::PackageVersionMetadata::InstalledArchitecture]; + bool isUpdate = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate); Portable::PortableEntry portableEntry = Portable::PortableEntry( ConvertToScopeEnum(installedScope), Utility::ConvertToArchitectureEnum(installedArch), - productCodes[0]); + productCodes[0], + isUpdate); - context.Add(portableEntry); + context.Add(std::move(portableEntry)); break; } default: diff --git a/src/AppInstallerCLITests/PortableEntry.cpp b/src/AppInstallerCLITests/PortableEntry.cpp index e172fa29b1..718d2fce72 100644 --- a/src/AppInstallerCLITests/PortableEntry.cpp +++ b/src/AppInstallerCLITests/PortableEntry.cpp @@ -48,7 +48,7 @@ TEST_CASE("VerifyPortableMove", "[PortableEntry]") REQUIRE(std::filesystem::exists(testEntry2.PortableTargetFullPath)); // InstallDirectoryCreated value should be preserved even though the directory was not created; REQUIRE(testEntry2.InstallDirectoryCreated); - testEntry2.RemoveARPEntry(); + //testEntry2.RemoveARPEntry(); } TEST_CASE("VerifySymlinkCheck", "[PortableEntry]") @@ -66,14 +66,14 @@ TEST_CASE("VerifySymlinkCheck", "[PortableEntry]") testEntry.PortableTargetFullPath = testFile.GetPath(); testEntry.PortableSymlinkFullPath = tempDirectory.GetPath() / "symlink.exe"; - testEntry.CreatePortableSymlink(); + testEntry.CreatePortableSymlink(testEntry.PortableTargetFullPath, testEntry.PortableSymlinkFullPath); - REQUIRE(testEntry.VerifySymlinkTarget()); + //REQUIRE(testEntry.VerifySymlinkTarget()); // Modify with incorrect target full path. testEntry.PortableTargetFullPath = tempDirectory.GetPath() / "invalidTarget.txt"; - REQUIRE_FALSE(testEntry.VerifySymlinkTarget()); - testEntry.RemoveARPEntry(); + //REQUIRE_FALSE(testEntry.VerifySymlinkTarget()); + //testEntry.RemoveARPEntry(); } //TEST_CASE("VerifyPathVariableModified", "[PortableEntry]") diff --git a/src/AppInstallerCommonCore/Filesystem.cpp b/src/AppInstallerCommonCore/Filesystem.cpp index ea96edce1a..e18c364b68 100644 --- a/src/AppInstallerCommonCore/Filesystem.cpp +++ b/src/AppInstallerCommonCore/Filesystem.cpp @@ -142,7 +142,7 @@ namespace AppInstaller::Filesystem } #endif - bool CreateSymlink(const std::filesystem::path& to, const std::filesystem::path& target) + bool CreateSymlink(const std::filesystem::path& target, const std::filesystem::path& link) { #ifndef AICLI_DISABLE_TEST_HOOKS if (s_CreateSymlinkResult_TestHook_Override) @@ -152,7 +152,7 @@ namespace AppInstaller::Filesystem #endif try { - std::filesystem::create_symlink(to, target); + std::filesystem::create_symlink(target, link); return true; } catch (std::filesystem::filesystem_error& error) diff --git a/src/AppInstallerCommonCore/PortableEntry.cpp b/src/AppInstallerCommonCore/PortableEntry.cpp index 129b00daf4..6c014ef768 100644 --- a/src/AppInstallerCommonCore/PortableEntry.cpp +++ b/src/AppInstallerCommonCore/PortableEntry.cpp @@ -61,34 +61,18 @@ namespace AppInstaller::Portable // Add each file/ directory to database. if (extractedItems.size() == 0) { - + // Record all extracted items and create database if not created. } for (const auto& nestedInstallerFile : nestedInstallerFiles) { - // This is supposed to be where the portable exe is located. - PortableTargetFullPath = InstallLocation / ConvertToUTF16(nestedInstallerFile.RelativeFilePath); + const std::filesystem::path& portableTargetPath = InstallLocation / ConvertToUTF16(nestedInstallerFile.RelativeFilePath); - // Create symlink if (!InstallDirectoryAddedToPath) { - PortableSymlinkFullPath = GetPortableLinksLocation(GetScope()) / ConvertToUTF16(nestedInstallerFile.PortableCommandAlias); - Commit(PortableValueName::PortableSymlinkFullPath, PortableSymlinkFullPath); - - std::filesystem::file_status status = std::filesystem::status(PortableSymlinkFullPath); - if (std::filesystem::is_directory(status)) - { - AICLI_LOG(CLI, Info, << "Unable to create symlink. '" << PortableSymlinkFullPath << "points to an existing directory."); - return APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY; - } - - if (std::filesystem::remove(PortableSymlinkFullPath)) - { - AICLI_LOG(CLI, Info, << "Removed existing file at " << PortableSymlinkFullPath); - //context.Reporter.Warn() << Resource::String::OverwritingExistingFileAtMessage << ' ' << PortableSymlinkFullPath.u8string() << std::endl; - } - - CreatePortableSymlink(); + const std::filesystem::path& symlinkFullPath = GetPortableLinksLocation(GetScope()) / ConvertToUTF16(nestedInstallerFile.PortableCommandAlias); + CreatePortableSymlink(portableTargetPath, symlinkFullPath); + // Record symlink file } else { @@ -96,18 +80,7 @@ namespace AppInstaller::Portable } } - // Add to path - const std::filesystem::path& pathValue = GetPathValue(); - if (PathVariable(GetScope()).Append(pathValue)) - { - AICLI_LOG(Core, Info, << "Appended target directory to PATH registry: " << pathValue); - //context.Reporter.Warn() << Resource::String::ModifiedPathRequiresShellRestart << std::endl; - m_addedToPath = true; - } - else - { - AICLI_LOG(CLI, Info, << "Target directory already exists in PATH registry: " << pathValue); - } + AddToPathVariable(); return ERROR_SUCCESS; } @@ -121,7 +94,7 @@ namespace AppInstaller::Portable Commit(PortableValueName::WinGetInstallerType, WinGetInstallerType = InstallerTypeToString(Manifest::InstallerTypeEnum::Portable)); Commit(PortableValueName::SHA256, SHA256); - // Move portable exe. + // Move portable exe if (rename) { PortableTargetFullPath = InstallLocation / commandAlias; @@ -136,46 +109,47 @@ namespace AppInstaller::Portable Commit(PortableValueName::InstallLocation, InstallLocation); MovePortableExe(installerPath); - // Create symlink if (!InstallDirectoryAddedToPath) { PortableSymlinkFullPath = GetPortableLinksLocation(GetScope()) / commandAlias; Commit(PortableValueName::PortableSymlinkFullPath, PortableSymlinkFullPath); - std::filesystem::file_status status = std::filesystem::status(PortableSymlinkFullPath); - if (std::filesystem::is_directory(status)) - { - AICLI_LOG(CLI, Info, << "Unable to create symlink. '" << PortableSymlinkFullPath << "points to an existing directory."); - return APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY; - } - - if (std::filesystem::remove(PortableSymlinkFullPath)) - { - AICLI_LOG(CLI, Info, << "Removed existing file at " << PortableSymlinkFullPath); - //context.Reporter.Warn() << Resource::String::OverwritingExistingFileAtMessage << ' ' << PortableSymlinkFullPath.u8string() << std::endl; - } - - CreatePortableSymlink(); + CreatePortableSymlink(PortableTargetFullPath, PortableSymlinkFullPath); } else { AICLI_LOG(CLI, Info, << "Package directory was previously added to PATH. Skipping symlink creation."); } - // Add to path - const std::filesystem::path& pathValue = GetPathValue(); - if (PathVariable(GetScope()).Append(pathValue)) + AddToPathVariable(); + return ERROR_SUCCESS; + } + + HRESULT PortableEntry::Uninstall(bool purge) + { + // remove portable + RemovePortableExe(PortableTargetFullPath, SHA256); + + // remove install directory + RemovePortableDirectory(InstallLocation, purge, InstallDirectoryCreated); + + // remove portable symlink + RemovePortableSymlink(PortableTargetFullPath, PortableSymlinkFullPath); + + // remove path + const std::filesystem::path& pathValue = GetPathDirectory(); + if (RemoveFromPathVariable()) { - AICLI_LOG(Core, Info, << "Appended target directory to PATH registry: " << pathValue); - //context.Reporter.Warn() << Resource::String::ModifiedPathRequiresShellRestart << std::endl; - m_addedToPath = true; + AICLI_LOG(CLI, Info, << "Removed target directory from PATH registry: " << pathValue); } else { - AICLI_LOG(CLI, Info, << "Target directory already exists in PATH registry: " << pathValue); + AICLI_LOG(CLI, Info, << "Target directory not removed from PATH registry: " << pathValue); } - + + m_portableARPEntry.Delete(); + AICLI_LOG(CLI, Info, << "PortableARPEntry deleted."); return ERROR_SUCCESS; } @@ -215,20 +189,48 @@ namespace AppInstaller::Portable } } - bool PortableEntry::VerifyPortableExeHash() + bool PortableEntry::VerifyPortableExeHash(const std::filesystem::path& targetPath, const std::string& hashValue) { - std::ifstream inStream{ PortableTargetFullPath, std::ifstream::binary }; + std::ifstream inStream{ targetPath, std::ifstream::binary }; const Utility::SHA256::HashBuffer& targetFileHash = Utility::SHA256::ComputeHash(inStream); inStream.close(); - return Utility::SHA256::AreEqual(Utility::SHA256::ConvertToBytes(SHA256), targetFileHash); + return Utility::SHA256::AreEqual(Utility::SHA256::ConvertToBytes(hashValue), targetFileHash); + } + + void PortableEntry::AddToPathVariable() + { + const std::filesystem::path& pathValue = GetPathDirectory(); + if (PathVariable(GetScope()).Append(pathValue)) + { + AICLI_LOG(Core, Info, << "Appended target directory to PATH registry: " << pathValue); + //context.Reporter.Warn() << Resource::String::ModifiedPathRequiresShellRestart << std::endl; + m_addedToPath = true; + } + else + { + AICLI_LOG(CLI, Info, << "Target directory already exists in PATH registry: " << pathValue); + } } - void PortableEntry::CreatePortableSymlink() + void PortableEntry::CreatePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath) { - if (Filesystem::CreateSymlink(PortableTargetFullPath, PortableSymlinkFullPath)) + std::filesystem::file_status status = std::filesystem::status(symlinkPath); + if (std::filesystem::is_directory(status)) { - AICLI_LOG(Core, Info, << "Symlink created at: " << PortableSymlinkFullPath); + AICLI_LOG(CLI, Info, << "Unable to create symlink. '" << symlinkPath << "points to an existing directory."); + throw APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY; + } + + if (std::filesystem::remove(symlinkPath)) + { + AICLI_LOG(CLI, Info, << "Removed existing file at " << symlinkPath); + //m_stream << Resource::String::OverwritingExistingFileAtMessage << ' ' << symlinkPath.u8string() << std::endl; + } + + if (Filesystem::CreateSymlink(targetPath, symlinkPath)) + { + AICLI_LOG(Core, Info, << "Symlink created at: " << symlinkPath); } else { @@ -239,12 +241,12 @@ namespace AppInstaller::Portable } } - bool PortableEntry::VerifySymlinkTarget() + bool PortableEntry::VerifySymlinkTarget(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath) { - AICLI_LOG(Core, Info, << "Expected portable target path: " << PortableTargetFullPath); - const std::filesystem::path& symlinkTargetPath = std::filesystem::read_symlink(PortableSymlinkFullPath); + AICLI_LOG(Core, Info, << "Expected portable target path: " << targetPath); + const std::filesystem::path& symlinkTargetPath = std::filesystem::read_symlink(symlinkPath); - if (symlinkTargetPath == PortableTargetFullPath) + if (symlinkTargetPath == targetPath) { AICLI_LOG(Core, Info, << "Portable symlink target matches portable target path: " << symlinkTargetPath); return true; @@ -256,10 +258,97 @@ namespace AppInstaller::Portable } } + void PortableEntry::RemovePortableExe(const std::filesystem::path& targetPath, const std::string& hash) + { + if (std::filesystem::exists(targetPath)) + { + if (!VerifyPortableExeHash(targetPath, hash)) + { + //bool overrideHashMismatch = context.Args.Contains(Execution::Args::Type::HashOverride); + bool overrideHashMismatch = true; + if (overrideHashMismatch) + { + //context.Reporter.Warn() << Resource::String::PortableHashMismatchOverridden << std::endl; + } + else + { + //context.Reporter.Warn() << Resource::String::PortableHashMismatchOverrideRequired << std::endl; + throw APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED; + } + } + + std::filesystem::remove(targetPath); + AICLI_LOG(CLI, Info, << "Successfully deleted portable exe:" << targetPath); + } + else + { + AICLI_LOG(CLI, Info, << "Portable exe not found; Unable to delete portable exe: " << targetPath); + } + } + + void PortableEntry::RemovePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath) + { + if (!std::filesystem::is_symlink(std::filesystem::symlink_status(symlinkPath))) + { + AICLI_LOG(Core, Info, << "The registry value for [PortableSymlinkFullPath] does not point to a valid symlink file."); + } + else + { + if (VerifySymlinkTarget(targetPath, symlinkPath)) + { + if (!std::filesystem::remove(symlinkPath)) + { + AICLI_LOG(CLI, Info, << "Portable symlink not found; Unable to delete portable symlink: " << symlinkPath); + } + } + else + { + //context.Reporter.Warn() << Resource::String::SymlinkModified << std::endl; + } + } + } + + void PortableEntry::RemovePortableDirectory(const std::filesystem::path& directoryPath, bool purge, bool isCreated) + { + if (std::filesystem::exists(directoryPath)) + { + if (purge) + { + if (isCreated) + { + //context.Reporter.Warn() << Resource::String::PurgeInstallDirectory << std::endl; + const auto& removedFilesCount = std::filesystem::remove_all(directoryPath); + AICLI_LOG(CLI, Info, << "Purged install location directory. Deleted " << removedFilesCount << " files or directories"); + } + else + { + //context.Reporter.Warn() << Resource::String::UnableToPurgeInstallDirectory << std::endl; + } + + } + else if (std::filesystem::is_empty(directoryPath)) + { + if (isCreated) + { + std::filesystem::remove(directoryPath); + AICLI_LOG(CLI, Info, << "Install directory deleted: " << directoryPath); + } + } + else + { + //context.Reporter.Warn() << Resource::String::FilesRemainInInstallDirectory << installDirectory << std::endl; + } + } + else + { + AICLI_LOG(CLI, Info, << "Install directory does not exist: " << directoryPath); + } + } + bool PortableEntry::RemoveFromPathVariable() { bool removeFromPath = true; - std::filesystem::path pathValue = GetPathValue(); + std::filesystem::path pathValue = GetPathDirectory(); if (!InstallDirectoryAddedToPath) { // Default links directory must be empty before removing from PATH. @@ -279,12 +368,6 @@ namespace AppInstaller::Portable return false; } } - - void PortableEntry::RemoveARPEntry() - { - m_portableARPEntry.Delete(); - } - std::string PortableEntry::GetStringValue(PortableValueName valueName) { if (m_portableARPEntry[valueName].has_value()) diff --git a/src/AppInstallerCommonCore/Public/winget/Filesystem.h b/src/AppInstallerCommonCore/Public/winget/Filesystem.h index 29828285e5..7313374abe 100644 --- a/src/AppInstallerCommonCore/Public/winget/Filesystem.h +++ b/src/AppInstallerCommonCore/Public/winget/Filesystem.h @@ -21,5 +21,5 @@ namespace AppInstaller::Filesystem void RenameFile(const std::filesystem::path& from, const std::filesystem::path& to); // Creates a symlink that points to the target path. - bool CreateSymlink(const std::filesystem::path& path, const std::filesystem::path& target); + bool CreateSymlink(const std::filesystem::path& target, const std::filesystem::path& link); } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/PortableEntry.h b/src/AppInstallerCommonCore/Public/winget/PortableEntry.h index 8a08a7819e..23d07445b1 100644 --- a/src/AppInstallerCommonCore/Public/winget/PortableEntry.h +++ b/src/AppInstallerCommonCore/Public/winget/PortableEntry.h @@ -36,6 +36,8 @@ namespace AppInstaller::Portable HRESULT MultipleInstall(const std::vector& nestedInstallerFiles, const std::vector& extractedItems); + HRESULT Uninstall(bool purge = false); + template void Commit(PortableValueName valueName, T value) { @@ -48,7 +50,7 @@ namespace AppInstaller::Portable PortableEntry(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode, bool isUpdate = false); - std::filesystem::path GetPathValue() const + std::filesystem::path GetPathDirectory() const { return InstallDirectoryAddedToPath ? InstallLocation : PortableSymlinkFullPath.parent_path(); } @@ -84,30 +86,32 @@ namespace AppInstaller::Portable } } - void SetAppsAndFeaturesMetadata(const Manifest::AppsAndFeaturesEntry& entry, const Manifest::Manifest& manifest); - bool VerifyPortableExeHash(); - - bool VerifySymlinkTarget(); - void MovePortableExe(const std::filesystem::path& installerPath); - void CreatePortableSymlink(); - - bool RemoveFromPathVariable(); - - void RemoveARPEntry(); + void CreatePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath); bool IsUpdate() { return m_isUpdate; }; private: - //std::unique_ptr m_portableIndex; bool m_isUpdate = false; bool m_addedToPath = false; std::string m_productCode; PortableARPEntry m_portableARPEntry; + std::stringstream m_stream; + + bool VerifySymlinkTarget(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath); + bool VerifyPortableExeHash(const std::filesystem::path& targetPath, const std::string& hashValue); + + void RemovePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath); + void RemovePortableDirectory(const std::filesystem::path& directoryPath, bool purge, bool isCreated); + void RemovePortableExe(const std::filesystem::path& targetPath, const std::string& hash); + + void AddToPathVariable(); + bool RemoveFromPathVariable(); + std::string GetStringValue(PortableValueName valueName); std::filesystem::path GetPathValue(PortableValueName valueName); bool GetBoolValue(PortableValueName valueName); From af72152ff2646e509cc2d380c9c0f56235e34609 Mon Sep 17 00:00:00 2001 From: Ryan Fu Date: Thu, 1 Sep 2022 00:41:59 -0700 Subject: [PATCH 03/22] refactor portable flow --- .../AppInstallerCLICore.vcxproj | 2 + .../AppInstallerCLICore.vcxproj.filters | 9 + .../ExecutionContextData.h | 13 +- .../PortableInstaller.cpp} | 296 ++++++++++++++---- .../PortableInstaller.h} | 93 +++--- src/AppInstallerCLICore/Resources.h | 1 - .../Workflows/ArchiveFlow.cpp | 50 ++- .../Workflows/InstallFlow.cpp | 4 +- .../Workflows/PortableFlow.cpp | 235 ++++++-------- .../Workflows/PortableFlow.h | 11 +- .../Workflows/UninstallFlow.cpp | 19 +- .../Shared/Strings/en-us/winget.resw | 3 - src/AppInstallerCLITests/PortableEntry.cpp | 10 +- .../AppInstallerCommonCore.vcxproj | 2 - .../AppInstallerCommonCore.vcxproj.filters | 6 - src/AppInstallerCommonCore/Archive.cpp | 1 + .../PortableARPEntry.cpp | 3 +- .../Public/winget/PortableARPEntry.h | 2 + .../Microsoft/PortableIndex.cpp | 13 + .../Microsoft/PortableIndex.h | 4 + .../Microsoft/Schema/IPortableIndex.h | 8 +- .../Portable_1_0/PortableIndexInterface.h | 2 + .../PortableIndexInterface_1_0.cpp | 10 + 23 files changed, 481 insertions(+), 316 deletions(-) rename src/{AppInstallerCommonCore/PortableEntry.cpp => AppInstallerCLICore/PortableInstaller.cpp} (56%) rename src/{AppInstallerCommonCore/Public/winget/PortableEntry.h => AppInstallerCLICore/PortableInstaller.h} (65%) diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj index 4cc8f5d9da..a78da26639 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -269,6 +269,7 @@ + @@ -327,6 +328,7 @@ Create + diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters index 4904eab23d..f96b062d52 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -185,6 +185,9 @@ Workflows + + Header Files + @@ -337,6 +340,12 @@ Workflows + + Source Files + + + Source Files + diff --git a/src/AppInstallerCLICore/ExecutionContextData.h b/src/AppInstallerCLICore/ExecutionContextData.h index 1cffc15ead..65d768f78d 100644 --- a/src/AppInstallerCLICore/ExecutionContextData.h +++ b/src/AppInstallerCLICore/ExecutionContextData.h @@ -4,11 +4,10 @@ #include #include #include -#include -#include #include "CompletionData.h" #include "PackageCollection.h" #include "Workflows/WorkflowBase.h" +#include #include #include @@ -54,8 +53,8 @@ namespace AppInstaller::CLI::Execution Dependencies, DependencySource, AllowedArchitectures, - PortableEntry, AllowUnknownScope, + PortableInstaller, ExtractedItems, Max }; @@ -221,15 +220,15 @@ namespace AppInstaller::CLI::Execution }; template <> - struct DataMapping + struct DataMapping { - using value_t = Portable::PortableEntry; + using value_t = bool; }; template <> - struct DataMapping + struct DataMapping { - using value_t = bool; + using value_t = CLI::Portable::PortableInstaller; }; template <> diff --git a/src/AppInstallerCommonCore/PortableEntry.cpp b/src/AppInstallerCLICore/PortableInstaller.cpp similarity index 56% rename from src/AppInstallerCommonCore/PortableEntry.cpp rename to src/AppInstallerCLICore/PortableInstaller.cpp index 6c014ef768..02cb63a8e9 100644 --- a/src/AppInstallerCommonCore/PortableEntry.cpp +++ b/src/AppInstallerCLICore/PortableInstaller.cpp @@ -1,67 +1,117 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #include "pch.h" -#include "winget/PortableEntry.h" +#include "PortableInstaller.h" #include "winget/PortableARPEntry.h" #include "winget/Manifest.h" +#include "winget/ManifestCommon.h" #include "winget/Filesystem.h" #include "winget/PathVariable.h" -#include "Public/AppInstallerLogging.h" +#include "Microsoft/PortableIndex.h" +#include "Microsoft/Schema/IPortableIndex.h" #include #include +#include "ExecutionContext.h" +using namespace AppInstaller::Utility; using namespace AppInstaller::Registry; using namespace AppInstaller::Registry::Portable; using namespace AppInstaller::Registry::Environment; +using namespace AppInstaller::Repository; +using namespace AppInstaller::Repository::Microsoft; +using namespace AppInstaller::Repository::Microsoft::Schema; -namespace AppInstaller::Portable +namespace AppInstaller::CLI::Portable { namespace { constexpr std::string_view c_PortableIndexFileName = "portable.db"; + constexpr std::string_view s_DefaultSource = "*DefaultSource"sv; - void AppendExeExtension(std::filesystem::path& value) + IPortableIndex::PortableFile CreatePortableFileFromPath(const std::filesystem::path& path) { - if (value.extension() != ".exe") + IPortableIndex::PortableFile portableFile; + portableFile.SetFilePath(path); + + if (std::filesystem::is_directory(path)) + { + portableFile.FileType = IPortableIndex::PortableFileType::Directory; + } + else if (std::filesystem::is_symlink(path)) + { + portableFile.FileType = IPortableIndex::PortableFileType::Symlink; + portableFile.SymlinkTarget = std::filesystem::read_symlink(path).u8string(); + } + else { - value += ".exe"; + portableFile.FileType = IPortableIndex::PortableFileType::File; + + std::ifstream inStream{ path, std::ifstream::binary }; + const Utility::SHA256::HashBuffer& targetFileHash = Utility::SHA256::ComputeHash(inStream); + inStream.close(); + portableFile.SHA256 = Utility::SHA256::ConvertToString(targetFileHash); } + + return portableFile; } } - PortableEntry::PortableEntry(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode, bool isUpdate) : - m_productCode(productCode), m_portableARPEntry(PortableARPEntry(scope, arch, productCode)), m_isUpdate(isUpdate) + std::filesystem::path GetPortableLinksLocation(Manifest::ScopeEnum scope) { - if (Exists()) + if (scope == Manifest::ScopeEnum::Machine) { - DisplayName = GetStringValue(PortableValueName::DisplayName); - DisplayVersion = GetStringValue(PortableValueName::DisplayVersion); - HelpLink = GetStringValue(PortableValueName::HelpLink); - InstallDate = GetStringValue(PortableValueName::InstallDate); - Publisher = GetStringValue(PortableValueName::Publisher); - SHA256 = GetStringValue(PortableValueName::SHA256); - URLInfoAbout = GetStringValue(PortableValueName::URLInfoAbout); - UninstallString = GetStringValue(PortableValueName::UninstallString); - WinGetInstallerType = GetStringValue(PortableValueName::WinGetInstallerType); - WinGetPackageIdentifier = GetStringValue(PortableValueName::WinGetPackageIdentifier); - WinGetSourceIdentifier = GetStringValue(PortableValueName::WinGetSourceIdentifier); - - InstallLocation = GetPathValue(PortableValueName::InstallLocation); - PortableSymlinkFullPath = GetPathValue(PortableValueName::PortableSymlinkFullPath); - PortableTargetFullPath = GetPathValue(PortableValueName::PortableTargetFullPath); - InstallLocation = GetPathValue(PortableValueName::InstallLocation); + return Runtime::GetPathTo(Runtime::PathName::PortableLinksMachineLocation); + } + else + { + return Runtime::GetPathTo(Runtime::PathName::PortableLinksUserLocation); + } + } - InstallDirectoryCreated = GetBoolValue(PortableValueName::InstallDirectoryCreated); - InstallDirectoryAddedToPath = GetBoolValue(PortableValueName::InstallDirectoryAddedToPath); + std::filesystem::path GetPortableInstallRoot(Manifest::ScopeEnum scope, Utility::Architecture arch) + { + if (scope == Manifest::ScopeEnum::Machine) + { + if (arch == Utility::Architecture::X86) + { + return Runtime::GetPathTo(Runtime::PathName::PortablePackageMachineRootX86); + } + else + { + return Runtime::GetPathTo(Runtime::PathName::PortablePackageMachineRootX64); + } + } + else + { + return Runtime::GetPathTo(Runtime::PathName::PortablePackageUserRoot); } } - HRESULT PortableEntry::MultipleInstall(const std::vector& nestedInstallerFiles, const std::vector& extractedItems) + HRESULT PortableInstaller::MultipleInstall(const std::vector& nestedInstallerFiles, const std::vector& extractedItems) { - // Add each file/ directory to database. - if (extractedItems.size() == 0) + const auto& indexPath = InstallLocation / Utility::ConvertToUTF16(c_PortableIndexFileName); + + if (!std::filesystem::exists(indexPath)) + { + PortableIndex::CreateNew(indexPath.u8string(), Schema::Version::Latest()); + } + + PortableIndex portableIndex = PortableIndex::Open(indexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); + + for (auto& item : extractedItems) { - // Record all extracted items and create database if not created. + const auto& itemPath = InstallLocation / item; + // This method should be able to create any portable file based on the extension. + IPortableIndex::PortableFile portableFile = CreatePortableFileFromPath(itemPath); + + if (portableIndex.Exists(portableFile)) + { + portableIndex.UpdatePortableFile(portableFile); + } + else + { + portableIndex.AddPortableFile(portableFile); + } } for (const auto& nestedInstallerFile : nestedInstallerFiles) @@ -70,9 +120,20 @@ namespace AppInstaller::Portable if (!InstallDirectoryAddedToPath) { + // append .exe if needed for portable command alias if it exists, otherwise just use portalbe command alias. const std::filesystem::path& symlinkFullPath = GetPortableLinksLocation(GetScope()) / ConvertToUTF16(nestedInstallerFile.PortableCommandAlias); CreatePortableSymlink(portableTargetPath, symlinkFullPath); - // Record symlink file + + // add portable file only if created symlink is true. + IPortableIndex::PortableFile symlinkPortableFile = CreatePortableFileFromPath(symlinkFullPath); + if (portableIndex.Exists(symlinkPortableFile)) + { + portableIndex.UpdatePortableFile(symlinkPortableFile); + } + else + { + portableIndex.AddPortableFile(symlinkPortableFile); + } } else { @@ -81,30 +142,22 @@ namespace AppInstaller::Portable } AddToPathVariable(); - return ERROR_SUCCESS; } - HRESULT PortableEntry::SingleInstall(const std::filesystem::path& installerPath, const std::filesystem::path& commandAlias, bool rename) + // providing a path and the alias + // providing a list of extracted files and the NestedInstallerFiles, + + HRESULT PortableInstaller::SingleInstall(const std::filesystem::path& installerPath) { // Initial registration. Commit(PortableValueName::WinGetPackageIdentifier, WinGetPackageIdentifier); Commit(PortableValueName::WinGetSourceIdentifier, WinGetSourceIdentifier); - Commit(PortableValueName::UninstallString, UninstallString = "winget uninstall --product-code " + m_productCode); - Commit(PortableValueName::WinGetInstallerType, WinGetInstallerType = InstallerTypeToString(Manifest::InstallerTypeEnum::Portable)); + Commit(PortableValueName::UninstallString, "winget uninstall --product-code " + GetProductCode()); + Commit(PortableValueName::WinGetInstallerType, InstallerTypeToString(Manifest::InstallerTypeEnum::Portable)); Commit(PortableValueName::SHA256, SHA256); - // Move portable exe - if (rename) - { - PortableTargetFullPath = InstallLocation / commandAlias; - } - else - { - PortableTargetFullPath = InstallLocation / installerPath.filename(); - } - AppendExeExtension(PortableTargetFullPath); Commit(PortableValueName::PortableTargetFullPath, PortableTargetFullPath); Commit(PortableValueName::InstallLocation, InstallLocation); MovePortableExe(installerPath); @@ -112,8 +165,8 @@ namespace AppInstaller::Portable // Create symlink if (!InstallDirectoryAddedToPath) { - PortableSymlinkFullPath = GetPortableLinksLocation(GetScope()) / commandAlias; - Commit(PortableValueName::PortableSymlinkFullPath, PortableSymlinkFullPath); + const std::filesystem::path& symlinkPath = PortableSymlinkFullPath; + Commit(PortableValueName::PortableSymlinkFullPath, symlinkPath); CreatePortableSymlink(PortableTargetFullPath, PortableSymlinkFullPath); } @@ -126,7 +179,7 @@ namespace AppInstaller::Portable return ERROR_SUCCESS; } - HRESULT PortableEntry::Uninstall(bool purge) + HRESULT PortableInstaller::Uninstall(bool purge) { // remove portable RemovePortableExe(PortableTargetFullPath, SHA256); @@ -153,7 +206,80 @@ namespace AppInstaller::Portable return ERROR_SUCCESS; } - void PortableEntry::SetAppsAndFeaturesMetadata(const Manifest::AppsAndFeaturesEntry& entry, const Manifest::Manifest& manifest) + HRESULT PortableInstaller::UninstallFromIndex(bool purge) + { + if (purge) + { + std::filesystem::remove(InstallLocation); + return ERROR_SUCCESS; + } + + const auto& indexPath = InstallLocation / Utility::ConvertToUTF16(c_PortableIndexFileName); + if (!std::filesystem::exists(indexPath)) + { + // portable index not found; + return ERROR_NOT_SUPPORTED; + } + + PortableIndex portableIndex = PortableIndex::Open(indexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); + + IPortableIndex::PortableFile file; + int i = 0; + while (portableIndex.GetPortableFileById(i).has_value()) + { + file = portableIndex.GetPortableFileById(i).value(); + + if (file.FileType == IPortableIndex::PortableFileType::File) + { + if (VerifyPortableExeHash(file.GetFilePath(), file.SHA256)) + { + std::filesystem::remove(file.GetFilePath()); + portableIndex.RemovePortableFile(file); + } + else + { + i++; + } + } + else if (file.FileType == IPortableIndex::PortableFileType::Directory) + { + std::filesystem::remove(file.GetFilePath()); + portableIndex.RemovePortableFile(file); + } + else if (file.FileType == IPortableIndex::PortableFileType::Symlink) + { + if (VerifySymlinkTarget(file.GetFilePath(), file.SymlinkTarget)) + { + std::filesystem::remove(file.GetFilePath()); + portableIndex.RemovePortableFile(file); + } + else + { + i++; + } + } + } + + // remove install directory + RemovePortableDirectory(InstallLocation, purge, InstallDirectoryCreated); + + // remove path + const std::filesystem::path& pathValue = GetPathDirectory(); + if (RemoveFromPathVariable()) + { + AICLI_LOG(CLI, Info, << "Removed target directory from PATH registry: " << pathValue); + } + else + { + AICLI_LOG(CLI, Info, << "Target directory not removed from PATH registry: " << pathValue); + } + + m_portableARPEntry.Delete(); + AICLI_LOG(CLI, Info, << "PortableARPEntry deleted."); + return ERROR_SUCCESS; + } + + void PortableInstaller::SetAppsAndFeaturesMetadata(const Manifest::AppsAndFeaturesEntry& entry, const Manifest::Manifest& manifest) { Commit(PortableValueName::DisplayName, DisplayName = entry.DisplayName); Commit(PortableValueName::DisplayVersion, DisplayVersion = entry.DisplayVersion); @@ -163,7 +289,7 @@ namespace AppInstaller::Portable Commit(PortableValueName::HelpLink, HelpLink = manifest.CurrentLocalization.Get < Manifest::Localization::PublisherSupportUrl>()); } - void PortableEntry::MovePortableExe(const std::filesystem::path& installerPath) + void PortableInstaller::MovePortableExe(const std::filesystem::path& installerPath) { bool isDirectoryCreated = false; if (std::filesystem::create_directories(InstallLocation)) @@ -189,7 +315,7 @@ namespace AppInstaller::Portable } } - bool PortableEntry::VerifyPortableExeHash(const std::filesystem::path& targetPath, const std::string& hashValue) + bool PortableInstaller::VerifyPortableExeHash(const std::filesystem::path& targetPath, const std::string& hashValue) { std::ifstream inStream{ targetPath, std::ifstream::binary }; const Utility::SHA256::HashBuffer& targetFileHash = Utility::SHA256::ComputeHash(inStream); @@ -198,14 +324,13 @@ namespace AppInstaller::Portable return Utility::SHA256::AreEqual(Utility::SHA256::ConvertToBytes(hashValue), targetFileHash); } - void PortableEntry::AddToPathVariable() + void PortableInstaller::AddToPathVariable() { const std::filesystem::path& pathValue = GetPathDirectory(); if (PathVariable(GetScope()).Append(pathValue)) { AICLI_LOG(Core, Info, << "Appended target directory to PATH registry: " << pathValue); - //context.Reporter.Warn() << Resource::String::ModifiedPathRequiresShellRestart << std::endl; - m_addedToPath = true; + m_stream << Resource::String::ModifiedPathRequiresShellRestart << std::endl; } else { @@ -213,7 +338,7 @@ namespace AppInstaller::Portable } } - void PortableEntry::CreatePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath) + void PortableInstaller::CreatePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath) { std::filesystem::file_status status = std::filesystem::status(symlinkPath); if (std::filesystem::is_directory(status)) @@ -225,7 +350,7 @@ namespace AppInstaller::Portable if (std::filesystem::remove(symlinkPath)) { AICLI_LOG(CLI, Info, << "Removed existing file at " << symlinkPath); - //m_stream << Resource::String::OverwritingExistingFileAtMessage << ' ' << symlinkPath.u8string() << std::endl; + m_stream << Resource::String::OverwritingExistingFileAtMessage << ' ' << symlinkPath.u8string() << std::endl; } if (Filesystem::CreateSymlink(targetPath, symlinkPath)) @@ -241,7 +366,7 @@ namespace AppInstaller::Portable } } - bool PortableEntry::VerifySymlinkTarget(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath) + bool PortableInstaller::VerifySymlinkTarget(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath) { AICLI_LOG(Core, Info, << "Expected portable target path: " << targetPath); const std::filesystem::path& symlinkTargetPath = std::filesystem::read_symlink(symlinkPath); @@ -258,7 +383,7 @@ namespace AppInstaller::Portable } } - void PortableEntry::RemovePortableExe(const std::filesystem::path& targetPath, const std::string& hash) + void PortableInstaller::RemovePortableExe(const std::filesystem::path& targetPath, const std::string& hash) { if (std::filesystem::exists(targetPath)) { @@ -286,7 +411,7 @@ namespace AppInstaller::Portable } } - void PortableEntry::RemovePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath) + void PortableInstaller::RemovePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath) { if (!std::filesystem::is_symlink(std::filesystem::symlink_status(symlinkPath))) { @@ -308,7 +433,7 @@ namespace AppInstaller::Portable } } - void PortableEntry::RemovePortableDirectory(const std::filesystem::path& directoryPath, bool purge, bool isCreated) + void PortableInstaller::RemovePortableDirectory(const std::filesystem::path& directoryPath, bool purge, bool isCreated) { if (std::filesystem::exists(directoryPath)) { @@ -336,7 +461,7 @@ namespace AppInstaller::Portable } else { - //context.Reporter.Warn() << Resource::String::FilesRemainInInstallDirectory << installDirectory << std::endl; + //m_stream << Resource::String::FilesRemainInInstallDirectory << directoryPath << std::endl; } } else @@ -345,7 +470,7 @@ namespace AppInstaller::Portable } } - bool PortableEntry::RemoveFromPathVariable() + bool PortableInstaller::RemoveFromPathVariable() { bool removeFromPath = true; std::filesystem::path pathValue = GetPathDirectory(); @@ -368,7 +493,46 @@ namespace AppInstaller::Portable return false; } } - std::string PortableEntry::GetStringValue(PortableValueName valueName) + + PortableInstaller::PortableInstaller(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode) : + m_portableARPEntry(PortableARPEntry(scope, arch, productCode)) + { + Initialize(); + } + + void PortableInstaller::Initialize() + { + // Initialize all values if present + if (Exists()) + { + DisplayName = GetStringValue(PortableValueName::DisplayName); + DisplayVersion = GetStringValue(PortableValueName::DisplayVersion); + HelpLink = GetStringValue(PortableValueName::HelpLink); + InstallDate = GetStringValue(PortableValueName::InstallDate); + Publisher = GetStringValue(PortableValueName::Publisher); + SHA256 = GetStringValue(PortableValueName::SHA256); + URLInfoAbout = GetStringValue(PortableValueName::URLInfoAbout); + UninstallString = GetStringValue(PortableValueName::UninstallString); + WinGetInstallerType = GetStringValue(PortableValueName::WinGetInstallerType); + WinGetPackageIdentifier = GetStringValue(PortableValueName::WinGetPackageIdentifier); + WinGetSourceIdentifier = GetStringValue(PortableValueName::WinGetSourceIdentifier); + + InstallLocation = GetPathValue(PortableValueName::InstallLocation); + PortableSymlinkFullPath = GetPathValue(PortableValueName::PortableSymlinkFullPath); + PortableTargetFullPath = GetPathValue(PortableValueName::PortableTargetFullPath); + InstallLocation = GetPathValue(PortableValueName::InstallLocation); + + InstallDirectoryCreated = GetBoolValue(PortableValueName::InstallDirectoryCreated); + InstallDirectoryAddedToPath = GetBoolValue(PortableValueName::InstallDirectoryAddedToPath); + } + } + + std::filesystem::path PortableInstaller::GetPortableIndexPath() + { + return InstallLocation / c_PortableIndexFileName; + } + + std::string PortableInstaller::GetStringValue(PortableValueName valueName) { if (m_portableARPEntry[valueName].has_value()) { @@ -380,7 +544,7 @@ namespace AppInstaller::Portable } } - std::filesystem::path PortableEntry::GetPathValue(PortableValueName valueName) + std::filesystem::path PortableInstaller::GetPathValue(PortableValueName valueName) { if (m_portableARPEntry[valueName].has_value()) { @@ -391,7 +555,7 @@ namespace AppInstaller::Portable } } - bool PortableEntry::GetBoolValue(PortableValueName valueName) + bool PortableInstaller::GetBoolValue(PortableValueName valueName) { if (m_portableARPEntry[valueName].has_value()) { diff --git a/src/AppInstallerCommonCore/Public/winget/PortableEntry.h b/src/AppInstallerCLICore/PortableInstaller.h similarity index 65% rename from src/AppInstallerCommonCore/Public/winget/PortableEntry.h rename to src/AppInstallerCLICore/PortableInstaller.h index 23d07445b1..fbec77ac2b 100644 --- a/src/AppInstallerCommonCore/Public/winget/PortableEntry.h +++ b/src/AppInstallerCLICore/PortableInstaller.h @@ -2,18 +2,22 @@ // Licensed under the MIT License. #pragma once #include -#include #include "winget/PortableARPEntry.h" -#include #include #include using namespace AppInstaller::Registry; using namespace AppInstaller::Registry::Portable; -namespace AppInstaller::Portable +namespace AppInstaller::CLI::Portable { - struct PortableEntry + // Helper methods for relevant locations. + std::filesystem::path GetPortableLinksLocation(Manifest::ScopeEnum scope); + + std::filesystem::path GetPortableInstallRoot(Manifest::ScopeEnum scope, Utility::Architecture arch); + + // Object representation of the arguments needed to install a portable. + struct PortableInstaller { std::string DisplayName; std::string DisplayVersion; @@ -31,12 +35,7 @@ namespace AppInstaller::Portable std::string WinGetPackageIdentifier; std::string WinGetSourceIdentifier; bool InstallDirectoryAddedToPath = false; - - HRESULT SingleInstall(const std::filesystem::path& installerPath, const std::filesystem::path& alias, bool rename = false); - - HRESULT MultipleInstall(const std::vector& nestedInstallerFiles, const std::vector& extractedItems); - - HRESULT Uninstall(bool purge = false); + bool IsUpdate = false; template void Commit(PortableValueName valueName, T value) @@ -46,62 +45,66 @@ namespace AppInstaller::Portable Manifest::ScopeEnum GetScope() { return m_portableARPEntry.GetScope(); }; + Utility::Architecture GetArch() { return m_portableARPEntry.GetArchitecture(); }; + + std::string GetProductCode() { return m_portableARPEntry.GetProductCode(); }; + + std::string GetPackageIdentifier() { return m_packageIdentifier; }; + + std::string GetSourceIdentifier() { return m_sourceIdentifier; }; + bool Exists() { return m_portableARPEntry.Exists(); }; - PortableEntry(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode, bool isUpdate = false); + PortableInstaller(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode); + + HRESULT SingleInstall(const std::filesystem::path& installerPath); + HRESULT MultipleInstall(const std::vector& nestedInstallerFiles, const std::vector& extractedItems); + HRESULT Uninstall(bool purge = false); + HRESULT UninstallFromIndex(bool purge = false); std::filesystem::path GetPathDirectory() const { return InstallDirectoryAddedToPath ? InstallLocation : PortableSymlinkFullPath.parent_path(); } - static std::filesystem::path GetPortableLinksLocation(Manifest::ScopeEnum scope) + void SetAppsAndFeaturesMetadata(const Manifest::AppsAndFeaturesEntry& entry, const Manifest::Manifest& manifest); + + void MovePortableExe(const std::filesystem::path& installerPath); + + void CreatePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath); + + bool VerifyPackageAndSourceIdentifiers() { - if (scope == Manifest::ScopeEnum::Machine) + if (Exists()) { - return Runtime::GetPathTo(Runtime::PathName::PortableLinksMachineLocation); + return m_portableARPEntry.IsSamePortablePackageEntry(m_packageIdentifier, m_sourceIdentifier); } else { - return Runtime::GetPathTo(Runtime::PathName::PortableLinksUserLocation); + return true; } - } + }; - static std::filesystem::path GetPortableInstallLocation(Manifest::ScopeEnum scope, Utility::Architecture arch) + std::string GetOutputMessage() { - if (scope == Manifest::ScopeEnum::Machine) - { - if (arch == Utility::Architecture::X86) - { - return Runtime::GetPathTo(Runtime::PathName::PortablePackageMachineRootX86); - } - else - { - return Runtime::GetPathTo(Runtime::PathName::PortablePackageMachineRootX64); - } - } - else - { - return Runtime::GetPathTo(Runtime::PathName::PortablePackageUserRoot); - } + return m_stream.str(); } - void SetAppsAndFeaturesMetadata(const Manifest::AppsAndFeaturesEntry& entry, const Manifest::Manifest& manifest); - - void MovePortableExe(const std::filesystem::path& installerPath); - - void CreatePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath); - - bool IsUpdate() { return m_isUpdate; }; + std::filesystem::path GetPortableIndexPath(); private: - bool m_isUpdate = false; - bool m_addedToPath = false; - std::string m_productCode; PortableARPEntry m_portableARPEntry; - + std::string m_packageIdentifier; + std::string m_sourceIdentifier; std::stringstream m_stream; + void Initialize(); + + std::string GetStringValue(PortableValueName valueName); + std::filesystem::path GetPathValue(PortableValueName valueName); + + bool GetBoolValue(PortableValueName valueName); + bool VerifySymlinkTarget(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath); bool VerifyPortableExeHash(const std::filesystem::path& targetPath, const std::string& hashValue); @@ -111,9 +114,5 @@ namespace AppInstaller::Portable void AddToPathVariable(); bool RemoveFromPathVariable(); - - std::string GetStringValue(PortableValueName valueName); - std::filesystem::path GetPathValue(PortableValueName valueName); - bool GetBoolValue(PortableValueName valueName); }; } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index be31921a12..63d01ae9af 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -230,7 +230,6 @@ 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 index 53133832bf..948a7f642b 100644 --- a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp @@ -5,6 +5,7 @@ #include "winget/Archive.h" #include "winget/Filesystem.h" #include "PortableFlow.h" +#include "PortableInstaller.h" using namespace AppInstaller::Manifest; @@ -20,6 +21,10 @@ namespace AppInstaller::CLI::Workflow if (context.Get()->NestedInstallerType == InstallerTypeEnum::Portable) { destinationFolder = GetPortableTargetDirectory(context); + + // temporarily creating directory now + std::filesystem::create_directory(destinationFolder); + hr = AppInstaller::Archive::TryExtractArchive(installerPath, destinationFolder, extractedItems); context.Add(extractedItems); } @@ -27,7 +32,6 @@ namespace AppInstaller::CLI::Workflow { destinationFolder = installerPath.parent_path(); hr = AppInstaller::Archive::TryExtractArchive(installerPath, destinationFolder, extractedItems); - } AICLI_LOG(CLI, Info, << "Extracting archive to: " << destinationFolder); @@ -54,28 +58,40 @@ namespace AppInstaller::CLI::Workflow AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INVALID_MANIFEST); } - const auto& installerPath = context.Get(); - const auto& installerParentPath = installerPath.parent_path(); - const auto& relativeFilePath = ConvertToUTF16(installer.NestedInstallerFiles[0].RelativeFilePath); - - const std::filesystem::path& nestedInstallerPath = installerParentPath / relativeFilePath; - if (Filesystem::PathEscapesBaseDirectory(nestedInstallerPath, installerParentPath)) + InstallerTypeEnum nestedInstallerType = installer.NestedInstallerType; + std::filesystem::path destinationFolder; + if (nestedInstallerType == InstallerTypeEnum::Portable) { - AICLI_LOG(CLI, Error, << "Path points to a location outside of the install directory: " << nestedInstallerPath); - context.Reporter.Error() << Resource::String::InvalidPathToNestedInstaller << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NESTEDINSTALLER_INVALID_PATH); + destinationFolder = GetPortableTargetDirectory(context); } - else if (!std::filesystem::exists(nestedInstallerPath)) + else { - 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); + destinationFolder = context.Get().parent_path(); } - else + + for (const auto& nestedInstallerFile : installer.NestedInstallerFiles) { - AICLI_LOG(CLI, Info, << "Setting installerPath to: " << nestedInstallerPath); - context.Add(nestedInstallerPath); + const std::filesystem::path& nestedInstallerPath = destinationFolder / ConvertToUTF16(nestedInstallerFile.RelativeFilePath); + + if (Filesystem::PathEscapesBaseDirectory(nestedInstallerPath, destinationFolder)) + { + AICLI_LOG(CLI, Error, << "Path points to a location outside of the install directory: " << nestedInstallerPath); + context.Reporter.Error() << Resource::String::InvalidPathToNestedInstaller << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NESTEDINSTALLER_INVALID_PATH); + } + else 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 if (nestedInstallerType != InstallerTypeEnum::Portable) + { + // Only update the installerPath if it points to a non-portable installer. + AICLI_LOG(CLI, Info, << "Setting installerPath to: " << nestedInstallerPath); + context.Add(nestedInstallerPath); + } } } diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp index 75a00f2d1b..1e852ccebe 100644 --- a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -334,8 +334,7 @@ namespace AppInstaller::CLI::Workflow void PortableInstall(Execution::Context& context) { context << - GetPortableInstallInfo << - VerifyPortableRegistryMatch << + GetPortableEntryForInstall << PortableInstallImpl << ReportInstallerResult("Portable"sv, APPINSTALLER_CLI_ERROR_PORTABLE_INSTALL_FAILED, true); } @@ -474,7 +473,6 @@ namespace AppInstaller::CLI::Workflow context << Workflow::EnsureFeatureEnabledForArchiveInstall << Workflow::EnsureSupportForPortableInstall << - Workflow::EnsureNonPortableTypeForArchiveInstall << Workflow::EnsureValidNestedInstallerMetadataForArchiveInstall; } diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp index 8b24e4840a..1110b8b4c9 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -4,25 +4,33 @@ #include "PortableFlow.h" #include "WorkflowBase.h" #include "winget/Filesystem.h" -#include "winget/PortableARPEntry.h" -#include "winget/PortableEntry.h" +#include "PortableInstaller.h" #include "winget/PathVariable.h" using namespace AppInstaller::Manifest; -using namespace AppInstaller::Portable; using namespace AppInstaller::Utility; using namespace AppInstaller::Registry; using namespace AppInstaller::Registry::Portable; using namespace AppInstaller::Registry::Environment; using namespace AppInstaller::Repository; +using namespace AppInstaller::CLI::Portable; using namespace std::filesystem; namespace AppInstaller::CLI::Workflow { namespace { + constexpr std::string_view c_PortableIndexFileName = "portable.db"; constexpr std::string_view s_DefaultSource = "*DefaultSource"sv; + void AppendExeExtension(std::filesystem::path& value) + { + if (value.extension() != ".exe") + { + value += ".exe"; + } + } + std::string GetPortableProductCode(Execution::Context& context) { const std::string& packageId = context.Get().Id; @@ -40,39 +48,54 @@ namespace AppInstaller::CLI::Workflow return MakeSuitablePathPart(packageId + "_" + source); } - std::filesystem::path GetPortableInstallRoot(Manifest::ScopeEnum scope, Utility::Architecture arch) + std::filesystem::path GetPortableTargetFullPath(Execution::Context& context) { - if (scope == Manifest::ScopeEnum::Machine) + const std::filesystem::path& installerPath = context.Get(); + const std::filesystem::path& targetInstallDirectory = GetPortableTargetDirectory(context); + std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); + + std::filesystem::path fileName; + if (!renameArg.empty()) { - if (arch == Utility::Architecture::X86) - { - return Runtime::GetPathTo(Runtime::PathName::PortablePackageMachineRootX86); - } - else - { - return Runtime::GetPathTo(Runtime::PathName::PortablePackageMachineRootX64); - } + fileName = ConvertToUTF16(renameArg); } else { - return Runtime::GetPathTo(Runtime::PathName::PortablePackageUserRoot); + fileName = installerPath.filename(); } + + AppendExeExtension(fileName); + return targetInstallDirectory / fileName; } - std::filesystem::path GetPortableLinksLocation(Manifest::ScopeEnum scope) + std::filesystem::path GetPortableSymlinkFullPath(Execution::Context& context) { - if (scope == Manifest::ScopeEnum::Machine) + const std::filesystem::path& installerPath = context.Get(); + const std::vector& commands = context.Get()->Commands; + Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); + std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); + + std::filesystem::path commandAlias; + if (!renameArg.empty()) { - return Runtime::GetPathTo(Runtime::PathName::PortableLinksMachineLocation); + commandAlias = ConvertToUTF16(renameArg); } else { - return Runtime::GetPathTo(Runtime::PathName::PortableLinksUserLocation); + if (!commands.empty()) + { + commandAlias = ConvertToUTF16(commands[0]); + } + else + { + commandAlias = installerPath.filename(); + } } + + AppendExeExtension(commandAlias); + return GetPortableLinksLocation(scope) / commandAlias; } - - Manifest::AppsAndFeaturesEntry GetAppsAndFeaturesEntryForPortableInstall( const std::vector& appsAndFeaturesEntries, const AppInstaller::Manifest::Manifest& manifest) @@ -141,67 +164,28 @@ namespace AppInstaller::CLI::Workflow } } - std::filesystem::path GetPortableTargetDirectory(Execution::Context& context) - { + std::filesystem::path GetPortableTargetDirectory(Execution::Context& context) + { Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); Utility::Architecture arch = context.Get()->Arch; std::string_view locationArg = context.Args.GetArg(Execution::Args::Type::InstallLocation); - std::filesystem::path targetInstallDirectory; - - if (!locationArg.empty()) - { - targetInstallDirectory = std::filesystem::path{ ConvertToUTF16(locationArg) }; - } - else - { - const std::string& productCode = GetPortableProductCode(context); - targetInstallDirectory = GetPortableInstallRoot(scope, arch); - targetInstallDirectory /= ConvertToUTF16(productCode); - } - - return targetInstallDirectory; - } - - void VerifyPortableRegistryMatch(Execution::Context& context) - { - const std::string& packageIdentifier = context.Get().Id; - - std::string sourceIdentifier; - if (context.Contains(Execution::Data::PackageVersion)) - { - sourceIdentifier = context.Get()->GetSource().GetIdentifier(); - } - else - { - sourceIdentifier = s_DefaultSource; - } + std::filesystem::path targetInstallDirectory; - Portable::PortableEntry& portableEntry = context.Get(); - - if (portableEntry.Exists()) + if (!locationArg.empty()) { - if (packageIdentifier != portableEntry.WinGetPackageIdentifier || sourceIdentifier != portableEntry.WinGetSourceIdentifier) - { - // TODO: Replace HashOverride with --Force when argument behavior gets updated. - if (!context.Args.Contains(Execution::Args::Type::HashOverride)) - { - AICLI_LOG(CLI, Error, << "Registry match failed, skipping write to uninstall registry"); - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS); - } - else - { - AICLI_LOG(CLI, Info, << "Overriding registry match check..."); - context.Reporter.Warn() << Resource::String::PortableRegistryCollisionOverridden << std::endl; - } - } - } - - portableEntry.WinGetPackageIdentifier = packageIdentifier; - portableEntry.WinGetSourceIdentifier = sourceIdentifier; + targetInstallDirectory = std::filesystem::path{ ConvertToUTF16(locationArg) }; + } + else + { + const std::string& productCode = GetPortableProductCode(context); + targetInstallDirectory = GetPortableInstallRoot(scope, arch); + targetInstallDirectory /= ConvertToUTF16(productCode); + } + + return targetInstallDirectory; } - - void GetPortableInstallInfo(Execution::Context& context) + void GetPortableEntryForInstall(Execution::Context& context) { Manifest::ScopeEnum scope = Manifest::ScopeEnum::Unknown; bool isUpdate = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate); @@ -216,96 +200,84 @@ namespace AppInstaller::CLI::Workflow } else { - scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); - } - - PortableEntry portableEntry = PortableEntry(scope, context.Get()->Arch, GetPortableProductCode(context), isUpdate); - portableEntry.InstallLocation = GetPortableTargetDirectory(context); - portableEntry.SHA256 = SHA256::ConvertToString(context.Get().second); - - const AppInstaller::Manifest::Manifest& manifest = context.Get(); - const Manifest::AppsAndFeaturesEntry& entry = GetAppsAndFeaturesEntryForPortableInstall(context.Get()->AppsAndFeaturesEntries, manifest); - portableEntry.SetAppsAndFeaturesMetadata(entry, manifest); - context.Add(std::move(portableEntry)); + scope = Manifest::ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); + } + + Utility::Architecture arch = context.Get()->Arch; + const std::string& productCode = GetPortableProductCode(context); + + PortableInstaller portableInstaller = PortableInstaller(scope, arch, productCode); + portableInstaller.SHA256 = Utility::SHA256::ConvertToString(context.Get().second); + portableInstaller.InstallLocation = GetPortableTargetDirectory(context); + portableInstaller.IsUpdate = isUpdate; + + // These are not needed if installing from archive with multiple portables + // But we'll set them regardless for convenience. + portableInstaller.PortableTargetFullPath = GetPortableTargetFullPath(context); + portableInstaller.PortableSymlinkFullPath = GetPortableSymlinkFullPath(context); + + context.Add(std::move(portableInstaller)); } void PortableInstallImpl(Execution::Context& context) { - Portable::PortableEntry& portableEntry = context.Get(); + HRESULT result = ERROR_SUCCESS; + PortableInstaller& portableInstaller = context.Get(); try { context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl; - HRESULT result; - const std::filesystem::path& installerPath = context.Get(); - if (IsArchiveType(context.Get()->BaseInstallerType)) { const std::vector& extractedItems = context.Get(); const std::vector& nestedInstallerFiles = context.Get()->NestedInstallerFiles; - result = portableEntry.MultipleInstall(nestedInstallerFiles, extractedItems); + result = portableInstaller.MultipleInstall(nestedInstallerFiles, extractedItems); } else { - std::filesystem::path commandAlias; - bool rename = false; - std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); - const std::vector& commands = context.Get()->Commands; - - if (!renameArg.empty()) - { - rename = true; - commandAlias = ConvertToUTF16(renameArg); - } - else if (!commands.empty()) - { - commandAlias = ConvertToUTF16(commands[0]); - } - else - { - commandAlias = installerPath.filename(); - } - - result = portableEntry.SingleInstall(installerPath, commandAlias, rename); + const std::filesystem::path& installerPath = context.Get(); + result = portableInstaller.SingleInstall(installerPath); } context.Add(result); + context.Reporter.Warn() << portableInstaller.GetOutputMessage(); } catch (...) { - // fix this to handle. context.Add(Workflow::HandleException(context, std::current_exception())); } // Perform cleanup only if the install fails and is not an update. - const auto& installReturnCode = context.Get(); - - if (installReturnCode != 0 && installReturnCode != APPINSTALLER_CLI_ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS && !portableEntry.IsUpdate()) + if (result != ERROR_SUCCESS && !portableInstaller.IsUpdate) { context.Reporter.Warn() << Resource::String::PortableInstallFailed << std::endl; - auto uninstallPortableContextPtr = context.CreateSubContext(); - Execution::Context& uninstallPortableContext = *uninstallPortableContextPtr; - auto previousThreadGlobals = uninstallPortableContext.SetForCurrentThread(); - - uninstallPortableContext.Add(context.Get()); - uninstallPortableContext << PortableUninstallImpl; + result = portableInstaller.Uninstall(); + context.Add(result); } } void PortableUninstallImpl(Execution::Context& context) { HRESULT result; + PortableInstaller& portableInstaller = context.Get(); try { - context.Reporter.Info() << Resource::String::UninstallFlowStartingPackageUninstall << std::endl; + context.Reporter.Info() << Resource::String::UninstallFlowStartingPackageUninstall << std::endl; - PortableEntry& portableEntry = context.Get(); - bool shouldPurge = context.Args.Contains(Execution::Args::Type::Purge) || - (!portableEntry.IsUpdate() && Settings::User().Get() && !context.Args.Contains(Execution::Args::Type::Preserve)); + bool purge = context.Args.Contains(Execution::Args::Type::Purge) || + (!portableInstaller.IsUpdate && Settings::User().Get() && !context.Args.Contains(Execution::Args::Type::Preserve)); - result = portableEntry.Uninstall(shouldPurge); + const auto& indexPath = portableInstaller.GetPortableIndexPath(); + if (std::filesystem::exists(indexPath)) + { + result = portableInstaller.UninstallFromIndex(purge); + } + else + { + result = portableInstaller.Uninstall(purge); + } } catch (HRESULT error) { @@ -313,6 +285,7 @@ namespace AppInstaller::CLI::Workflow } context.Add(result); + context.Reporter.Warn() << portableInstaller.GetOutputMessage(); } void EnsureSupportForPortableInstall(Execution::Context& context) @@ -341,16 +314,4 @@ namespace AppInstaller::CLI::Workflow } } } - - // TODO: remove this check once support for portable in archive has been implemented - void EnsureNonPortableTypeForArchiveInstall(Execution::Context& context) - { - auto nestedInstallerType = context.Get().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 index 7526021791..cdaf5b0d4f 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.h +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.h @@ -21,15 +21,8 @@ namespace AppInstaller::CLI::Workflow void EnsureSupportForPortableUninstall(Execution::Context& context); - void EnsureNonPortableTypeForArchiveInstall(Execution::Context& context); - - // Gets the portable install info from the context. - // Required Args: None - // Inputs: Manifest?, Installer, InstallerPath - // Outputs: InstallerArgs - void GetPortableInstallInfo(Execution::Context& context); - - void VerifyPortableRegistryMatch(Execution::Context& context); + void GetPortableEntryForInstall(Execution::Context& context); + // Returns the target install directory for the portable package. std::filesystem::path GetPortableTargetDirectory(Execution::Context& context); } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp b/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp index ff0abadb65..ae79875266 100644 --- a/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp @@ -7,8 +7,6 @@ #include "ShellExecuteInstallerHandler.h" #include "AppInstallerMsixInfo.h" #include "PortableFlow.h" -#include "winget/PortableARPEntry.h" - #include using namespace AppInstaller::CLI::Execution; @@ -16,6 +14,7 @@ using namespace AppInstaller::Manifest; using namespace AppInstaller::Msix; using namespace AppInstaller::Repository; using namespace AppInstaller::Registry; +using namespace AppInstaller::CLI::Portable; namespace AppInstaller::CLI::Workflow { @@ -139,16 +138,14 @@ namespace AppInstaller::CLI::Workflow AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_UNINSTALL_INFO_FOUND); } - const std::string installedScope = context.Get()->GetMetadata()[Repository::PackageVersionMetadata::InstalledScope]; + const std::string installedScope = context.Get()->GetMetadata()[Repository::PackageVersionMetadata::InstalledScope]; const std::string installedArch = context.Get()->GetMetadata()[Repository::PackageVersionMetadata::InstalledArchitecture]; - bool isUpdate = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate); - Portable::PortableEntry portableEntry = Portable::PortableEntry( - ConvertToScopeEnum(installedScope), - Utility::ConvertToArchitectureEnum(installedArch), - productCodes[0], - isUpdate); - - context.Add(std::move(portableEntry)); + + PortableInstaller portableInstaller = PortableInstaller( + Manifest::ConvertToScopeEnum(installedScope), + Utility::ConvertToArchitectureEnum(installedArch), + productCodes[0]); + context.Add(std::move(portableInstaller)); break; } default: diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index ac583d45ae..33d661b0de 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -1375,9 +1375,6 @@ Please specify one of them using the `--source` option to proceed. A provided argument is not supported for this package - - Installing a portable package from an archive is not yet supported - Failed to extract the contents of the archive diff --git a/src/AppInstallerCLITests/PortableEntry.cpp b/src/AppInstallerCLITests/PortableEntry.cpp index 718d2fce72..bc03b431f8 100644 --- a/src/AppInstallerCLITests/PortableEntry.cpp +++ b/src/AppInstallerCLITests/PortableEntry.cpp @@ -3,17 +3,17 @@ #include "pch.h" #include "TestCommon.h" #include -#include +#include #include #include -using namespace AppInstaller::Portable; using namespace AppInstaller::Utility; +using namespace AppInstaller::CLI::Portable; using namespace TestCommon; TEST_CASE("VerifyPortableMove", "[PortableEntry]") { - PortableEntry testEntry = PortableEntry( + PortableInstaller testEntry = PortableInstaller( AppInstaller::Manifest::ScopeEnum::User, Architecture::X64, "testProductCode"); @@ -31,7 +31,7 @@ TEST_CASE("VerifyPortableMove", "[PortableEntry]") REQUIRE(testEntry.InstallDirectoryCreated); // Create a second PortableEntry instance to emulate installing for a second time. (ARP entry should already exist) - PortableEntry testEntry2 = PortableEntry( + PortableInstaller testEntry2 = PortableInstaller( AppInstaller::Manifest::ScopeEnum::User, Architecture::X64, "testProductCode"); @@ -53,7 +53,7 @@ TEST_CASE("VerifyPortableMove", "[PortableEntry]") TEST_CASE("VerifySymlinkCheck", "[PortableEntry]") { - PortableEntry testEntry = PortableEntry( + PortableInstaller testEntry = PortableInstaller( AppInstaller::Manifest::ScopeEnum::User, Architecture::X64, "testProductCode"); diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj index 2998e0c63b..fb09a10e6d 100644 --- a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj +++ b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -329,7 +329,6 @@ - @@ -405,7 +404,6 @@ - diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters index 53ea8a4049..11eeff0e44 100644 --- a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters +++ b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -192,9 +192,6 @@ Public\winget - - Public\winget - Public\winget @@ -362,9 +359,6 @@ Source Files - - Source Files - Source Files diff --git a/src/AppInstallerCommonCore/Archive.cpp b/src/AppInstallerCommonCore/Archive.cpp index 57e9af1905..f8c122842e 100644 --- a/src/AppInstallerCommonCore/Archive.cpp +++ b/src/AppInstallerCommonCore/Archive.cpp @@ -36,6 +36,7 @@ namespace AppInstaller::Archive 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)); + // Append the full path location of the extracted item extractedItems.emplace_back(szFolderName); } diff --git a/src/AppInstallerCommonCore/PortableARPEntry.cpp b/src/AppInstallerCommonCore/PortableARPEntry.cpp index 73a39f214c..1889481f4c 100644 --- a/src/AppInstallerCommonCore/PortableARPEntry.cpp +++ b/src/AppInstallerCommonCore/PortableARPEntry.cpp @@ -36,6 +36,7 @@ namespace AppInstaller::Registry::Portable { m_scope = scope; m_arch = arch; + m_productCode = productCode; if (m_scope == Manifest::ScopeEnum::Machine) { @@ -59,7 +60,7 @@ namespace AppInstaller::Registry::Portable m_samDesired = KEY_WOW64_64KEY; } - m_subKey += L"\\" + ConvertToUTF16(productCode); + m_subKey += L"\\" + ConvertToUTF16(m_productCode); m_key = Key::OpenIfExists(m_root, m_subKey, 0, KEY_ALL_ACCESS); if (m_key != NULL) { diff --git a/src/AppInstallerCommonCore/Public/winget/PortableARPEntry.h b/src/AppInstallerCommonCore/Public/winget/PortableARPEntry.h index caf9bce8ff..8caf202383 100644 --- a/src/AppInstallerCommonCore/Public/winget/PortableARPEntry.h +++ b/src/AppInstallerCommonCore/Public/winget/PortableARPEntry.h @@ -47,9 +47,11 @@ namespace AppInstaller::Registry::Portable Registry::Key GetKey() { return m_key; }; Manifest::ScopeEnum GetScope() { return m_scope; }; Utility::Architecture GetArchitecture() { return m_arch; }; + std::string GetProductCode() { return m_productCode; }; private: bool m_exists = false; + std::string m_productCode; Key m_key; HKEY m_root; std::wstring m_subKey; diff --git a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp index 3b09ee00a0..451cf3f2f6 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp @@ -74,6 +74,19 @@ namespace AppInstaller::Repository::Microsoft return result; } + std::optional PortableIndex::GetPortableFileById(SQLite::rowid_t rowId) + { + // change to using rowid number. + AICLI_LOG(Repo, Verbose, << "Retrieving portable file at row [" << rowId << "]"); + return m_interface->GetPortableFileById(m_dbconn, rowId); + } + + bool PortableIndex::Exists(const Schema::IPortableIndex::PortableFile& file) + { + AICLI_LOG(Repo, Verbose, << "Checking if portable file exists [" << file.GetFilePath() << "]"); + return m_interface->Exists(m_dbconn, file); + } + std::unique_ptr PortableIndex::CreateIPortableIndex() const { if (m_version == Schema::Version{ 1, 0 } || diff --git a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h index 2becefd82c..c104b9344e 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h @@ -35,6 +35,10 @@ namespace AppInstaller::Repository::Microsoft bool UpdatePortableFile(const Schema::IPortableIndex::PortableFile& file); + std::optional GetPortableFileById(SQLite::rowid_t rowId); + + bool Exists(const Schema::IPortableIndex::PortableFile& file); + private: // Constructor used to open an existing index. PortableIndex(const std::string& target, SQLiteStorageBase::OpenDisposition disposition, Utility::ManagedFile&& indexFile); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/IPortableIndex.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/IPortableIndex.h index 2ac19f9543..f0c1c1ebb8 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/IPortableIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/IPortableIndex.h @@ -26,7 +26,7 @@ namespace AppInstaller::Repository::Microsoft::Schema std::string SHA256; std::string SymlinkTarget; - void SetFilePath(const std::filesystem::path& path) { m_filePath = std::filesystem::weakly_canonical(path); }; + void SetFilePath(const std::filesystem::path& path) { m_filePath = std::filesystem::absolute(path); }; std::filesystem::path GetFilePath() const { return m_filePath; }; @@ -52,5 +52,11 @@ namespace AppInstaller::Repository::Microsoft::Schema // Updates the file with matching FilePath in the index. // The return value indicates whether the index was modified by the function. virtual std::pair UpdatePortableFile(SQLite::Connection& connection, const PortableFile& file) = 0; + + // Returns the next PortableFile entry in the index if available. + virtual std::optional GetPortableFileById(SQLite::Connection& connection, SQLite::rowid_t id) = 0; + + // Returns a bool value indicating whether the PortableFile already exists in the index. + virtual bool Exists(SQLite::Connection& connection, const PortableFile& file) = 0; }; } \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface.h index b87f07d5a6..e89de61716 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface.h @@ -16,5 +16,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 SQLite::rowid_t AddPortableFile(SQLite::Connection& connection, const PortableFile& file) override; SQLite::rowid_t RemovePortableFile(SQLite::Connection& connection, const PortableFile& file) override; std::pair UpdatePortableFile(SQLite::Connection& connection, const PortableFile& file) override; + std::optional GetPortableFileById(SQLite::Connection& connection, SQLite::rowid_t id) override; + bool Exists(SQLite::Connection& connection, const PortableFile& file) override; }; } \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface_1_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface_1_0.cpp index d7e49588ac..5d9a3a474c 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface_1_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface_1_0.cpp @@ -73,4 +73,14 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 savepoint.Commit(); return { status, portableEntryResult.value() }; } + + std::optional PortableIndexInterface::GetPortableFileById(SQLite::Connection& connection, SQLite::rowid_t id) + { + return PortableTable::GetPortableFileById(connection, id); + } + + bool PortableIndexInterface::Exists(SQLite::Connection& connection, const PortableFile& file) + { + return GetExistingPortableFileId(connection, file).has_value(); + } } \ No newline at end of file From 7812f05af14c377f9ed77da2abe8f0370baa8313 Mon Sep 17 00:00:00 2001 From: Ryan Fu Date: Fri, 2 Sep 2022 00:56:19 -0700 Subject: [PATCH 04/22] add support for uninstall from index --- src/AppInstallerCLICore/PortableInstaller.cpp | 466 ++++++++---------- src/AppInstallerCLICore/PortableInstaller.h | 68 +-- src/AppInstallerCLICore/Resources.h | 2 +- .../Workflows/ArchiveFlow.cpp | 13 +- .../Workflows/InstallFlow.cpp | 1 + .../Workflows/PortableFlow.cpp | 116 +++-- .../Workflows/PortableFlow.h | 2 + .../Shared/Strings/en-us/winget.resw | 10 +- src/AppInstallerCLITests/PortableEntry.cpp | 3 - src/AppInstallerCommonCore/Errors.cpp | 2 - src/AppInstallerCommonCore/Filesystem.cpp | 14 + .../PortableARPEntry.cpp | 21 - .../Public/winget/Filesystem.h | 6 + .../Public/winget/PortableARPEntry.h | 2 - .../Microsoft/PortableIndex.cpp | 29 +- .../Microsoft/PortableIndex.h | 6 +- .../Microsoft/Schema/IPortableIndex.h | 9 +- .../Portable_1_0/PortableIndexInterface.h | 3 +- .../PortableIndexInterface_1_0.cpp | 13 +- .../Schema/Portable_1_0/PortableTable.cpp | 25 + .../Schema/Portable_1_0/PortableTable.h | 3 + 21 files changed, 411 insertions(+), 403 deletions(-) diff --git a/src/AppInstallerCLICore/PortableInstaller.cpp b/src/AppInstallerCLICore/PortableInstaller.cpp index 02cb63a8e9..a38aeb80f9 100644 --- a/src/AppInstallerCLICore/PortableInstaller.cpp +++ b/src/AppInstallerCLICore/PortableInstaller.cpp @@ -18,6 +18,7 @@ using namespace AppInstaller::Registry; using namespace AppInstaller::Registry::Portable; using namespace AppInstaller::Registry::Environment; using namespace AppInstaller::Repository; +using namespace AppInstaller::Repository::SQLite; using namespace AppInstaller::Repository::Microsoft; using namespace AppInstaller::Repository::Microsoft::Schema; @@ -25,8 +26,7 @@ namespace AppInstaller::CLI::Portable { namespace { - constexpr std::string_view c_PortableIndexFileName = "portable.db"; - constexpr std::string_view s_DefaultSource = "*DefaultSource"sv; + constexpr std::wstring_view c_PortableIndexFileName = L"portable.db"; IPortableIndex::PortableFile CreatePortableFileFromPath(const std::filesystem::path& path) { @@ -54,6 +54,49 @@ namespace AppInstaller::CLI::Portable return portableFile; } + + bool VerifyPortableExeHash(const std::filesystem::path& targetPath, const std::string& hashValue) + { + std::ifstream inStream{ targetPath, std::ifstream::binary }; + const Utility::SHA256::HashBuffer& targetFileHash = Utility::SHA256::ComputeHash(inStream); + inStream.close(); + + return Utility::SHA256::AreEqual(Utility::SHA256::ConvertToBytes(hashValue), targetFileHash); + } + + void RemoveItemsFromIndex(const std::filesystem::path& indexPath, bool purge) + { + bool deleteIndex = false; + { + PortableIndex portableIndex = PortableIndex::Open(indexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); + std::vector portableFiles = portableIndex.GetAllPortableFiles(); + + for (const auto& file : portableFiles) + { + IPortableIndex::PortableFileType fileType = file.FileType; + const auto& filePath = file.GetFilePath(); + + if (purge || (fileType == IPortableIndex::PortableFileType::File && VerifyPortableExeHash(filePath, file.SHA256)) || + (fileType == IPortableIndex::PortableFileType::Symlink && Filesystem::VerifySymlink(filePath, file.SymlinkTarget))) + { + std::filesystem::remove_all(filePath); + portableIndex.RemovePortableFile(file); + } + } + + if (portableIndex.IsEmpty()) + { + deleteIndex = true; + + }; + } + + if (deleteIndex) + { + AICLI_LOG(CLI, Info, << "Removing portable index: " << indexPath); + std::filesystem::remove(indexPath); + } + } } std::filesystem::path GetPortableLinksLocation(Manifest::ScopeEnum scope) @@ -87,87 +130,46 @@ namespace AppInstaller::CLI::Portable } } - HRESULT PortableInstaller::MultipleInstall(const std::vector& nestedInstallerFiles, const std::vector& extractedItems) + bool PortableInstaller::VerifyPortableFilesForUninstall() { - const auto& indexPath = InstallLocation / Utility::ConvertToUTF16(c_PortableIndexFileName); - - if (!std::filesystem::exists(indexPath)) - { - PortableIndex::CreateNew(indexPath.u8string(), Schema::Version::Latest()); - } - - PortableIndex portableIndex = PortableIndex::Open(indexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); - - for (auto& item : extractedItems) + namespace fs = std::filesystem; + const auto& indexPath = GetPortableIndexPath(); + if (fs:: exists(indexPath)) { - const auto& itemPath = InstallLocation / item; - // This method should be able to create any portable file based on the extension. - IPortableIndex::PortableFile portableFile = CreatePortableFileFromPath(itemPath); - - if (portableIndex.Exists(portableFile)) - { - portableIndex.UpdatePortableFile(portableFile); - } - else - { - portableIndex.AddPortableFile(portableFile); - } - } - - for (const auto& nestedInstallerFile : nestedInstallerFiles) - { - const std::filesystem::path& portableTargetPath = InstallLocation / ConvertToUTF16(nestedInstallerFile.RelativeFilePath); - - if (!InstallDirectoryAddedToPath) + PortableIndex portableIndex = PortableIndex::Open(indexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); + std::vector portableFiles = portableIndex.GetAllPortableFiles(); + for (const auto& file : portableFiles) { - // append .exe if needed for portable command alias if it exists, otherwise just use portalbe command alias. - const std::filesystem::path& symlinkFullPath = GetPortableLinksLocation(GetScope()) / ConvertToUTF16(nestedInstallerFile.PortableCommandAlias); - CreatePortableSymlink(portableTargetPath, symlinkFullPath); + IPortableIndex::PortableFileType fileType = file.FileType; + const auto& filePath = file.GetFilePath(); - // add portable file only if created symlink is true. - IPortableIndex::PortableFile symlinkPortableFile = CreatePortableFileFromPath(symlinkFullPath); - if (portableIndex.Exists(symlinkPortableFile)) - { - portableIndex.UpdatePortableFile(symlinkPortableFile); - } - else + if (fileType == IPortableIndex::PortableFileType::File && !VerifyPortableExeHash(filePath, file.SHA256) || + fileType == IPortableIndex::PortableFileType::Symlink && fs::is_symlink(fs::symlink_status(filePath)) && !Filesystem::VerifySymlink(filePath, file.SymlinkTarget)) { - portableIndex.AddPortableFile(symlinkPortableFile); + return false; } } - else - { - AICLI_LOG(CLI, Info, << "Package directory was previously added to PATH. Skipping symlink creation."); - } - } - AddToPathVariable(); - return ERROR_SUCCESS; + return true; + } + else + { + // symlink_status() will return false if symlink does not exist and does not follow symlink to target. + return VerifyPortableExeHash(PortableTargetFullPath, SHA256) && + fs::is_symlink(fs::symlink_status(PortableSymlinkFullPath)) && Filesystem::VerifySymlink(PortableSymlinkFullPath, PortableTargetFullPath); + } } - // providing a path and the alias - // providing a list of extracted files and the NestedInstallerFiles, - HRESULT PortableInstaller::SingleInstall(const std::filesystem::path& installerPath) { - // Initial registration. - Commit(PortableValueName::WinGetPackageIdentifier, WinGetPackageIdentifier); - Commit(PortableValueName::WinGetSourceIdentifier, WinGetSourceIdentifier); - Commit(PortableValueName::UninstallString, "winget uninstall --product-code " + GetProductCode()); - Commit(PortableValueName::WinGetInstallerType, InstallerTypeToString(Manifest::InstallerTypeEnum::Portable)); - Commit(PortableValueName::SHA256, SHA256); - + InitializeRegistryEntry(); - Commit(PortableValueName::PortableTargetFullPath, PortableTargetFullPath); - Commit(PortableValueName::InstallLocation, InstallLocation); MovePortableExe(installerPath); - // Create symlink if (!InstallDirectoryAddedToPath) { const std::filesystem::path& symlinkPath = PortableSymlinkFullPath; Commit(PortableValueName::PortableSymlinkFullPath, symlinkPath); - CreatePortableSymlink(PortableTargetFullPath, PortableSymlinkFullPath); } else @@ -176,122 +178,108 @@ namespace AppInstaller::CLI::Portable } AddToPathVariable(); + FinalizeRegistryEntry(); return ERROR_SUCCESS; } - HRESULT PortableInstaller::Uninstall(bool purge) - { - // remove portable - RemovePortableExe(PortableTargetFullPath, SHA256); - - // remove install directory - RemovePortableDirectory(InstallLocation, purge, InstallDirectoryCreated); - - // remove portable symlink - RemovePortableSymlink(PortableTargetFullPath, PortableSymlinkFullPath); - - // remove path - const std::filesystem::path& pathValue = GetPathDirectory(); - if (RemoveFromPathVariable()) - { - AICLI_LOG(CLI, Info, << "Removed target directory from PATH registry: " << pathValue); - } - else - { - AICLI_LOG(CLI, Info, << "Target directory not removed from PATH registry: " << pathValue); - } - - m_portableARPEntry.Delete(); - AICLI_LOG(CLI, Info, << "PortableARPEntry deleted."); - return ERROR_SUCCESS; - } - - HRESULT PortableInstaller::UninstallFromIndex(bool purge) + HRESULT PortableInstaller::MultipleInstall(const std::vector& nestedInstallerFiles, const std::vector& extractedItems) { - if (purge) - { - std::filesystem::remove(InstallLocation); - return ERROR_SUCCESS; - } + InitializeRegistryEntry(); - const auto& indexPath = InstallLocation / Utility::ConvertToUTF16(c_PortableIndexFileName); + const auto& indexPath = InstallLocation / c_PortableIndexFileName; if (!std::filesystem::exists(indexPath)) { - // portable index not found; - return ERROR_NOT_SUPPORTED; + PortableIndex::CreateNew(indexPath.u8string(), Schema::Version::Latest()); } PortableIndex portableIndex = PortableIndex::Open(indexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); - - IPortableIndex::PortableFile file; - int i = 0; - while (portableIndex.GetPortableFileById(i).has_value()) + for (auto& item : extractedItems) { - file = portableIndex.GetPortableFileById(i).value(); + const auto& itemPath = InstallLocation / item; + IPortableIndex::PortableFile portableFile = CreatePortableFileFromPath(itemPath); + portableIndex.AddOrUpdatePortableFile(portableFile); + } - if (file.FileType == IPortableIndex::PortableFileType::File) + for (const auto& nestedInstallerFile : nestedInstallerFiles) + { + const std::filesystem::path& portableTargetPath = InstallLocation / ConvertToUTF16(nestedInstallerFile.RelativeFilePath); + if (!InstallDirectoryAddedToPath) { - if (VerifyPortableExeHash(file.GetFilePath(), file.SHA256)) + std::filesystem::path commandAlias; + if (!nestedInstallerFile.PortableCommandAlias.empty()) { - std::filesystem::remove(file.GetFilePath()); - portableIndex.RemovePortableFile(file); + commandAlias = ConvertToUTF16(nestedInstallerFile.PortableCommandAlias); } else { - i++; - } - } - else if (file.FileType == IPortableIndex::PortableFileType::Directory) - { - std::filesystem::remove(file.GetFilePath()); - portableIndex.RemovePortableFile(file); - } - else if (file.FileType == IPortableIndex::PortableFileType::Symlink) - { - if (VerifySymlinkTarget(file.GetFilePath(), file.SymlinkTarget)) - { - std::filesystem::remove(file.GetFilePath()); - portableIndex.RemovePortableFile(file); + commandAlias = portableTargetPath.filename(); } - else + + Filesystem::AppendExtension(commandAlias, ".exe"); + const std::filesystem::path& symlinkFullPath = GetPortableLinksLocation(GetScope()) / commandAlias; + if (CreatePortableSymlink(portableTargetPath, symlinkFullPath)) { - i++; + IPortableIndex::PortableFile symlinkPortableFile = CreatePortableFileFromPath(symlinkFullPath); + portableIndex.AddOrUpdatePortableFile(symlinkPortableFile); } } + else + { + AICLI_LOG(CLI, Info, << "Package directory was previously added to PATH. Skipping symlink creation."); + } } - // remove install directory - RemovePortableDirectory(InstallLocation, purge, InstallDirectoryCreated); + AddToPathVariable(); + FinalizeRegistryEntry(); + return ERROR_SUCCESS; + } - // remove path - const std::filesystem::path& pathValue = GetPathDirectory(); - if (RemoveFromPathVariable()) + HRESULT PortableInstaller::UninstallSingle(bool purge) + { + if (std::filesystem::exists(PortableTargetFullPath)) { - AICLI_LOG(CLI, Info, << "Removed target directory from PATH registry: " << pathValue); + std::filesystem::remove(PortableTargetFullPath); + AICLI_LOG(CLI, Info, << "Successfully deleted portable exe:" << PortableTargetFullPath); } else { - AICLI_LOG(CLI, Info, << "Target directory not removed from PATH registry: " << pathValue); + AICLI_LOG(CLI, Info, << "Portable exe not found; Unable to delete portable exe: " << PortableTargetFullPath); + } + + RemoveInstallDirectory(purge); + + if (!std::filesystem::remove(PortableSymlinkFullPath)) + { + AICLI_LOG(CLI, Info, << "Portable symlink not found; Unable to delete portable symlink: " << PortableSymlinkFullPath); } + RemoveFromPathVariable(); + m_portableARPEntry.Delete(); AICLI_LOG(CLI, Info, << "PortableARPEntry deleted."); return ERROR_SUCCESS; } - void PortableInstaller::SetAppsAndFeaturesMetadata(const Manifest::AppsAndFeaturesEntry& entry, const Manifest::Manifest& manifest) + HRESULT PortableInstaller::UninstallFromIndex(bool purge) { - Commit(PortableValueName::DisplayName, DisplayName = entry.DisplayName); - Commit(PortableValueName::DisplayVersion, DisplayVersion = entry.DisplayVersion); - Commit(PortableValueName::Publisher, Publisher = entry.Publisher); - Commit(PortableValueName::InstallDate, InstallDate = AppInstaller::Utility::GetCurrentDateForARP()); - Commit(PortableValueName::URLInfoAbout, URLInfoAbout = manifest.CurrentLocalization.Get()); - Commit(PortableValueName::HelpLink, HelpLink = manifest.CurrentLocalization.Get < Manifest::Localization::PublisherSupportUrl>()); + const auto& indexPath = GetPortableIndexPath(); + RemoveItemsFromIndex(indexPath, purge); + + RemoveInstallDirectory(purge); + + RemoveFromPathVariable(); + + m_portableARPEntry.Delete(); + AICLI_LOG(CLI, Info, << "PortableARPEntry deleted."); + return ERROR_SUCCESS; } void PortableInstaller::MovePortableExe(const std::filesystem::path& installerPath) { bool isDirectoryCreated = false; + + Commit(PortableValueName::InstallLocation, InstallLocation); + if (std::filesystem::create_directories(InstallLocation)) { AICLI_LOG(Core, Info, << "Created target install directory: " << InstallLocation); @@ -300,45 +288,18 @@ namespace AppInstaller::CLI::Portable if (std::filesystem::exists(PortableTargetFullPath)) { - std::filesystem::remove(PortableTargetFullPath); AICLI_LOG(Core, Info, << "Removing existing portable exe at: " << PortableTargetFullPath); + std::filesystem::remove(PortableTargetFullPath); } - Filesystem::RenameFile(installerPath, PortableTargetFullPath); AICLI_LOG(Core, Info, << "Portable exe moved to: " << PortableTargetFullPath); - // Only assign this value if this is a new portable install or the install directory was actually created. - // Otherwise, we want to preserve the existing value from the prior install. - if (!Exists() || isDirectoryCreated) - { - Commit(PortableValueName::InstallDirectoryCreated, InstallDirectoryCreated = isDirectoryCreated); - } - } - - bool PortableInstaller::VerifyPortableExeHash(const std::filesystem::path& targetPath, const std::string& hashValue) - { - std::ifstream inStream{ targetPath, std::ifstream::binary }; - const Utility::SHA256::HashBuffer& targetFileHash = Utility::SHA256::ComputeHash(inStream); - inStream.close(); - - return Utility::SHA256::AreEqual(Utility::SHA256::ConvertToBytes(hashValue), targetFileHash); - } + Commit(PortableValueName::PortableTargetFullPath, PortableTargetFullPath); - void PortableInstaller::AddToPathVariable() - { - const std::filesystem::path& pathValue = GetPathDirectory(); - if (PathVariable(GetScope()).Append(pathValue)) - { - AICLI_LOG(Core, Info, << "Appended target directory to PATH registry: " << pathValue); - m_stream << Resource::String::ModifiedPathRequiresShellRestart << std::endl; - } - else - { - AICLI_LOG(CLI, Info, << "Target directory already exists in PATH registry: " << pathValue); - } + Filesystem::RenameFile(installerPath, PortableTargetFullPath); } - void PortableInstaller::CreatePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath) + bool PortableInstaller::CreatePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath) { std::filesystem::file_status status = std::filesystem::status(symlinkPath); if (std::filesystem::is_directory(status)) @@ -356,6 +317,7 @@ namespace AppInstaller::CLI::Portable if (Filesystem::CreateSymlink(targetPath, symlinkPath)) { AICLI_LOG(Core, Info, << "Symlink created at: " << symlinkPath); + return true; } else { @@ -363,146 +325,103 @@ namespace AppInstaller::CLI::Portable // Resort to adding install directory to PATH directly. AICLI_LOG(Core, Info, << "Portable install executed in user mode. Adding package directory to PATH."); Commit(PortableValueName::InstallDirectoryAddedToPath, InstallDirectoryAddedToPath = true); + return false; } } - bool PortableInstaller::VerifySymlinkTarget(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath) + void PortableInstaller::RemoveInstallDirectory(bool purge) { - AICLI_LOG(Core, Info, << "Expected portable target path: " << targetPath); - const std::filesystem::path& symlinkTargetPath = std::filesystem::read_symlink(symlinkPath); - - if (symlinkTargetPath == targetPath) + if (std::filesystem::exists(InstallLocation)) { - AICLI_LOG(Core, Info, << "Portable symlink target matches portable target path: " << symlinkTargetPath); - return true; + if (purge) + { + m_stream << Resource::String::PurgeInstallDirectory << std::endl; + const auto& removedFilesCount = std::filesystem::remove_all(InstallLocation); + AICLI_LOG(CLI, Info, << "Purged install location directory. Deleted " << removedFilesCount << " files or directories"); + } + else if (std::filesystem::is_empty(InstallLocation)) + { + AICLI_LOG(CLI, Info, << "Removing empty install directory: " << InstallLocation); + std::filesystem::remove(InstallLocation); + } + else + { + AICLI_LOG(CLI, Info, << "Unable to remove install directory as there are remaining files in: " << InstallLocation); + m_stream << Resource::String::FilesRemainInInstallDirectory << InstallLocation << std::endl; + } } else { - AICLI_LOG(Core, Info, << "Portable symlink does not match portable target path: " << symlinkTargetPath); - return false; + AICLI_LOG(CLI, Info, << "Install directory does not exist: " << InstallLocation); } } - void PortableInstaller::RemovePortableExe(const std::filesystem::path& targetPath, const std::string& hash) + void PortableInstaller::AddToPathVariable() { - if (std::filesystem::exists(targetPath)) + const std::filesystem::path& pathValue = GetInstallDirectoryForPathVariable(); + if (PathVariable(GetScope()).Append(pathValue)) { - if (!VerifyPortableExeHash(targetPath, hash)) - { - //bool overrideHashMismatch = context.Args.Contains(Execution::Args::Type::HashOverride); - bool overrideHashMismatch = true; - if (overrideHashMismatch) - { - //context.Reporter.Warn() << Resource::String::PortableHashMismatchOverridden << std::endl; - } - else - { - //context.Reporter.Warn() << Resource::String::PortableHashMismatchOverrideRequired << std::endl; - throw APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED; - } - } - - std::filesystem::remove(targetPath); - AICLI_LOG(CLI, Info, << "Successfully deleted portable exe:" << targetPath); + AICLI_LOG(Core, Info, << "Appended target directory to PATH registry: " << pathValue); + m_stream << Resource::String::ModifiedPathRequiresShellRestart << std::endl; } else { - AICLI_LOG(CLI, Info, << "Portable exe not found; Unable to delete portable exe: " << targetPath); + AICLI_LOG(CLI, Info, << "Target directory already exists in PATH registry: " << pathValue); } } - void PortableInstaller::RemovePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath) + void PortableInstaller::RemoveFromPathVariable() { - if (!std::filesystem::is_symlink(std::filesystem::symlink_status(symlinkPath))) + std::filesystem::path pathValue = GetInstallDirectoryForPathVariable(); + if (!std::filesystem::is_empty(pathValue)) { - AICLI_LOG(Core, Info, << "The registry value for [PortableSymlinkFullPath] does not point to a valid symlink file."); + AICLI_LOG(Core, Info, << "Install directory is not empty: " << pathValue); } else { - if (VerifySymlinkTarget(targetPath, symlinkPath)) + if (PathVariable(GetScope()).Remove(pathValue)) { - if (!std::filesystem::remove(symlinkPath)) - { - AICLI_LOG(CLI, Info, << "Portable symlink not found; Unable to delete portable symlink: " << symlinkPath); - } + AICLI_LOG(CLI, Info, << "Removed target directory from PATH registry: " << pathValue); } else { - //context.Reporter.Warn() << Resource::String::SymlinkModified << std::endl; + AICLI_LOG(CLI, Info, << "Target directory not removed from PATH registry: " << pathValue); } } } - void PortableInstaller::RemovePortableDirectory(const std::filesystem::path& directoryPath, bool purge, bool isCreated) + void PortableInstaller::SetAppsAndFeaturesMetadata(const Manifest::Manifest& manifest, const std::vector& entries) { - if (std::filesystem::exists(directoryPath)) + AppInstaller::Manifest::AppsAndFeaturesEntry entry; + if (!entries.empty()) { - if (purge) - { - if (isCreated) - { - //context.Reporter.Warn() << Resource::String::PurgeInstallDirectory << std::endl; - const auto& removedFilesCount = std::filesystem::remove_all(directoryPath); - AICLI_LOG(CLI, Info, << "Purged install location directory. Deleted " << removedFilesCount << " files or directories"); - } - else - { - //context.Reporter.Warn() << Resource::String::UnableToPurgeInstallDirectory << std::endl; - } - - } - else if (std::filesystem::is_empty(directoryPath)) - { - if (isCreated) - { - std::filesystem::remove(directoryPath); - AICLI_LOG(CLI, Info, << "Install directory deleted: " << directoryPath); - } - } - else - { - //m_stream << Resource::String::FilesRemainInInstallDirectory << directoryPath << std::endl; - } + entry = entries[0]; } - else - { - AICLI_LOG(CLI, Info, << "Install directory does not exist: " << directoryPath); - } - } - bool PortableInstaller::RemoveFromPathVariable() - { - bool removeFromPath = true; - std::filesystem::path pathValue = GetPathDirectory(); - if (!InstallDirectoryAddedToPath) + if (entry.DisplayName.empty()) { - // Default links directory must be empty before removing from PATH. - if (!std::filesystem::is_empty(pathValue)) - { - AICLI_LOG(Core, Info, << "Install directory is not empty: " << pathValue); - removeFromPath = false; - } + entry.DisplayName = manifest.CurrentLocalization.Get(); } - - if (removeFromPath) + if (entry.DisplayVersion.empty()) { - return PathVariable(GetScope()).Remove(pathValue); + entry.DisplayVersion = manifest.Version; } - else + if (entry.Publisher.empty()) { - return false; + entry.Publisher = manifest.CurrentLocalization.Get(); } + + DisplayName = entry.DisplayName; + DisplayVersion = entry.DisplayVersion; + Publisher = entry.Publisher; + InstallDate = Utility::GetCurrentDateForARP(); + URLInfoAbout = manifest.CurrentLocalization.Get(); + HelpLink = manifest.CurrentLocalization.Get(); } PortableInstaller::PortableInstaller(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode) : m_portableARPEntry(PortableARPEntry(scope, arch, productCode)) { - Initialize(); - } - - void PortableInstaller::Initialize() - { - // Initialize all values if present if (Exists()) { DisplayName = GetStringValue(PortableValueName::DisplayName); @@ -521,12 +440,29 @@ namespace AppInstaller::CLI::Portable PortableSymlinkFullPath = GetPathValue(PortableValueName::PortableSymlinkFullPath); PortableTargetFullPath = GetPathValue(PortableValueName::PortableTargetFullPath); InstallLocation = GetPathValue(PortableValueName::InstallLocation); - - InstallDirectoryCreated = GetBoolValue(PortableValueName::InstallDirectoryCreated); InstallDirectoryAddedToPath = GetBoolValue(PortableValueName::InstallDirectoryAddedToPath); } } + void PortableInstaller::InitializeRegistryEntry() + { + Commit(PortableValueName::WinGetPackageIdentifier, WinGetPackageIdentifier); + Commit(PortableValueName::WinGetSourceIdentifier, WinGetSourceIdentifier); + Commit(PortableValueName::UninstallString, "winget uninstall --product-code " + GetProductCode()); + Commit(PortableValueName::WinGetInstallerType, InstallerTypeToString(Manifest::InstallerTypeEnum::Portable)); + Commit(PortableValueName::SHA256, SHA256); + } + + void PortableInstaller::FinalizeRegistryEntry() + { + Commit(PortableValueName::DisplayName, DisplayName); + Commit(PortableValueName::DisplayVersion, DisplayVersion); + Commit(PortableValueName::Publisher, Publisher); + Commit(PortableValueName::InstallDate, InstallDate); + Commit(PortableValueName::URLInfoAbout, URLInfoAbout); + Commit(PortableValueName::HelpLink, HelpLink); + } + std::filesystem::path PortableInstaller::GetPortableIndexPath() { return InstallLocation / c_PortableIndexFileName; diff --git a/src/AppInstallerCLICore/PortableInstaller.h b/src/AppInstallerCLICore/PortableInstaller.h index fbec77ac2b..bd94f38289 100644 --- a/src/AppInstallerCLICore/PortableInstaller.h +++ b/src/AppInstallerCLICore/PortableInstaller.h @@ -19,11 +19,11 @@ namespace AppInstaller::CLI::Portable // Object representation of the arguments needed to install a portable. struct PortableInstaller { + // Display Name std::string DisplayName; std::string DisplayVersion; std::string HelpLink; std::string InstallDate; - bool InstallDirectoryCreated = false; std::filesystem::path InstallLocation; std::filesystem::path PortableSymlinkFullPath; std::filesystem::path PortableTargetFullPath; @@ -34,6 +34,8 @@ namespace AppInstaller::CLI::Portable std::string WinGetInstallerType; std::string WinGetPackageIdentifier; std::string WinGetSourceIdentifier; + + // If we fail to create a symlink, add install directory bool InstallDirectoryAddedToPath = false; bool IsUpdate = false; @@ -43,6 +45,30 @@ namespace AppInstaller::CLI::Portable m_portableARPEntry.SetValue(valueName, value); } + std::filesystem::path GetInstallDirectoryForPathVariable() + { + return InstallDirectoryAddedToPath ? InstallLocation : GetPortableLinksLocation(GetScope()); + } + + HRESULT Uninstall(bool purge = false) + { + if (std::filesystem::exists(GetPortableIndexPath())) + { + return UninstallFromIndex(purge); + } + else + { + return UninstallSingle(purge); + } + } + + HRESULT SingleInstall(const std::filesystem::path& installerPath); + HRESULT MultipleInstall( + const std::vector& nestedInstallerFiles, + const std::vector& extractedItems); + + bool VerifyPortableFilesForUninstall(); + Manifest::ScopeEnum GetScope() { return m_portableARPEntry.GetScope(); }; Utility::Architecture GetArch() { return m_portableARPEntry.GetArchitecture(); }; @@ -57,33 +83,11 @@ namespace AppInstaller::CLI::Portable PortableInstaller(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode); - HRESULT SingleInstall(const std::filesystem::path& installerPath); - HRESULT MultipleInstall(const std::vector& nestedInstallerFiles, const std::vector& extractedItems); - HRESULT Uninstall(bool purge = false); - HRESULT UninstallFromIndex(bool purge = false); - - std::filesystem::path GetPathDirectory() const - { - return InstallDirectoryAddedToPath ? InstallLocation : PortableSymlinkFullPath.parent_path(); - } - void SetAppsAndFeaturesMetadata(const Manifest::AppsAndFeaturesEntry& entry, const Manifest::Manifest& manifest); void MovePortableExe(const std::filesystem::path& installerPath); - void CreatePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath); - - bool VerifyPackageAndSourceIdentifiers() - { - if (Exists()) - { - return m_portableARPEntry.IsSamePortablePackageEntry(m_packageIdentifier, m_sourceIdentifier); - } - else - { - return true; - } - }; + bool CreatePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath); std::string GetOutputMessage() { @@ -92,27 +96,29 @@ namespace AppInstaller::CLI::Portable std::filesystem::path GetPortableIndexPath(); + void SetAppsAndFeaturesMetadata(const Manifest::Manifest& manifest, const std::vector& entries); + private: PortableARPEntry m_portableARPEntry; std::string m_packageIdentifier; std::string m_sourceIdentifier; std::stringstream m_stream; - void Initialize(); - std::string GetStringValue(PortableValueName valueName); std::filesystem::path GetPathValue(PortableValueName valueName); - bool GetBoolValue(PortableValueName valueName); - bool VerifySymlinkTarget(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath); - bool VerifyPortableExeHash(const std::filesystem::path& targetPath, const std::string& hashValue); + void InitializeRegistryEntry(); + void FinalizeRegistryEntry(); + + HRESULT UninstallSingle(bool purge = false); + HRESULT UninstallFromIndex(bool purge = false); void RemovePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath); - void RemovePortableDirectory(const std::filesystem::path& directoryPath, bool purge, bool isCreated); + void RemoveInstallDirectory(bool purge); void RemovePortableExe(const std::filesystem::path& targetPath, const std::string& hash); void AddToPathVariable(); - bool RemoveFromPathVariable(); + void RemoveFromPathVariable(); }; } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index 63d01ae9af..f9a342f553 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -230,6 +230,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(PortableHashMismatchOverridden); WINGET_DEFINE_RESOURCE_STRINGID(PortableHashMismatchOverrideRequired); WINGET_DEFINE_RESOURCE_STRINGID(PortableInstallFailed); + WINGET_DEFINE_RESOURCE_STRINGID(PortablePackageAlreadyExists); WINGET_DEFINE_RESOURCE_STRINGID(PortableRegistryCollisionOverridden); WINGET_DEFINE_RESOURCE_STRINGID(PositionArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(PreserveArgumentDescription); @@ -361,7 +362,6 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SourceUpdateCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceUpdateOne); WINGET_DEFINE_RESOURCE_STRINGID(SystemArchitecture); - WINGET_DEFINE_RESOURCE_STRINGID(SymlinkModified); WINGET_DEFINE_RESOURCE_STRINGID(TagArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ThankYou); WINGET_DEFINE_RESOURCE_STRINGID(ThirdPartSoftwareNotices); diff --git a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp index 948a7f642b..f9f19e6a66 100644 --- a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp @@ -16,17 +16,14 @@ namespace AppInstaller::CLI::Workflow const auto& installerPath = context.Get(); std::filesystem::path destinationFolder; std::vector extractedItems; + bool isDirectoryCreated = false; HRESULT hr; if (context.Get()->NestedInstallerType == InstallerTypeEnum::Portable) { destinationFolder = GetPortableTargetDirectory(context); - - // temporarily creating directory now - std::filesystem::create_directory(destinationFolder); - + isDirectoryCreated = std::filesystem::create_directory(destinationFolder); hr = AppInstaller::Archive::TryExtractArchive(installerPath, destinationFolder, extractedItems); - context.Add(extractedItems); } else { @@ -39,9 +36,15 @@ namespace AppInstaller::CLI::Workflow if (SUCCEEDED(hr)) { AICLI_LOG(CLI, Info, << "Successfully extracted archive"); + context.Add(extractedItems); } else { + if (isDirectoryCreated) + { + std::filesystem::remove(destinationFolder); + } + 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); diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp index 1e852ccebe..e2b2772400 100644 --- a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -335,6 +335,7 @@ namespace AppInstaller::CLI::Workflow { context << GetPortableEntryForInstall << + VerifyPackageAndSourceMatch << PortableInstallImpl << ReportInstallerResult("Portable"sv, APPINSTALLER_CLI_ERROR_PORTABLE_INSTALL_FAILED, true); } diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp index 1110b8b4c9..5b33580378 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -20,17 +20,8 @@ namespace AppInstaller::CLI::Workflow { namespace { - constexpr std::string_view c_PortableIndexFileName = "portable.db"; constexpr std::string_view s_DefaultSource = "*DefaultSource"sv; - void AppendExeExtension(std::filesystem::path& value) - { - if (value.extension() != ".exe") - { - value += ".exe"; - } - } - std::string GetPortableProductCode(Execution::Context& context) { const std::string& packageId = context.Get().Id; @@ -64,7 +55,7 @@ namespace AppInstaller::CLI::Workflow fileName = installerPath.filename(); } - AppendExeExtension(fileName); + AppInstaller::Filesystem::AppendExtension(fileName, ".exe"); return targetInstallDirectory / fileName; } @@ -92,36 +83,10 @@ namespace AppInstaller::CLI::Workflow } } - AppendExeExtension(commandAlias); + AppInstaller::Filesystem::AppendExtension(commandAlias, ".exe"); return GetPortableLinksLocation(scope) / commandAlias; } - Manifest::AppsAndFeaturesEntry GetAppsAndFeaturesEntryForPortableInstall( - const std::vector& appsAndFeaturesEntries, - const AppInstaller::Manifest::Manifest& manifest) - { - AppInstaller::Manifest::AppsAndFeaturesEntry appsAndFeaturesEntry; - if (!appsAndFeaturesEntries.empty()) - { - appsAndFeaturesEntry = appsAndFeaturesEntries[0]; - } - - if (appsAndFeaturesEntry.DisplayName.empty()) - { - appsAndFeaturesEntry.DisplayName = manifest.CurrentLocalization.Get(); - } - if (appsAndFeaturesEntry.DisplayVersion.empty()) - { - appsAndFeaturesEntry.DisplayVersion = manifest.Version; - } - if (appsAndFeaturesEntry.Publisher.empty()) - { - appsAndFeaturesEntry.Publisher = manifest.CurrentLocalization.Get(); - } - - return appsAndFeaturesEntry; - } - void EnsureValidArgsForPortableInstall(Execution::Context& context) { std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); @@ -177,14 +142,54 @@ namespace AppInstaller::CLI::Workflow } else { - const std::string& productCode = GetPortableProductCode(context); + targetInstallDirectory = GetPortableInstallRoot(scope, arch); - targetInstallDirectory /= ConvertToUTF16(productCode); } + const std::string& productCode = GetPortableProductCode(context); + targetInstallDirectory /= ConvertToUTF16(productCode); + return targetInstallDirectory; } + void VerifyPackageAndSourceMatch(Execution::Context& context) + { + const std::string& packageIdentifier = context.Get().Id; + + std::string sourceIdentifier; + if (context.Contains(Execution::Data::PackageVersion)) + { + sourceIdentifier = context.Get()->GetSource().GetIdentifier(); + } + else + { + sourceIdentifier = s_DefaultSource; + } + + PortableInstaller& portableInstaller = context.Get(); + if (portableInstaller.Exists()) + { + if (packageIdentifier != portableInstaller.WinGetPackageIdentifier || sourceIdentifier != portableInstaller.WinGetSourceIdentifier) + { + // TODO: Replace HashOverride with --Force when argument behavior gets updated. + if (!context.Args.Contains(Execution::Args::Type::HashOverride)) + { + AICLI_LOG(CLI, Error, << "Registry match failed, skipping write to uninstall registry"); + context.Reporter.Error() << Resource::String::PortablePackageAlreadyExists << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS); + } + else + { + AICLI_LOG(CLI, Info, << "Overriding registry match check..."); + context.Reporter.Warn() << Resource::String::PortableRegistryCollisionOverridden << std::endl; + } + } + } + + portableInstaller.WinGetPackageIdentifier = packageIdentifier; + portableInstaller.WinGetSourceIdentifier = sourceIdentifier; + } + void GetPortableEntryForInstall(Execution::Context& context) { Manifest::ScopeEnum scope = Manifest::ScopeEnum::Unknown; @@ -216,6 +221,7 @@ namespace AppInstaller::CLI::Workflow portableInstaller.PortableTargetFullPath = GetPortableTargetFullPath(context); portableInstaller.PortableSymlinkFullPath = GetPortableSymlinkFullPath(context); + portableInstaller.SetAppsAndFeaturesMetadata(context.Get(), context.Get()->AppsAndFeaturesEntries); context.Add(std::move(portableInstaller)); } @@ -259,32 +265,38 @@ namespace AppInstaller::CLI::Workflow void PortableUninstallImpl(Execution::Context& context) { - HRESULT result; PortableInstaller& portableInstaller = context.Get(); try { context.Reporter.Info() << Resource::String::UninstallFlowStartingPackageUninstall << std::endl; + if (!portableInstaller.VerifyPortableFilesForUninstall()) + { + // TODO: replace with appropriate --force argument when available. + if (context.Args.Contains(Execution::Args::Type::HashOverride)) + { + context.Reporter.Warn() << Resource::String::PortableHashMismatchOverridden << std::endl; + } + else + { + context.Reporter.Warn() << Resource::String::PortableHashMismatchOverrideRequired << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED); + } + } + bool purge = context.Args.Contains(Execution::Args::Type::Purge) || (!portableInstaller.IsUpdate && Settings::User().Get() && !context.Args.Contains(Execution::Args::Type::Preserve)); - const auto& indexPath = portableInstaller.GetPortableIndexPath(); - if (std::filesystem::exists(indexPath)) - { - result = portableInstaller.UninstallFromIndex(purge); - } - else - { - result = portableInstaller.Uninstall(purge); - } + HRESULT result = portableInstaller.Uninstall(purge); + context.Add(result); + } - catch (HRESULT error) + catch (...) { - result = error; + context.Add(Workflow::HandleException(context, std::current_exception())); } - context.Add(result); context.Reporter.Warn() << portableInstaller.GetOutputMessage(); } diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.h b/src/AppInstallerCLICore/Workflows/PortableFlow.h index cdaf5b0d4f..942e7b4c1e 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.h +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.h @@ -23,6 +23,8 @@ namespace AppInstaller::CLI::Workflow void GetPortableEntryForInstall(Execution::Context& context); + void VerifyPackageAndSourceMatch(Execution::Context& context); + // Returns the target install directory for the portable package. std::filesystem::path GetPortableTargetDirectory(Execution::Context& context); } \ No newline at end of file diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 33d661b0de..53e8dc5203 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -1341,11 +1341,11 @@ Please specify one of them using the `--source` option to proceed. Both `purge` and `preserve` arguments are provided - Portable exe has been modified; proceeding due to --force + Portable package has been modified; proceeding due to --force {Locked="--force"} - Unable to remove Portable exe as it has been modified; to override this check use --force + Unable to remove Portable package as it has been modified; to override this check use --force {Locked="--force"} @@ -1369,9 +1369,6 @@ Please specify one of them using the `--source` option to proceed. Installation Notes: - - Portable symlink not deleted as it was modified and points to a different target exe - A provided argument is not supported for this package @@ -1429,4 +1426,7 @@ Please specify one of them using the `--source` option to proceed. Disable interactive prompts Description for a command line argument, shown next to it in the help + + Portable package from a different source already exists + \ No newline at end of file diff --git a/src/AppInstallerCLITests/PortableEntry.cpp b/src/AppInstallerCLITests/PortableEntry.cpp index bc03b431f8..7fe492f7d7 100644 --- a/src/AppInstallerCLITests/PortableEntry.cpp +++ b/src/AppInstallerCLITests/PortableEntry.cpp @@ -28,14 +28,12 @@ TEST_CASE("VerifyPortableMove", "[PortableEntry]") testEntry.MovePortableExe(testFile.GetPath()); REQUIRE(std::filesystem::exists(testEntry.PortableTargetFullPath)); - REQUIRE(testEntry.InstallDirectoryCreated); // Create a second PortableEntry instance to emulate installing for a second time. (ARP entry should already exist) PortableInstaller testEntry2 = PortableInstaller( AppInstaller::Manifest::ScopeEnum::User, Architecture::X64, "testProductCode"); - REQUIRE(testEntry2.InstallDirectoryCreated); // InstallDirectoryCreated should already be initialized as true. testEntry2.InstallLocation = tempDirectory.GetPath(); testEntry2.PortableTargetFullPath = tempDirectory.GetPath() / "output2.txt"; @@ -47,7 +45,6 @@ TEST_CASE("VerifyPortableMove", "[PortableEntry]") testEntry2.MovePortableExe(testFile2.GetPath()); REQUIRE(std::filesystem::exists(testEntry2.PortableTargetFullPath)); // InstallDirectoryCreated value should be preserved even though the directory was not created; - REQUIRE(testEntry2.InstallDirectoryCreated); //testEntry2.RemoveARPEntry(); } diff --git a/src/AppInstallerCommonCore/Errors.cpp b/src/AppInstallerCommonCore/Errors.cpp index 346d5936a4..008ff9966d 100644 --- a/src/AppInstallerCommonCore/Errors.cpp +++ b/src/AppInstallerCommonCore/Errors.cpp @@ -180,8 +180,6 @@ namespace AppInstaller return "Failed to install portable package"; case APPINSTALLER_CLI_ERROR_PORTABLE_REPARSE_POINT_NOT_SUPPORTED: return "Volume does not support reparse points."; - case APPINSTALLER_CLI_ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS: - return "Portable package from a different source already exists."; case APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY: return "Unable to create symlink, path points to a directory."; case APPINSTALLER_CLI_ERROR_INSTALLER_PROHIBITS_ELEVATION: diff --git a/src/AppInstallerCommonCore/Filesystem.cpp b/src/AppInstallerCommonCore/Filesystem.cpp index e18c364b68..3bd97575aa 100644 --- a/src/AppInstallerCommonCore/Filesystem.cpp +++ b/src/AppInstallerCommonCore/Filesystem.cpp @@ -167,4 +167,18 @@ namespace AppInstaller::Filesystem } } } + + bool VerifySymlink(const std::filesystem::path& symlink, const std::filesystem::path& target) + { + const std::filesystem::path& symlinkTargetPath = std::filesystem::read_symlink(symlink); + return symlinkTargetPath == target; + } + + void AppendExtension(std::filesystem::path& target, const std::string& value) + { + if (target.extension() != value) + { + target += value; + } + } } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/PortableARPEntry.cpp b/src/AppInstallerCommonCore/PortableARPEntry.cpp index 1889481f4c..084341067a 100644 --- a/src/AppInstallerCommonCore/PortableARPEntry.cpp +++ b/src/AppInstallerCommonCore/PortableARPEntry.cpp @@ -97,27 +97,6 @@ namespace AppInstaller::Registry::Portable } } - bool PortableARPEntry::IsSamePortablePackageEntry(const std::string& packageId, const std::string& sourceId) - { - auto existingWinGetPackageId = m_key[std::wstring{ s_WinGetPackageIdentifier }]; - auto existingWinGetSourceId = m_key[std::wstring{ s_WinGetSourceIdentifier }]; - - bool isSamePackageId = false; - bool isSamePackageSource = false; - - if (existingWinGetPackageId.has_value()) - { - isSamePackageId = existingWinGetPackageId.value().GetValue() == packageId; - } - - if (existingWinGetSourceId.has_value()) - { - isSamePackageSource = existingWinGetSourceId.value().GetValue() == sourceId; - } - - return isSamePackageId && isSamePackageSource; - } - std::optional PortableARPEntry::operator[](PortableValueName valueName) const { return m_key[std::wstring{ ToString(valueName) }]; diff --git a/src/AppInstallerCommonCore/Public/winget/Filesystem.h b/src/AppInstallerCommonCore/Public/winget/Filesystem.h index 7313374abe..af82999521 100644 --- a/src/AppInstallerCommonCore/Public/winget/Filesystem.h +++ b/src/AppInstallerCommonCore/Public/winget/Filesystem.h @@ -22,4 +22,10 @@ namespace AppInstaller::Filesystem // Creates a symlink that points to the target path. bool CreateSymlink(const std::filesystem::path& target, const std::filesystem::path& link); + + // Verifies that a symlink points to the target path. + bool VerifySymlink(const std::filesystem::path& symlink, const std::filesystem::path& target); + + // Appends the .exe extension to the path if not present. + void AppendExtension(std::filesystem::path& value, const std::string& extension); } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/PortableARPEntry.h b/src/AppInstallerCommonCore/Public/winget/PortableARPEntry.h index 8caf202383..f4a22f191f 100644 --- a/src/AppInstallerCommonCore/Public/winget/PortableARPEntry.h +++ b/src/AppInstallerCommonCore/Public/winget/PortableARPEntry.h @@ -34,8 +34,6 @@ namespace AppInstaller::Registry::Portable std::optional operator[](PortableValueName valueName) const; - bool IsSamePortablePackageEntry(const std::string& packageId, const std::string& sourceId); - bool Exists() { return m_exists; } void SetValue(PortableValueName valueName, const std::wstring& value); diff --git a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp index 451cf3f2f6..af3691dc37 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp @@ -74,19 +74,34 @@ namespace AppInstaller::Repository::Microsoft return result; } - std::optional PortableIndex::GetPortableFileById(SQLite::rowid_t rowId) - { - // change to using rowid number. - AICLI_LOG(Repo, Verbose, << "Retrieving portable file at row [" << rowId << "]"); - return m_interface->GetPortableFileById(m_dbconn, rowId); - } - bool PortableIndex::Exists(const Schema::IPortableIndex::PortableFile& file) { AICLI_LOG(Repo, Verbose, << "Checking if portable file exists [" << file.GetFilePath() << "]"); return m_interface->Exists(m_dbconn, file); } + bool PortableIndex::IsEmpty() + { + return m_interface->IsEmpty(m_dbconn); + } + + void PortableIndex::AddOrUpdatePortableFile(const Schema::IPortableIndex::PortableFile& file) + { + if (Exists(file)) + { + UpdatePortableFile(file); + } + else + { + AddPortableFile(file); + } + } + + std::vector PortableIndex::GetAllPortableFiles() + { + return m_interface->GetAllPortableFiles(m_dbconn); + } + std::unique_ptr PortableIndex::CreateIPortableIndex() const { if (m_version == Schema::Version{ 1, 0 } || diff --git a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h index c104b9344e..30bd0151cc 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h @@ -35,10 +35,14 @@ namespace AppInstaller::Repository::Microsoft bool UpdatePortableFile(const Schema::IPortableIndex::PortableFile& file); - std::optional GetPortableFileById(SQLite::rowid_t rowId); + void AddOrUpdatePortableFile(const Schema::IPortableIndex::PortableFile& file); + + std::vector GetAllPortableFiles(); bool Exists(const Schema::IPortableIndex::PortableFile& file); + bool IsEmpty(); + private: // Constructor used to open an existing index. PortableIndex(const std::string& target, SQLiteStorageBase::OpenDisposition disposition, Utility::ManagedFile&& indexFile); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/IPortableIndex.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/IPortableIndex.h index f0c1c1ebb8..e0618d06d4 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/IPortableIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/IPortableIndex.h @@ -53,10 +53,13 @@ namespace AppInstaller::Repository::Microsoft::Schema // The return value indicates whether the index was modified by the function. virtual std::pair UpdatePortableFile(SQLite::Connection& connection, const PortableFile& file) = 0; - // Returns the next PortableFile entry in the index if available. - virtual std::optional GetPortableFileById(SQLite::Connection& connection, SQLite::rowid_t id) = 0; - // Returns a bool value indicating whether the PortableFile already exists in the index. virtual bool Exists(SQLite::Connection& connection, const PortableFile& file) = 0; + + // Returns a bool value indicating whether the index is empty. + virtual bool IsEmpty(SQLite::Connection& connection) = 0; + + // Returns a vector including all the portable files recorded in the index. + virtual std::vector GetAllPortableFiles(SQLite::Connection& connection) = 0; }; } \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface.h index e89de61716..bc35cd8cca 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface.h @@ -16,7 +16,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 SQLite::rowid_t AddPortableFile(SQLite::Connection& connection, const PortableFile& file) override; SQLite::rowid_t RemovePortableFile(SQLite::Connection& connection, const PortableFile& file) override; std::pair UpdatePortableFile(SQLite::Connection& connection, const PortableFile& file) override; - std::optional GetPortableFileById(SQLite::Connection& connection, SQLite::rowid_t id) override; bool Exists(SQLite::Connection& connection, const PortableFile& file) override; + bool IsEmpty(SQLite::Connection& connection) override; + std::vector GetAllPortableFiles(SQLite::Connection& connection) override; }; } \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface_1_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface_1_0.cpp index 5d9a3a474c..5c78821117 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface_1_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface_1_0.cpp @@ -74,13 +74,18 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 return { status, portableEntryResult.value() }; } - std::optional PortableIndexInterface::GetPortableFileById(SQLite::Connection& connection, SQLite::rowid_t id) + bool PortableIndexInterface::Exists(SQLite::Connection& connection, const PortableFile& file) { - return PortableTable::GetPortableFileById(connection, id); + return GetExistingPortableFileId(connection, file).has_value(); } - bool PortableIndexInterface::Exists(SQLite::Connection& connection, const PortableFile& file) + bool PortableIndexInterface::IsEmpty(SQLite::Connection& connection) { - return GetExistingPortableFileId(connection, file).has_value(); + return PortableTable::IsEmpty(connection); + } + + std::vector PortableIndexInterface::GetAllPortableFiles(SQLite::Connection& connection) + { + return PortableTable::GetAllPortableFiles(connection); } } \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp index b6c0c830d1..4442d53148 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp @@ -150,4 +150,29 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 return (countStatement.GetColumn(0) == 0); } + + std::vector PortableTable::GetAllPortableFiles(SQLite::Connection& connection) + { + SQLite::Builder::StatementBuilder builder; + builder.Select({ s_PortableTable_FilePath_Column, + s_PortableTable_FileType_Column, + s_PortableTable_SHA256_Column, + s_PortableTable_SymlinkTarget_Column }) + .From(s_PortableTable_Table_Name); + + SQLite::Statement select = builder.Prepare(connection); + std::vector result; + while (select.Step()) + { + IPortableIndex::PortableFile portableFile; + auto [filePath, fileType, sha256, symlinkTarget] = select.GetRow(); + portableFile.SetFilePath(std::move(filePath)); + portableFile.FileType = fileType; + portableFile.SHA256 = std::move(sha256); + portableFile.SymlinkTarget = std::move(symlinkTarget); + result.emplace_back(portableFile); + } + + return result; + } } \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.h index 513e100010..479c2cc3be 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.h @@ -40,5 +40,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 // Updates the portable file in the table by id. static bool UpdatePortableFileById(SQLite::Connection& connection, SQLite::rowid_t id, const IPortableIndex::PortableFile& file); + + // Gets all portable files recorded in the index. + static std::vector GetAllPortableFiles(SQLite::Connection& connection); }; } \ No newline at end of file From e2fc0628ef10827c6c4f78c6c822da315a858714 Mon Sep 17 00:00:00 2001 From: Ryan Fu Date: Fri, 2 Sep 2022 17:12:16 -0700 Subject: [PATCH 05/22] Add Unit tests for portable installer --- src/AppInstallerCLICore/PortableInstaller.cpp | 49 ++-- src/AppInstallerCLICore/PortableInstaller.h | 30 +-- .../Workflows/PortableFlow.cpp | 19 +- .../AppInstallerCLITests.vcxproj | 2 +- .../AppInstallerCLITests.vcxproj.filters | 2 +- src/AppInstallerCLITests/PortableEntry.cpp | 95 -------- .../PortableInstaller.cpp | 209 ++++++++++++++++++ src/AppInstallerCommonCore/Filesystem.cpp | 5 + .../Public/winget/Filesystem.h | 3 + 9 files changed, 273 insertions(+), 141 deletions(-) delete mode 100644 src/AppInstallerCLITests/PortableEntry.cpp create mode 100644 src/AppInstallerCLITests/PortableInstaller.cpp diff --git a/src/AppInstallerCLICore/PortableInstaller.cpp b/src/AppInstallerCLICore/PortableInstaller.cpp index a38aeb80f9..a954becfeb 100644 --- a/src/AppInstallerCLICore/PortableInstaller.cpp +++ b/src/AppInstallerCLICore/PortableInstaller.cpp @@ -2,7 +2,6 @@ // Licensed under the MIT License. #include "pch.h" #include "PortableInstaller.h" -#include "winget/PortableARPEntry.h" #include "winget/Manifest.h" #include "winget/ManifestCommon.h" #include "winget/Filesystem.h" @@ -143,10 +142,19 @@ namespace AppInstaller::CLI::Portable IPortableIndex::PortableFileType fileType = file.FileType; const auto& filePath = file.GetFilePath(); - if (fileType == IPortableIndex::PortableFileType::File && !VerifyPortableExeHash(filePath, file.SHA256) || - fileType == IPortableIndex::PortableFileType::Symlink && fs::is_symlink(fs::symlink_status(filePath)) && !Filesystem::VerifySymlink(filePath, file.SymlinkTarget)) + if (fileType == IPortableIndex::PortableFileType::File) { - return false; + if (std::filesystem::exists(filePath) && !VerifyPortableExeHash(filePath, file.SHA256)) + { + return false; + } + } + else if (fileType == IPortableIndex::PortableFileType::Symlink) + { + if (Filesystem::SymlinkExists(filePath) && !Filesystem::VerifySymlink(filePath, file.SymlinkTarget)) + { + return false; + } } } @@ -154,9 +162,18 @@ namespace AppInstaller::CLI::Portable } else { - // symlink_status() will return false if symlink does not exist and does not follow symlink to target. - return VerifyPortableExeHash(PortableTargetFullPath, SHA256) && - fs::is_symlink(fs::symlink_status(PortableSymlinkFullPath)) && Filesystem::VerifySymlink(PortableSymlinkFullPath, PortableTargetFullPath); + if (std::filesystem::exists(PortableTargetFullPath) && !VerifyPortableExeHash(PortableTargetFullPath, SHA256)) + { + return false; + } + else if (Filesystem::SymlinkExists(PortableSymlinkFullPath) && !Filesystem::VerifySymlink(PortableSymlinkFullPath, PortableTargetFullPath)) + { + return false; + } + else + { + return true; + } } } @@ -178,7 +195,9 @@ namespace AppInstaller::CLI::Portable } AddToPathVariable(); + FinalizeRegistryEntry(); + return ERROR_SUCCESS; } @@ -230,7 +249,9 @@ namespace AppInstaller::CLI::Portable } AddToPathVariable(); + FinalizeRegistryEntry(); + return ERROR_SUCCESS; } @@ -255,8 +276,9 @@ namespace AppInstaller::CLI::Portable RemoveFromPathVariable(); - m_portableARPEntry.Delete(); AICLI_LOG(CLI, Info, << "PortableARPEntry deleted."); + m_portableARPEntry.Delete(); + return ERROR_SUCCESS; } @@ -269,8 +291,9 @@ namespace AppInstaller::CLI::Portable RemoveFromPathVariable(); - m_portableARPEntry.Delete(); AICLI_LOG(CLI, Info, << "PortableARPEntry deleted."); + m_portableARPEntry.Delete(); + return ERROR_SUCCESS; } @@ -278,8 +301,6 @@ namespace AppInstaller::CLI::Portable { bool isDirectoryCreated = false; - Commit(PortableValueName::InstallLocation, InstallLocation); - if (std::filesystem::create_directories(InstallLocation)) { AICLI_LOG(Core, Info, << "Created target install directory: " << InstallLocation); @@ -321,7 +342,7 @@ namespace AppInstaller::CLI::Portable } else { - // Symlink creation should only fail if the user executes in user mode and non-admin. + // Symlink creation should only fail if the user executes without admin rights or developer mode. // Resort to adding install directory to PATH directly. AICLI_LOG(Core, Info, << "Portable install executed in user mode. Adding package directory to PATH."); Commit(PortableValueName::InstallDirectoryAddedToPath, InstallDirectoryAddedToPath = true); @@ -373,7 +394,7 @@ namespace AppInstaller::CLI::Portable void PortableInstaller::RemoveFromPathVariable() { std::filesystem::path pathValue = GetInstallDirectoryForPathVariable(); - if (!std::filesystem::is_empty(pathValue)) + if (std::filesystem::exists(pathValue) && !std::filesystem::is_empty(pathValue)) { AICLI_LOG(Core, Info, << "Install directory is not empty: " << pathValue); } @@ -435,7 +456,6 @@ namespace AppInstaller::CLI::Portable WinGetInstallerType = GetStringValue(PortableValueName::WinGetInstallerType); WinGetPackageIdentifier = GetStringValue(PortableValueName::WinGetPackageIdentifier); WinGetSourceIdentifier = GetStringValue(PortableValueName::WinGetSourceIdentifier); - InstallLocation = GetPathValue(PortableValueName::InstallLocation); PortableSymlinkFullPath = GetPathValue(PortableValueName::PortableSymlinkFullPath); PortableTargetFullPath = GetPathValue(PortableValueName::PortableTargetFullPath); @@ -451,6 +471,7 @@ namespace AppInstaller::CLI::Portable Commit(PortableValueName::UninstallString, "winget uninstall --product-code " + GetProductCode()); Commit(PortableValueName::WinGetInstallerType, InstallerTypeToString(Manifest::InstallerTypeEnum::Portable)); Commit(PortableValueName::SHA256, SHA256); + Commit(PortableValueName::InstallLocation, InstallLocation); } void PortableInstaller::FinalizeRegistryEntry() diff --git a/src/AppInstallerCLICore/PortableInstaller.h b/src/AppInstallerCLICore/PortableInstaller.h index bd94f38289..6c89031271 100644 --- a/src/AppInstallerCLICore/PortableInstaller.h +++ b/src/AppInstallerCLICore/PortableInstaller.h @@ -1,25 +1,20 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once -#include #include "winget/PortableARPEntry.h" #include -#include -using namespace AppInstaller::Registry; using namespace AppInstaller::Registry::Portable; namespace AppInstaller::CLI::Portable { - // Helper methods for relevant locations. std::filesystem::path GetPortableLinksLocation(Manifest::ScopeEnum scope); std::filesystem::path GetPortableInstallRoot(Manifest::ScopeEnum scope, Utility::Architecture arch); - // Object representation of the arguments needed to install a portable. + // Object representation of the metadata and functionality required for installing a Portable package. struct PortableInstaller { - // Display Name std::string DisplayName; std::string DisplayVersion; std::string HelpLink; @@ -34,10 +29,9 @@ namespace AppInstaller::CLI::Portable std::string WinGetInstallerType; std::string WinGetPackageIdentifier; std::string WinGetSourceIdentifier; - - // If we fail to create a symlink, add install directory - bool InstallDirectoryAddedToPath = false; bool IsUpdate = false; + // If we fail to create a symlink, add install directory to PATH variable + bool InstallDirectoryAddedToPath = false; template void Commit(PortableValueName valueName, T value) @@ -50,6 +44,12 @@ namespace AppInstaller::CLI::Portable return InstallDirectoryAddedToPath ? InstallLocation : GetPortableLinksLocation(GetScope()); } + HRESULT SingleInstall(const std::filesystem::path& installerPath); + + HRESULT MultipleInstall( + const std::vector& nestedInstallerFiles, + const std::vector& extractedItems); + HRESULT Uninstall(bool purge = false) { if (std::filesystem::exists(GetPortableIndexPath())) @@ -62,11 +62,6 @@ namespace AppInstaller::CLI::Portable } } - HRESULT SingleInstall(const std::filesystem::path& installerPath); - HRESULT MultipleInstall( - const std::vector& nestedInstallerFiles, - const std::vector& extractedItems); - bool VerifyPortableFilesForUninstall(); Manifest::ScopeEnum GetScope() { return m_portableARPEntry.GetScope(); }; @@ -85,10 +80,6 @@ namespace AppInstaller::CLI::Portable void SetAppsAndFeaturesMetadata(const Manifest::AppsAndFeaturesEntry& entry, const Manifest::Manifest& manifest); - void MovePortableExe(const std::filesystem::path& installerPath); - - bool CreatePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath); - std::string GetOutputMessage() { return m_stream.str(); @@ -114,6 +105,9 @@ namespace AppInstaller::CLI::Portable HRESULT UninstallSingle(bool purge = false); HRESULT UninstallFromIndex(bool purge = false); + void MovePortableExe(const std::filesystem::path& installerPath); + bool CreatePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath); + void RemovePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath); void RemoveInstallDirectory(bool purge); void RemovePortableExe(const std::filesystem::path& targetPath, const std::string& hash); diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp index 5b33580378..a3fd81c419 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -2,19 +2,14 @@ // Licensed under the MIT License. #include "pch.h" #include "PortableFlow.h" +#include "PortableInstaller.h" #include "WorkflowBase.h" #include "winget/Filesystem.h" -#include "PortableInstaller.h" -#include "winget/PathVariable.h" using namespace AppInstaller::Manifest; -using namespace AppInstaller::Utility; -using namespace AppInstaller::Registry; -using namespace AppInstaller::Registry::Portable; -using namespace AppInstaller::Registry::Environment; using namespace AppInstaller::Repository; +using namespace AppInstaller::Utility; using namespace AppInstaller::CLI::Portable; -using namespace std::filesystem; namespace AppInstaller::CLI::Workflow { @@ -216,10 +211,11 @@ namespace AppInstaller::CLI::Workflow portableInstaller.InstallLocation = GetPortableTargetDirectory(context); portableInstaller.IsUpdate = isUpdate; - // These are not needed if installing from archive with multiple portables - // But we'll set them regardless for convenience. - portableInstaller.PortableTargetFullPath = GetPortableTargetFullPath(context); - portableInstaller.PortableSymlinkFullPath = GetPortableSymlinkFullPath(context); + if (!IsArchiveType(context.Get()->BaseInstallerType)) + { + portableInstaller.PortableTargetFullPath = GetPortableTargetFullPath(context); + portableInstaller.PortableSymlinkFullPath = GetPortableSymlinkFullPath(context); + } portableInstaller.SetAppsAndFeaturesMetadata(context.Get(), context.Get()->AppsAndFeaturesEntries); context.Add(std::move(portableInstaller)); @@ -254,7 +250,6 @@ namespace AppInstaller::CLI::Workflow context.Add(Workflow::HandleException(context, std::current_exception())); } - // Perform cleanup only if the install fails and is not an update. if (result != ERROR_SUCCESS && !portableInstaller.IsUpdate) { context.Reporter.Warn() << Resource::String::PortableInstallFailed << std::endl; diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index 5fe317e436..34e7ab1217 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -212,7 +212,7 @@ - + diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters index e4f7c48cfd..393b2b81cd 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -227,7 +227,7 @@ Source Files\Common - + Source Files\Common diff --git a/src/AppInstallerCLITests/PortableEntry.cpp b/src/AppInstallerCLITests/PortableEntry.cpp deleted file mode 100644 index 7fe492f7d7..0000000000 --- a/src/AppInstallerCLITests/PortableEntry.cpp +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#include "pch.h" -#include "TestCommon.h" -#include -#include -#include -#include - -using namespace AppInstaller::Utility; -using namespace AppInstaller::CLI::Portable; -using namespace TestCommon; - -TEST_CASE("VerifyPortableMove", "[PortableEntry]") -{ - PortableInstaller testEntry = PortableInstaller( - AppInstaller::Manifest::ScopeEnum::User, - Architecture::X64, - "testProductCode"); - - TestCommon::TempDirectory tempDirectory("TempDirectory", false); - testEntry.InstallLocation = tempDirectory.GetPath(); - testEntry.PortableTargetFullPath = tempDirectory.GetPath() / "output.txt"; - - TestCommon::TempFile testFile("input.txt"); - std::ofstream file(testFile.GetPath(), std::ofstream::out); - file.close(); - - testEntry.MovePortableExe(testFile.GetPath()); - REQUIRE(std::filesystem::exists(testEntry.PortableTargetFullPath)); - - // Create a second PortableEntry instance to emulate installing for a second time. (ARP entry should already exist) - PortableInstaller testEntry2 = PortableInstaller( - AppInstaller::Manifest::ScopeEnum::User, - Architecture::X64, - "testProductCode"); - - testEntry2.InstallLocation = tempDirectory.GetPath(); - testEntry2.PortableTargetFullPath = tempDirectory.GetPath() / "output2.txt"; - - TestCommon::TempFile testFile2("input2.txt"); - std::ofstream file2(testFile2, std::ofstream::out); - file2.close(); - - testEntry2.MovePortableExe(testFile2.GetPath()); - REQUIRE(std::filesystem::exists(testEntry2.PortableTargetFullPath)); - // InstallDirectoryCreated value should be preserved even though the directory was not created; - //testEntry2.RemoveARPEntry(); -} - -TEST_CASE("VerifySymlinkCheck", "[PortableEntry]") -{ - PortableInstaller testEntry = PortableInstaller( - AppInstaller::Manifest::ScopeEnum::User, - Architecture::X64, - "testProductCode"); - - TestCommon::TempFile testFile("target.txt"); - std::ofstream file(testFile.GetPath(), std::ofstream::out); - file.close(); - - TestCommon::TempDirectory tempDirectory("TempDirectory", true); - testEntry.PortableTargetFullPath = testFile.GetPath(); - testEntry.PortableSymlinkFullPath = tempDirectory.GetPath() / "symlink.exe"; - - testEntry.CreatePortableSymlink(testEntry.PortableTargetFullPath, testEntry.PortableSymlinkFullPath); - - //REQUIRE(testEntry.VerifySymlinkTarget()); - - // Modify with incorrect target full path. - testEntry.PortableTargetFullPath = tempDirectory.GetPath() / "invalidTarget.txt"; - //REQUIRE_FALSE(testEntry.VerifySymlinkTarget()); - //testEntry.RemoveARPEntry(); -} - -//TEST_CASE("VerifyPathVariableModified", "[PortableEntry]") -//{ -// PortableEntry testEntry = PortableEntry( -// AppInstaller::Manifest::ScopeEnum::User, -// Architecture::X64, -// "testProductCode"); -// -// testEntry.InstallDirectoryAddedToPath = true; -// TestCommon::TempDirectory tempDirectory("TempDirectory", false); -// const std::filesystem::path& pathValue = tempDirectory.GetPath(); -// testEntry.InstallLocation = pathValue; -// testEntry.AddToPathVariable(); -// -// AppInstaller::Registry::Environment::PathVariable pathVariable(AppInstaller::Manifest::ScopeEnum::User); -// REQUIRE(pathVariable.Contains(pathValue)); -// -// testEntry.RemoveFromPathVariable(); -// REQUIRE_FALSE(pathVariable.Contains(pathValue)); -// testEntry.RemoveARPEntry(); -//} \ No newline at end of file diff --git a/src/AppInstallerCLITests/PortableInstaller.cpp b/src/AppInstallerCLITests/PortableInstaller.cpp new file mode 100644 index 0000000000..70a08ea89e --- /dev/null +++ b/src/AppInstallerCLITests/PortableInstaller.cpp @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace AppInstaller::CLI::Portable; +using namespace AppInstaller::Filesystem; +using namespace AppInstaller::Manifest; +using namespace AppInstaller::Registry::Environment; +using namespace AppInstaller::Repository::Microsoft; +using namespace AppInstaller::Repository::SQLite; +using namespace AppInstaller::Repository::Microsoft::Schema; +using namespace AppInstaller::Utility; +using namespace TestCommon; + +PortableInstaller CreateTestPortableInstaller(const std::filesystem::path& installLocation) +{ + PortableInstaller portableInstaller = PortableInstaller( + ScopeEnum::User, + Architecture::X64, + "testProductCode"); + + const auto& filename = "testPortable.txt"; + const auto& alias = "testSymlink.exe"; + + portableInstaller.InstallLocation = installLocation; + portableInstaller.PortableTargetFullPath = installLocation / filename; + portableInstaller.PortableSymlinkFullPath = GetPortableLinksLocation(ScopeEnum::User) / alias; + return portableInstaller; +} + +std::vector CreateExtractedItemsFromArchive(const std::filesystem::path& installLocation) +{ + std::vector extractedItems; + const auto& itemPath = installLocation / "testPortable.txt"; + const auto& itemPath2 = installLocation / "testPortable2.txt"; + extractedItems.emplace_back(itemPath); + extractedItems.emplace_back(itemPath2); + + for (const auto& item : extractedItems) + { + std::ofstream file(item, std::ofstream::out); + file.close(); + } + + return extractedItems; +} + +std::vector CreateTestNestedInstallerFiles() +{ + std::vector nestedInstallerFiles; + NestedInstallerFile file; + file.RelativeFilePath = "testPortable.txt"; + nestedInstallerFiles.emplace_back(file); + + NestedInstallerFile file2; + file2.RelativeFilePath = "testPortable2.txt"; + file2.PortableCommandAlias = "testSymlink.exe"; + nestedInstallerFiles.emplace_back(file2); + + return nestedInstallerFiles; +} + +// Ensures that the portable exes and symlinks all got recorded in the index. +void VerifyPortableFilesTrackedByIndex( + const std::filesystem::path& indexPath, + const std::vector& extractedItems, + const std::vector& nestedInstallerFiles) +{ + { + // Verify that files were added to index. + Connection connection = Connection::Create(indexPath.u8string(), Connection::OpenDisposition::ReadWrite); + REQUIRE(!Schema::Portable_V1_0::PortableTable::IsEmpty(connection)); + + for (const auto& item : extractedItems) + { + REQUIRE(Schema::Portable_V1_0::PortableTable::SelectByFilePath(connection, item).has_value()); + } + + // Verify that all symlinks were added to the index. + for (const auto& item : nestedInstallerFiles) + { + std::filesystem::path symlinkPath = GetPortableLinksLocation(ScopeEnum::User); + if (!item.PortableCommandAlias.empty()) + { + symlinkPath /= ConvertToUTF16(item.PortableCommandAlias); + } + else + { + symlinkPath /= ConvertToUTF16(item.RelativeFilePath); + } + + AppendExtension(symlinkPath, ".exe"); + + REQUIRE(Schema::Portable_V1_0::PortableTable::SelectByFilePath(connection, symlinkPath).has_value()); + } + } +} + +TEST_CASE("PortableInstaller_SingleInstall", "[PortableInstaller]") +{ + TempDirectory tempDirectory = TestCommon::TempDirectory("TempDirectory", false); + + { + PortableInstaller portableInstaller = CreateTestPortableInstaller(tempDirectory.GetPath()); + + TestCommon::TempFile testPortable("testPortable.txt"); + std::ofstream file2(testPortable, std::ofstream::out); + file2.close(); + + HRESULT installResult = portableInstaller.SingleInstall(testPortable.GetPath()); + REQUIRE(SUCCEEDED(installResult)); + } + + { + // Create a new portable installer instance and verify that values from ARP are loaded correctly. + PortableInstaller portableInstaller = PortableInstaller(ScopeEnum::User, Architecture::X64, "testProductCode"); + + REQUIRE(std::filesystem::exists(portableInstaller.PortableTargetFullPath)); + REQUIRE(SymlinkExists(portableInstaller.PortableSymlinkFullPath)); + REQUIRE(VerifySymlink(portableInstaller.PortableSymlinkFullPath, portableInstaller.PortableTargetFullPath)); + REQUIRE(portableInstaller.Exists()); + + HRESULT uninstallResult = portableInstaller.Uninstall(); + + REQUIRE(SUCCEEDED(uninstallResult)); + REQUIRE_FALSE(SymlinkExists(portableInstaller.PortableSymlinkFullPath)); + REQUIRE_FALSE(std::filesystem::exists(portableInstaller.PortableTargetFullPath)); + REQUIRE_FALSE(std::filesystem::exists(portableInstaller.InstallLocation)); + } +} + +TEST_CASE("PortableInstaller_InstallDirectoryAddedToPath", "[PortableInstaller]") +{ + TempDirectory tempDirectory = TestCommon::TempDirectory("TempDirectory", false); + + { + PortableInstaller portableInstaller = CreateTestPortableInstaller(tempDirectory.GetPath()); + + TestCommon::TempFile testPortable("testPortable.txt"); + std::ofstream file2(testPortable, std::ofstream::out); + file2.close(); + + // This value is only set to true if we fail to create a symlink. + // If true no symlink should be created and InstallDirectory is added to PATH variable. + portableInstaller.Commit(PortableValueName::InstallDirectoryAddedToPath, portableInstaller.InstallDirectoryAddedToPath = true); + + HRESULT installResult = portableInstaller.SingleInstall(testPortable.GetPath()); + REQUIRE(SUCCEEDED(installResult)); + } + + { + PortableInstaller portableInstaller = PortableInstaller(ScopeEnum::User, Architecture::X64, "testProductCode"); + + REQUIRE(std::filesystem::exists(portableInstaller.PortableTargetFullPath)); + REQUIRE_FALSE(SymlinkExists(portableInstaller.PortableSymlinkFullPath)); + REQUIRE(PathVariable(ScopeEnum::User).Contains(portableInstaller.InstallLocation)); + REQUIRE(portableInstaller.Exists()); + + HRESULT uninstallResult = portableInstaller.Uninstall(); + + REQUIRE(SUCCEEDED(uninstallResult)); + REQUIRE(portableInstaller.InstallDirectoryAddedToPath); + REQUIRE_FALSE(std::filesystem::exists(portableInstaller.PortableTargetFullPath)); + REQUIRE_FALSE(std::filesystem::exists(portableInstaller.InstallLocation)); + REQUIRE_FALSE(PathVariable(ScopeEnum::User).Contains(portableInstaller.InstallLocation)); + } +} + +TEST_CASE("PortableInstaller_MultipleInstall", "[PortableInstaller]") +{ + PortableInstaller portableInstaller = PortableInstaller( + ScopeEnum::User, + Architecture::X64, + "testProductCode"); + + TempDirectory tempDirectory = TestCommon::TempDirectory("TempDirectory", true); + const auto& tempDirectoryPath = tempDirectory.GetPath(); + portableInstaller.InstallLocation = tempDirectoryPath; + + std::vector nestedInstallerFiles = CreateTestNestedInstallerFiles(); + std::vector extractedItems = CreateExtractedItemsFromArchive(tempDirectoryPath); + + HRESULT installResult = portableInstaller.MultipleInstall(nestedInstallerFiles, extractedItems); + REQUIRE(SUCCEEDED(installResult)); + + const auto& indexPath = portableInstaller.GetPortableIndexPath(); + REQUIRE(std::filesystem::exists(indexPath)); + + VerifyPortableFilesTrackedByIndex(indexPath, extractedItems, nestedInstallerFiles); + + REQUIRE(portableInstaller.VerifyPortableFilesForUninstall()); + + // Perform uninstall + HRESULT uninstallResult = portableInstaller.Uninstall(); + REQUIRE(SUCCEEDED(uninstallResult)); + + REQUIRE_FALSE(std::filesystem::exists(indexPath)); + REQUIRE_FALSE(std::filesystem::exists(portableInstaller.InstallLocation)); +} diff --git a/src/AppInstallerCommonCore/Filesystem.cpp b/src/AppInstallerCommonCore/Filesystem.cpp index 3bd97575aa..14cb0e46f1 100644 --- a/src/AppInstallerCommonCore/Filesystem.cpp +++ b/src/AppInstallerCommonCore/Filesystem.cpp @@ -181,4 +181,9 @@ namespace AppInstaller::Filesystem target += value; } } + + bool SymlinkExists(const std::filesystem::path& symlinkPath) + { + return std::filesystem::is_symlink(std::filesystem::symlink_status(symlinkPath)); + } } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/Filesystem.h b/src/AppInstallerCommonCore/Public/winget/Filesystem.h index af82999521..42ab90296b 100644 --- a/src/AppInstallerCommonCore/Public/winget/Filesystem.h +++ b/src/AppInstallerCommonCore/Public/winget/Filesystem.h @@ -28,4 +28,7 @@ namespace AppInstaller::Filesystem // Appends the .exe extension to the path if not present. void AppendExtension(std::filesystem::path& value, const std::string& extension); + + // Checks if the path is a symlink and exists. + bool SymlinkExists(const std::filesystem::path& symlinkPath); } \ No newline at end of file From 150c41b5a5df97ba35ccdc49f6254686d45b8bf0 Mon Sep 17 00:00:00 2001 From: Ryan Fu Date: Tue, 6 Sep 2022 16:43:57 -0700 Subject: [PATCH 06/22] add tests for portable in zip --- .../AppInstallerCLICore.vcxproj.filters | 3 - .../ExecutionContextData.h | 2 +- src/AppInstallerCLICore/PortableInstaller.cpp | 52 ++------- src/AppInstallerCLICore/PortableInstaller.h | 43 ++++--- .../Workflows/ArchiveFlow.cpp | 17 +-- .../Workflows/InstallFlow.cpp | 2 +- .../Workflows/PortableFlow.cpp | 6 +- .../Workflows/PortableFlow.h | 23 +++- src/AppInstallerCLIE2ETests/InstallCommand.cs | 32 ++++-- .../TestZipInstaller_Portable.2.0.0.0.yaml | 18 +++ .../Manifests/TestZipInstaller_Portable.yaml | 18 +++ .../UninstallCommand.cs | 43 +++++-- src/AppInstallerCLIE2ETests/UpgradeCommand.cs | 20 ++++ .../AppInstallerCLITests.vcxproj | 4 +- .../AppInstallerCLITests.vcxproj.filters | 4 +- src/AppInstallerCLITests/Filesystem.cpp | 28 +++++ src/AppInstallerCLITests/PortableIndex.cpp | 4 +- .../PortableInstaller.cpp | 105 ++++++++++++++++++ ...hExe.yaml => InstallFlowTest_Zip_Exe.yaml} | 0 ...thExe.yaml => UpdateFlowTest_Zip_Exe.yaml} | 0 src/AppInstallerCLITests/WorkFlow.cpp | 8 +- .../Manifest/ManifestCommon.cpp | 5 + .../Public/winget/ManifestCommon.h | 3 + .../Microsoft/PortableIndex.cpp | 27 +++++ .../Microsoft/PortableIndex.h | 2 + 25 files changed, 356 insertions(+), 113 deletions(-) create mode 100644 src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Portable.2.0.0.0.yaml create mode 100644 src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Portable.yaml rename src/AppInstallerCLITests/TestData/{InstallFlowTest_ZipWithExe.yaml => InstallFlowTest_Zip_Exe.yaml} (100%) rename src/AppInstallerCLITests/TestData/{UpdateFlowTest_ZipWithExe.yaml => UpdateFlowTest_Zip_Exe.yaml} (100%) diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters index f96b062d52..5ddf0b00df 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -340,9 +340,6 @@ Workflows - - Source Files - Source Files diff --git a/src/AppInstallerCLICore/ExecutionContextData.h b/src/AppInstallerCLICore/ExecutionContextData.h index 65d768f78d..fa6f441f92 100644 --- a/src/AppInstallerCLICore/ExecutionContextData.h +++ b/src/AppInstallerCLICore/ExecutionContextData.h @@ -6,8 +6,8 @@ #include #include "CompletionData.h" #include "PackageCollection.h" +#include "PortableInstaller.h" #include "Workflows/WorkflowBase.h" -#include #include #include diff --git a/src/AppInstallerCLICore/PortableInstaller.cpp b/src/AppInstallerCLICore/PortableInstaller.cpp index a954becfeb..55a107f2be 100644 --- a/src/AppInstallerCLICore/PortableInstaller.cpp +++ b/src/AppInstallerCLICore/PortableInstaller.cpp @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #include "pch.h" +#include "ExecutionContext.h" #include "PortableInstaller.h" #include "winget/Manifest.h" #include "winget/ManifestCommon.h" @@ -9,8 +10,6 @@ #include "Microsoft/PortableIndex.h" #include "Microsoft/Schema/IPortableIndex.h" #include -#include -#include "ExecutionContext.h" using namespace AppInstaller::Utility; using namespace AppInstaller::Registry; @@ -27,33 +26,6 @@ namespace AppInstaller::CLI::Portable { constexpr std::wstring_view c_PortableIndexFileName = L"portable.db"; - IPortableIndex::PortableFile CreatePortableFileFromPath(const std::filesystem::path& path) - { - IPortableIndex::PortableFile portableFile; - portableFile.SetFilePath(path); - - if (std::filesystem::is_directory(path)) - { - portableFile.FileType = IPortableIndex::PortableFileType::Directory; - } - else if (std::filesystem::is_symlink(path)) - { - portableFile.FileType = IPortableIndex::PortableFileType::Symlink; - portableFile.SymlinkTarget = std::filesystem::read_symlink(path).u8string(); - } - else - { - portableFile.FileType = IPortableIndex::PortableFileType::File; - - std::ifstream inStream{ path, std::ifstream::binary }; - const Utility::SHA256::HashBuffer& targetFileHash = Utility::SHA256::ComputeHash(inStream); - inStream.close(); - portableFile.SHA256 = Utility::SHA256::ConvertToString(targetFileHash); - } - - return portableFile; - } - bool VerifyPortableExeHash(const std::filesystem::path& targetPath, const std::string& hashValue) { std::ifstream inStream{ targetPath, std::ifstream::binary }; @@ -63,7 +35,7 @@ namespace AppInstaller::CLI::Portable return Utility::SHA256::AreEqual(Utility::SHA256::ConvertToBytes(hashValue), targetFileHash); } - void RemoveItemsFromIndex(const std::filesystem::path& indexPath, bool purge) + void RemoveItemsFromIndex(const std::filesystem::path& indexPath) { bool deleteIndex = false; { @@ -72,21 +44,19 @@ namespace AppInstaller::CLI::Portable for (const auto& file : portableFiles) { - IPortableIndex::PortableFileType fileType = file.FileType; const auto& filePath = file.GetFilePath(); - if (purge || (fileType == IPortableIndex::PortableFileType::File && VerifyPortableExeHash(filePath, file.SHA256)) || - (fileType == IPortableIndex::PortableFileType::Symlink && Filesystem::VerifySymlink(filePath, file.SymlinkTarget))) + if (std::filesystem::exists(filePath)) { std::filesystem::remove_all(filePath); - portableIndex.RemovePortableFile(file); } + + portableIndex.RemovePortableFile(file); } if (portableIndex.IsEmpty()) { deleteIndex = true; - }; } @@ -131,9 +101,8 @@ namespace AppInstaller::CLI::Portable bool PortableInstaller::VerifyPortableFilesForUninstall() { - namespace fs = std::filesystem; const auto& indexPath = GetPortableIndexPath(); - if (fs:: exists(indexPath)) + if (std::filesystem::exists(indexPath)) { PortableIndex portableIndex = PortableIndex::Open(indexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); std::vector portableFiles = portableIndex.GetAllPortableFiles(); @@ -215,7 +184,7 @@ namespace AppInstaller::CLI::Portable for (auto& item : extractedItems) { const auto& itemPath = InstallLocation / item; - IPortableIndex::PortableFile portableFile = CreatePortableFileFromPath(itemPath); + IPortableIndex::PortableFile portableFile = PortableIndex::CreatePortableFileFromPath(itemPath); portableIndex.AddOrUpdatePortableFile(portableFile); } @@ -238,13 +207,14 @@ namespace AppInstaller::CLI::Portable const std::filesystem::path& symlinkFullPath = GetPortableLinksLocation(GetScope()) / commandAlias; if (CreatePortableSymlink(portableTargetPath, symlinkFullPath)) { - IPortableIndex::PortableFile symlinkPortableFile = CreatePortableFileFromPath(symlinkFullPath); + IPortableIndex::PortableFile symlinkPortableFile = PortableIndex::CreatePortableFileFromPath(symlinkFullPath); portableIndex.AddOrUpdatePortableFile(symlinkPortableFile); } } else { AICLI_LOG(CLI, Info, << "Package directory was previously added to PATH. Skipping symlink creation."); + break; } } @@ -259,8 +229,8 @@ namespace AppInstaller::CLI::Portable { if (std::filesystem::exists(PortableTargetFullPath)) { - std::filesystem::remove(PortableTargetFullPath); AICLI_LOG(CLI, Info, << "Successfully deleted portable exe:" << PortableTargetFullPath); + std::filesystem::remove(PortableTargetFullPath); } else { @@ -285,7 +255,7 @@ namespace AppInstaller::CLI::Portable HRESULT PortableInstaller::UninstallFromIndex(bool purge) { const auto& indexPath = GetPortableIndexPath(); - RemoveItemsFromIndex(indexPath, purge); + RemoveItemsFromIndex(indexPath); RemoveInstallDirectory(purge); diff --git a/src/AppInstallerCLICore/PortableInstaller.h b/src/AppInstallerCLICore/PortableInstaller.h index 6c89031271..2400486fe7 100644 --- a/src/AppInstallerCLICore/PortableInstaller.h +++ b/src/AppInstallerCLICore/PortableInstaller.h @@ -8,6 +8,7 @@ using namespace AppInstaller::Registry::Portable; namespace AppInstaller::CLI::Portable { + std::filesystem::path GetPortableLinksLocation(Manifest::ScopeEnum scope); std::filesystem::path GetPortableInstallRoot(Manifest::ScopeEnum scope, Utility::Architecture arch); @@ -33,16 +34,7 @@ namespace AppInstaller::CLI::Portable // If we fail to create a symlink, add install directory to PATH variable bool InstallDirectoryAddedToPath = false; - template - void Commit(PortableValueName valueName, T value) - { - m_portableARPEntry.SetValue(valueName, value); - } - - std::filesystem::path GetInstallDirectoryForPathVariable() - { - return InstallDirectoryAddedToPath ? InstallLocation : GetPortableLinksLocation(GetScope()); - } + PortableInstaller(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode); HRESULT SingleInstall(const std::filesystem::path& installerPath); @@ -62,6 +54,19 @@ namespace AppInstaller::CLI::Portable } } + template + void Commit(PortableValueName valueName, T value) + { + m_portableARPEntry.SetValue(valueName, value); + } + + std::filesystem::path GetInstallDirectoryForPathVariable() + { + return InstallDirectoryAddedToPath ? InstallLocation : GetPortableLinksLocation(GetScope()); + } + + std::filesystem::path GetPortableIndexPath(); + bool VerifyPortableFilesForUninstall(); Manifest::ScopeEnum GetScope() { return m_portableARPEntry.GetScope(); }; @@ -70,29 +75,19 @@ namespace AppInstaller::CLI::Portable std::string GetProductCode() { return m_portableARPEntry.GetProductCode(); }; - std::string GetPackageIdentifier() { return m_packageIdentifier; }; - - std::string GetSourceIdentifier() { return m_sourceIdentifier; }; - bool Exists() { return m_portableARPEntry.Exists(); }; - PortableInstaller(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode); - - void SetAppsAndFeaturesMetadata(const Manifest::AppsAndFeaturesEntry& entry, const Manifest::Manifest& manifest); - std::string GetOutputMessage() { return m_stream.str(); } - std::filesystem::path GetPortableIndexPath(); - - void SetAppsAndFeaturesMetadata(const Manifest::Manifest& manifest, const std::vector& entries); + void SetAppsAndFeaturesMetadata( + const Manifest::Manifest& manifest, + const std::vector& entries); private: PortableARPEntry m_portableARPEntry; - std::string m_packageIdentifier; - std::string m_sourceIdentifier; std::stringstream m_stream; std::string GetStringValue(PortableValueName valueName); @@ -108,9 +103,9 @@ namespace AppInstaller::CLI::Portable void MovePortableExe(const std::filesystem::path& installerPath); bool CreatePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath); + void RemovePortableExe(const std::filesystem::path& targetPath, const std::string& hash); void RemovePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath); void RemoveInstallDirectory(bool purge); - void RemovePortableExe(const std::filesystem::path& targetPath, const std::string& hash); void AddToPathVariable(); void RemoveFromPathVariable(); diff --git a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp index f9f19e6a66..c875e2fe4f 100644 --- a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp @@ -5,7 +5,6 @@ #include "winget/Archive.h" #include "winget/Filesystem.h" #include "PortableFlow.h" -#include "PortableInstaller.h" using namespace AppInstaller::Manifest; @@ -18,22 +17,20 @@ namespace AppInstaller::CLI::Workflow std::vector extractedItems; bool isDirectoryCreated = false; - HRESULT hr; if (context.Get()->NestedInstallerType == InstallerTypeEnum::Portable) { destinationFolder = GetPortableTargetDirectory(context); isDirectoryCreated = std::filesystem::create_directory(destinationFolder); - hr = AppInstaller::Archive::TryExtractArchive(installerPath, destinationFolder, extractedItems); } else { destinationFolder = installerPath.parent_path(); - hr = AppInstaller::Archive::TryExtractArchive(installerPath, destinationFolder, extractedItems); } AICLI_LOG(CLI, Info, << "Extracting archive to: " << destinationFolder); + HRESULT result = AppInstaller::Archive::TryExtractArchive(installerPath, destinationFolder, extractedItems); - if (SUCCEEDED(hr)) + if (SUCCEEDED(result)) { AICLI_LOG(CLI, Info, << "Successfully extracted archive"); context.Add(extractedItems); @@ -45,7 +42,7 @@ namespace AppInstaller::CLI::Workflow std::filesystem::remove(destinationFolder); } - AICLI_LOG(CLI, Info, << "Failed to extract archive with code " << hr); + 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); } @@ -61,10 +58,8 @@ namespace AppInstaller::CLI::Workflow AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INVALID_MANIFEST); } - - InstallerTypeEnum nestedInstallerType = installer.NestedInstallerType; std::filesystem::path destinationFolder; - if (nestedInstallerType == InstallerTypeEnum::Portable) + if (IsPortableType(installer.NestedInstallerType)) { destinationFolder = GetPortableTargetDirectory(context); } @@ -89,9 +84,9 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Error() << Resource::String::NestedInstallerNotFound << ' ' << nestedInstallerPath << std::endl; AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NESTEDINSTALLER_NOT_FOUND); } - else if (nestedInstallerType != InstallerTypeEnum::Portable) + else if (!IsPortableType(installer.NestedInstallerType)) { - // Only update the installerPath if it points to a non-portable installer. + // Update the installerPath to the extracted non-portable installer. AICLI_LOG(CLI, Info, << "Setting installerPath to: " << nestedInstallerPath); context.Add(nestedInstallerPath); } diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp index e2b2772400..51561fe650 100644 --- a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -334,7 +334,7 @@ namespace AppInstaller::CLI::Workflow void PortableInstall(Execution::Context& context) { context << - GetPortableEntryForInstall << + InitializePortableInstaller << VerifyPackageAndSourceMatch << PortableInstallImpl << ReportInstallerResult("Portable"sv, APPINSTALLER_CLI_ERROR_PORTABLE_INSTALL_FAILED, true); diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp index a3fd81c419..0d9b188942 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -185,7 +185,7 @@ namespace AppInstaller::CLI::Workflow portableInstaller.WinGetSourceIdentifier = sourceIdentifier; } - void GetPortableEntryForInstall(Execution::Context& context) + void InitializePortableInstaller(Execution::Context& context) { Manifest::ScopeEnum scope = Manifest::ScopeEnum::Unknown; bool isUpdate = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate); @@ -285,14 +285,12 @@ namespace AppInstaller::CLI::Workflow HRESULT result = portableInstaller.Uninstall(purge); context.Add(result); - + context.Reporter.Warn() << portableInstaller.GetOutputMessage(); } catch (...) { context.Add(Workflow::HandleException(context, std::current_exception())); } - - context.Reporter.Warn() << portableInstaller.GetOutputMessage(); } void EnsureSupportForPortableInstall(Execution::Context& context) diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.h b/src/AppInstallerCLICore/Workflows/PortableFlow.h index 942e7b4c1e..3dc6c28bdb 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.h +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.h @@ -17,14 +17,33 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void PortableUninstallImpl(Execution::Context& context); + // Verifies that the portable install operation is supported. + // Required Args: None + // Inputs: Scope, Rename + // Outputs: None void EnsureSupportForPortableInstall(Execution::Context& context); + // Verifies that the portable uninstall operation is supported. + // Required Args: None + // Inputs: Scope + // Outputs: None void EnsureSupportForPortableUninstall(Execution::Context& context); - void GetPortableEntryForInstall(Execution::Context& context); + // Initializes the portable installer. + // Required Args: None + // Inputs: Scope, Architecture, Manifest, Installer + // Outputs: None + void InitializePortableInstaller(Execution::Context& context); + // Verifies that the package identifier and the source identifier match the ARP entry. + // Required Args: None + // Inputs: Manifest, PackageVersion, PortableInstaller + // Outputs: None void VerifyPackageAndSourceMatch(Execution::Context& context); - // Returns the target install directory for the portable package. + // Returns the target install directory for portable installation. + // Required Args: None + // Inputs: Scope, Arch, InstallLocation + // Outputs: Path std::filesystem::path GetPortableTargetDirectory(Execution::Context& context); } \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/InstallCommand.cs b/src/AppInstallerCLIE2ETests/InstallCommand.cs index 503276b9eb..f5a4456d77 100644 --- a/src/AppInstallerCLIE2ETests/InstallCommand.cs +++ b/src/AppInstallerCLIE2ETests/InstallCommand.cs @@ -161,7 +161,7 @@ public void InstallPortableExe() packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier; commandAlias = fileName = "AppInstallerTestExeInstaller.exe"; - var result = TestCommon.RunAICLICommand("install", "AppInstallerTest.TestPortableExe"); + var result = TestCommon.RunAICLICommand("install", $"{packageId}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(result.StdOut.Contains("Successfully installed")); // If no location specified, default behavior is to create a package directory with the name "{packageId}_{sourceId}" @@ -237,7 +237,7 @@ public void InstallPortableToExistingDirectory() productCode = packageId + "_" + Constants.TestSourceIdentifier; commandAlias = fileName = "AppInstallerTestExeInstaller.exe"; - var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestPortableExe -l {existingDir}"); + var result = TestCommon.RunAICLICommand("install", $"{packageId} -l {existingDir}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(result.StdOut.Contains("Successfully installed")); TestCommon.VerifyPortablePackage(existingDir, commandAlias, fileName, productCode, true); @@ -258,7 +258,7 @@ public void InstallPortableFailsWithCleanup() string conflictDirectory = Path.Combine(symlinkDirectory, commandAlias); Directory.CreateDirectory(conflictDirectory); - var result = TestCommon.RunAICLICommand("install", "AppInstallerTest.TestPortableExe"); + var result = TestCommon.RunAICLICommand("install", $"{packageId}"); // Remove directory prior to assertions as this will impact other tests if assertions fail. Directory.Delete(conflictDirectory, true); @@ -277,7 +277,7 @@ public void ReinstallPortable() packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier; commandAlias = fileName = "AppInstallerTestExeInstaller.exe"; - var result = TestCommon.RunAICLICommand("install", "AppInstallerTest.TestPortableExe"); + var result = TestCommon.RunAICLICommand("install", $"{packageId}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); string symlinkDirectory = TestCommon.GetPortableSymlinkDirectory(TestCommon.Scope.User); @@ -288,7 +288,7 @@ public void ReinstallPortable() Assert.False(result.StdOut.Contains($"Overwriting existing file: {symlinkPath}")); // Perform second install and verify that file overwrite message is displayed. - var result2 = TestCommon.RunAICLICommand("install", "AppInstallerTest.TestPortableExe"); + var result2 = TestCommon.RunAICLICommand("install", $"{packageId}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result2.ExitCode); Assert.True(result2.StdOut.Contains("Successfully installed")); Assert.True(result2.StdOut.Contains($"Overwriting existing file: {symlinkPath}")); @@ -307,7 +307,7 @@ public void InstallPortable_UserScope() packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier; commandAlias = fileName = "AppInstallerTestExeInstaller.exe"; - var result = TestCommon.RunAICLICommand("install", "AppInstallerTest.TestPortableExe --scope user"); + var result = TestCommon.RunAICLICommand("install", $"{packageId} --scope user"); ConfigureInstallBehavior(Constants.PortablePackageUserRoot, string.Empty); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(result.StdOut.Contains("Successfully installed")); @@ -325,7 +325,7 @@ public void InstallPortable_MachineScope() packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier; commandAlias = fileName = "AppInstallerTestExeInstaller.exe"; - var result = TestCommon.RunAICLICommand("install", "AppInstallerTest.TestPortableExe --scope machine"); + var result = TestCommon.RunAICLICommand("install", $"{packageId} --scope machine"); ConfigureInstallBehavior(Constants.PortablePackageMachineRoot, string.Empty); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(result.StdOut.Contains("Successfully installed")); @@ -333,7 +333,7 @@ public void InstallPortable_MachineScope() } [Test] - public void InstallZipWithExe() + public void InstallZip_Exe() { var installDir = TestCommon.GetRandomTestDir(); var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestZipInstallerWithExe --silent -l {installDir}"); @@ -342,6 +342,22 @@ public void InstallZipWithExe() Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(installDir, "/execustom")); } + [Test] + public void InstallZip_Portable() + { + string installDir = TestCommon.GetPortablePackagesDirectory(); + string packageId, commandAlias, fileName, packageDirName, productCode; + packageId = "AppInstallerTest.TestZipInstallerWithPortable"; + packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier; + commandAlias = "TestPortable.exe"; + fileName = "AppInstallerTestExeInstaller.exe"; + + var result = TestCommon.RunAICLICommand("install", $"{packageId}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true, TestCommon.Scope.User); + } + [Test] public void InstallZipWithInvalidRelativeFilePath() { diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Portable.2.0.0.0.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Portable.2.0.0.0.yaml new file mode 100644 index 0000000000..5acafad386 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Portable.2.0.0.0.yaml @@ -0,0 +1,18 @@ +PackageIdentifier: AppInstallerTest.TestZipInstallerWithPortable +PackageVersion: 2.0.0.0 +PackageName: TestZipInstallerWithPortable +PackageLocale: en-US +Publisher: AppInstallerTest +License: Test +ShortDescription: E2E test for installing a zip with portable. +Installers: + - Architecture: x64 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestZipInstaller/AppInstallerTestZipInstaller.zip + InstallerType: zip + InstallerSha256: + NestedInstallerType: portable + NestedInstallerFiles: + - RelativeFilePath: AppInstallerTestExeInstaller.exe + PortableCommandAlias: TestPortable +ManifestType: singleton +ManifestVersion: 1.4.0 \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Portable.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Portable.yaml new file mode 100644 index 0000000000..8f7b0dae36 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_Portable.yaml @@ -0,0 +1,18 @@ +PackageIdentifier: AppInstallerTest.TestZipInstallerWithPortable +PackageVersion: 1.0.0.0 +PackageName: TestZipInstallerWithPortable +PackageLocale: en-US +Publisher: AppInstallerTest +License: Test +ShortDescription: E2E test for installing a zip with portable. +Installers: + - Architecture: x64 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestZipInstaller/AppInstallerTestZipInstaller.zip + InstallerType: zip + InstallerSha256: + NestedInstallerType: portable + NestedInstallerFiles: + - RelativeFilePath: AppInstallerTestExeInstaller.exe + PortableCommandAlias: TestPortable +ManifestType: singleton +ManifestVersion: 1.4.0 \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/UninstallCommand.cs b/src/AppInstallerCLIE2ETests/UninstallCommand.cs index 9be0cb8943..16e04d2ec8 100644 --- a/src/AppInstallerCLIE2ETests/UninstallCommand.cs +++ b/src/AppInstallerCLIE2ETests/UninstallCommand.cs @@ -4,8 +4,8 @@ namespace AppInstallerCLIE2ETests { using NUnit.Framework; - using System.IO; - + using System.IO; + public class UninstallCommand : BaseCommand { // Custom product code for overriding the default in the test exe @@ -81,7 +81,7 @@ public void UninstallPortable() public void UninstallPortableWithProductCode() { // Uninstall a Portable with ProductCode - string installDir = Path.Combine(System.Environment.GetEnvironmentVariable("LocalAppData"), "Microsoft", "WinGet", "Packages"); + string installDir = TestCommon.GetPortablePackagesDirectory(); string packageId, commandAlias, fileName, packageDirName, productCode; packageId = "AppInstallerTest.TestPortableExe"; packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier; @@ -97,13 +97,15 @@ public void UninstallPortableWithProductCode() [Test] public void UninstallPortableModifiedSymlink() { - string packageId, commandAlias; + string installDir = TestCommon.GetPortablePackagesDirectory(); + string packageId, commandAlias, fileName, packageDirName, productCode; packageId = "AppInstallerTest.TestPortableExe"; - commandAlias = "AppInstallerTestExeInstaller.exe"; + packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier; + commandAlias = fileName = "AppInstallerTestExeInstaller.exe"; TestCommon.RunAICLICommand("install", $"{packageId}"); - string symlinkDirectory = Path.Combine(System.Environment.GetEnvironmentVariable("LocalAppData"), "Microsoft", "WinGet", "Links"); + string symlinkDirectory = TestCommon.GetPortableSymlinkDirectory(TestCommon.Scope.User); string symlinkPath = Path.Combine(symlinkDirectory, commandAlias); // Replace symlink with modified symlink @@ -115,12 +117,37 @@ public void UninstallPortableModifiedSymlink() bool modifiedSymlinkExists = modifiedSymlinkInfo.Exists; modifiedSymlinkInfo.Delete(); + Assert.AreEqual(Constants.ErrorCode.ERROR_PORTABLE_UNINSTALL_FAILED, result.ExitCode); + Assert.True(result.StdOut.Contains("Unable to remove Portable package as it has been modified; to override this check use --force")); + Assert.True(modifiedSymlinkExists, "Modified symlink should still exist"); + + // Try again with --force + var result2 = TestCommon.RunAICLICommand("uninstall", $"{packageId} --force"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result2.ExitCode); + Assert.True(result.StdOut.Contains("Portable package has been modified; proceeding due to --force")); + Assert.True(result.StdOut.Contains("Successfully uninstalled")); + + TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, false); + } + + [Test] + public void UninstallZip_Portable() + { + string installDir = TestCommon.GetPortablePackagesDirectory(); + string packageId, commandAlias, fileName, packageDirName, productCode; + packageId = "AppInstallerTest.TestZipInstallerWithPortable"; + packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier; + commandAlias = "TestPortable.exe"; + fileName = "AppInstallerTestExeInstaller.exe"; + + TestCommon.RunAICLICommand("install", $"{packageId}"); + var result = TestCommon.RunAICLICommand("uninstall", $"{packageId}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(result.StdOut.Contains("Successfully uninstalled")); - Assert.True(result.StdOut.Contains("Portable symlink not deleted as it was modified and points to a different target exe")); - Assert.True(modifiedSymlinkExists, "Modified symlink should still exist"); + TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, false); } + [Test] public void UninstallNotIndexed() { diff --git a/src/AppInstallerCLIE2ETests/UpgradeCommand.cs b/src/AppInstallerCLIE2ETests/UpgradeCommand.cs index 460d060f1c..293fdc18b0 100644 --- a/src/AppInstallerCLIE2ETests/UpgradeCommand.cs +++ b/src/AppInstallerCLIE2ETests/UpgradeCommand.cs @@ -113,5 +113,25 @@ public void UpgradePortableMachineScope() Assert.True(result2.StdOut.Contains("Successfully installed")); TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true, TestCommon.Scope.Machine); } + + [Test] + public void UpgradeZip_Portable() + { + string installDir = TestCommon.GetPortablePackagesDirectory(); + string packageId, commandAlias, fileName, packageDirName, productCode; + packageId = "AppInstallerTest.TestZipInstallerWithPortable"; + packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier; + commandAlias = "TestPortable.exe"; + fileName = "AppInstallerTestExeInstaller.exe"; + + var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestZipInstallerWithPortable -v 1.0.0.0"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + + var result2 = TestCommon.RunAICLICommand("upgrade", $"{packageId} -v 2.0.0.0"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result2.ExitCode); + Assert.True(result2.StdOut.Contains("Successfully installed")); + TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true, TestCommon.Scope.User); + } } } diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index 34e7ab1217..cc6f314ff9 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -297,7 +297,7 @@ true - + true @@ -633,7 +633,7 @@ true - + true diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters index 393b2b81cd..ca1e86b5db 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -534,7 +534,7 @@ TestData - + TestData @@ -585,7 +585,7 @@ TestData - + TestData diff --git a/src/AppInstallerCLITests/Filesystem.cpp b/src/AppInstallerCLITests/Filesystem.cpp index 88af44fb43..2e851cac06 100644 --- a/src/AppInstallerCLITests/Filesystem.cpp +++ b/src/AppInstallerCLITests/Filesystem.cpp @@ -28,4 +28,32 @@ TEST_CASE("PathEscapesDirectory", "[filesystem]") REQUIRE(PathEscapesBaseDirectory(badPath2, basePath)); REQUIRE_FALSE(PathEscapesBaseDirectory(goodPath, basePath)); REQUIRE_FALSE(PathEscapesBaseDirectory(goodPath2, basePath)); +} + +TEST_CASE("VerifySymlink", "[filesystem]") +{ + TestCommon::TempDirectory tempDirectory("TempDirectory"); + const std::filesystem::path& basePath = tempDirectory.GetPath(); + + std::filesystem::path testFilePath = basePath / "testFile.txt"; + std::filesystem::path symlinkPath = basePath / "symlink.exe"; + + TestCommon::TempFile testFile(testFilePath); + std::ofstream file2(testFile, std::ofstream::out); + file2.close(); + + std::filesystem::create_symlink(testFile.GetPath(), symlinkPath); + + REQUIRE(SymlinkExists(symlinkPath)); + REQUIRE(VerifySymlink(symlinkPath, testFilePath)); + REQUIRE_FALSE(VerifySymlink(symlinkPath, "badPath")); + + std::filesystem::remove(testFilePath); + + // Ensure that symlink existence does not check the target + REQUIRE(SymlinkExists(symlinkPath)); + + std::filesystem::remove(symlinkPath); + + REQUIRE_FALSE(SymlinkExists(symlinkPath)); } \ No newline at end of file diff --git a/src/AppInstallerCLITests/PortableIndex.cpp b/src/AppInstallerCLITests/PortableIndex.cpp index aadc1bf5b9..8f5f69634e 100644 --- a/src/AppInstallerCLITests/PortableIndex.cpp +++ b/src/AppInstallerCLITests/PortableIndex.cpp @@ -114,7 +114,7 @@ TEST_CASE("PortableIndex_AddUpdateRemove", "[portableIndex]") Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadOnly); auto fileFromIndex = Schema::Portable_V1_0::PortableTable::GetPortableFileById(connection, 1); REQUIRE(fileFromIndex.has_value()); - REQUIRE(fileFromIndex->GetFilePath() == "testPortableFile.exe"); + REQUIRE(fileFromIndex->GetFilePath() == portableFile.GetFilePath()); REQUIRE(fileFromIndex->FileType == IPortableIndex::PortableFileType::Symlink); REQUIRE(fileFromIndex->SHA256 == updatedHash); REQUIRE(fileFromIndex->SymlinkTarget == "fakeSymlinkTarget.exe"); @@ -157,7 +157,7 @@ TEST_CASE("PortableIndex_UpdateFile_CaseInsensitive", "[portableIndex]") Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadOnly); auto fileFromIndex = Schema::Portable_V1_0::PortableTable::GetPortableFileById(connection, 1); REQUIRE(fileFromIndex.has_value()); - REQUIRE(fileFromIndex->GetFilePath() == "TESTPORTABLEFILE.exe"); + REQUIRE(fileFromIndex->GetFilePath() == portableFile.GetFilePath()); REQUIRE(fileFromIndex->FileType == IPortableIndex::PortableFileType::Symlink); REQUIRE(fileFromIndex->SHA256 == updatedHash); REQUIRE(fileFromIndex->SymlinkTarget == "fakeSymlinkTarget.exe"); diff --git a/src/AppInstallerCLITests/PortableInstaller.cpp b/src/AppInstallerCLITests/PortableInstaller.cpp index 70a08ea89e..6b1f7f7f47 100644 --- a/src/AppInstallerCLITests/PortableInstaller.cpp +++ b/src/AppInstallerCLITests/PortableInstaller.cpp @@ -12,6 +12,7 @@ #include #include +using namespace std::string_literals; using namespace AppInstaller::CLI::Portable; using namespace AppInstaller::Filesystem; using namespace AppInstaller::Manifest; @@ -207,3 +208,107 @@ TEST_CASE("PortableInstaller_MultipleInstall", "[PortableInstaller]") REQUIRE_FALSE(std::filesystem::exists(indexPath)); REQUIRE_FALSE(std::filesystem::exists(portableInstaller.InstallLocation)); } + +TEST_CASE("PortableInstaller_VerifyFilesFromIndex", "[PortableInstaller]") +{ + // Create installer and set install location to temp directory + // Create portable index and add files + PortableInstaller portableInstaller = PortableInstaller( + ScopeEnum::User, + Architecture::X64, + "testProductCode"); + + TempDirectory tempDirectory = TestCommon::TempDirectory("TempDirectory", true); + const auto& tempDirectoryPath = tempDirectory.GetPath(); + portableInstaller.InstallLocation = tempDirectoryPath; + + std::filesystem::path indexPath = tempDirectoryPath / "portable.db"; + + PortableIndex index = PortableIndex::CreateNew(indexPath.u8string(), Schema::Version::Latest()); + + std::filesystem::path symlinkPath = tempDirectoryPath / "symlink.exe"; + std::filesystem::path exePath = tempDirectoryPath / "testExe.txt"; + + // Create exe and symlink + TestCommon::TempFile testFile(exePath); + std::ofstream file2(testFile, std::ofstream::out); + file2.close(); + + std::filesystem::create_symlink(exePath, symlinkPath); + + // Add files to index + IPortableIndex::PortableFile exeFile = PortableIndex::CreatePortableFileFromPath(exePath); + IPortableIndex::PortableFile symlinkFile = PortableIndex::CreatePortableFileFromPath(symlinkPath); + + index.AddPortableFile(exeFile); + index.AddPortableFile(symlinkFile); + + REQUIRE(portableInstaller.VerifyPortableFilesForUninstall()); + + // Modify symlink target + std::filesystem::remove(symlinkPath); + std::filesystem::create_symlink("badPath", symlinkPath); + + REQUIRE_FALSE(portableInstaller.VerifyPortableFilesForUninstall()); + + std::filesystem::remove(symlinkPath); + + // Modify exe hash in index + exeFile.SHA256 = "2413fb3709b05939f04cf2e92f7d0897fc2596f9ad0b8a9ea855c7bfebaae892"; + index.UpdatePortableFile(exeFile); + + REQUIRE_FALSE(portableInstaller.VerifyPortableFilesForUninstall()); + + std::filesystem::remove(exePath); + + // Files that do not exist should still pass as they have already been removed. + REQUIRE(portableInstaller.VerifyPortableFilesForUninstall()); +} + +TEST_CASE("PortableInstaller_VerifyFiles", "[PortableInstaller]") +{ + PortableInstaller portableInstaller = PortableInstaller( + ScopeEnum::User, + Architecture::X64, + "testProductCode"); + + TempDirectory tempDirectory = TestCommon::TempDirectory("TempDirectory", true); + const auto& tempDirectoryPath = tempDirectory.GetPath(); + portableInstaller.InstallLocation = tempDirectoryPath; + + std::filesystem::path symlinkPath = tempDirectoryPath / "symlink.exe"; + std::filesystem::path exePath = tempDirectoryPath / "testExe.txt"; + + // Create and set test file, symlink, and hash + TestCommon::TempFile testFile(exePath); + std::ofstream file(testFile, std::ofstream::out); + file.close(); + + std::filesystem::create_symlink(exePath, symlinkPath); + + std::ifstream inStream{ exePath, std::ifstream::binary }; + const SHA256::HashBuffer& targetFileHash = SHA256::ComputeHash(inStream); + inStream.close(); + portableInstaller.SHA256 = SHA256::ConvertToString(targetFileHash); + portableInstaller.PortableTargetFullPath = exePath; + portableInstaller.PortableSymlinkFullPath = symlinkPath; + + REQUIRE(portableInstaller.VerifyPortableFilesForUninstall()); + + // Modify symlink target + std::filesystem::remove(symlinkPath); + std::filesystem::create_symlink("badPath", symlinkPath); + + REQUIRE_FALSE(portableInstaller.VerifyPortableFilesForUninstall()); + + // Modify exe hash + std::filesystem::remove(symlinkPath); + portableInstaller.SHA256 = "2413fb3709b05939f04cf2e92f7d0897fc2596f9ad0b8a9ea855c7bfebaae892"; + + REQUIRE_FALSE(portableInstaller.VerifyPortableFilesForUninstall()); + + std::filesystem::remove(exePath); + + // Files that do not exist should still pass as they have already been removed. + REQUIRE(portableInstaller.VerifyPortableFilesForUninstall()); +} \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_ZipWithExe.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_Zip_Exe.yaml similarity index 100% rename from src/AppInstallerCLITests/TestData/InstallFlowTest_ZipWithExe.yaml rename to src/AppInstallerCLITests/TestData/InstallFlowTest_Zip_Exe.yaml diff --git a/src/AppInstallerCLITests/TestData/UpdateFlowTest_ZipWithExe.yaml b/src/AppInstallerCLITests/TestData/UpdateFlowTest_Zip_Exe.yaml similarity index 100% rename from src/AppInstallerCLITests/TestData/UpdateFlowTest_ZipWithExe.yaml rename to src/AppInstallerCLITests/TestData/UpdateFlowTest_Zip_Exe.yaml diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp index 365aa73be5..acd11c2210 100644 --- a/src/AppInstallerCLITests/WorkFlow.cpp +++ b/src/AppInstallerCLITests/WorkFlow.cpp @@ -225,8 +225,8 @@ namespace if (input.empty() || input == "AppInstallerCliTest.TestZipInstaller") { - auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_ZipWithExe.yaml")); - auto manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_ZipWithExe.yaml")); + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Zip_Exe.yaml")); + auto manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Zip_Exe.yaml")); result.Matches.emplace_back( ResultMatch( TestPackage::Make( @@ -1040,7 +1040,7 @@ TEST_CASE("InstallFlowWithNonApplicableArchitecture", "[InstallFlow][workflow]") REQUIRE(!std::filesystem::exists(installResultPath.GetPath())); } -TEST_CASE("InstallFlow_ZipWithExe", "[InstallFlow][workflow]") +TEST_CASE("InstallFlow_Zip_Exe", "[InstallFlow][workflow]") { TestCommon::TempFile installResultPath("TestExeInstalled.txt"); TestCommon::TestUserSettings testSettings; @@ -1052,7 +1052,7 @@ TEST_CASE("InstallFlow_ZipWithExe", "[InstallFlow][workflow]") OverrideForShellExecute(context); OverrideForExtractInstallerFromArchive(context); OverrideForVerifyAndSetNestedInstaller(context); - context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_ZipWithExe.yaml").GetPath().u8string()); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_Zip_Exe.yaml").GetPath().u8string()); InstallCommand install({}); install.Execute(context); diff --git a/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp index c3cc446767..8b766e77ef 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp @@ -493,6 +493,11 @@ namespace AppInstaller::Manifest return (installerType == InstallerTypeEnum::Zip); } + bool IsPortableType(InstallerTypeEnum installerType) + { + return (installerType == InstallerTypeEnum::Portable); + } + bool IsNestedInstallerTypeSupported(InstallerTypeEnum nestedInstallerType) { return ( diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h index 6688ffa5b4..9f96c2a1ca 100644 --- a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h +++ b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h @@ -321,6 +321,9 @@ namespace AppInstaller::Manifest // Gets a value indicating whether the given installer type is an archive. bool IsArchiveType(InstallerTypeEnum installerType); + // Gets a value indicating whether the given installer type is a portable. + bool IsPortableType(InstallerTypeEnum installerType); + // Gets a value indicating whether the given nested installer type is supported. bool IsNestedInstallerTypeSupported(InstallerTypeEnum nestedInstallerType); diff --git a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp index af3691dc37..2bee392801 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp @@ -26,6 +26,33 @@ namespace AppInstaller::Repository::Microsoft return result; } + Schema::IPortableIndex::PortableFile PortableIndex::CreatePortableFileFromPath(const std::filesystem::path& path) + { + Schema::IPortableIndex::PortableFile portableFile; + portableFile.SetFilePath(path); + + if (std::filesystem::is_directory(path)) + { + portableFile.FileType = Schema::IPortableIndex::PortableFileType::Directory; + } + else if (std::filesystem::is_symlink(path)) + { + portableFile.FileType = Schema::IPortableIndex::PortableFileType::Symlink; + portableFile.SymlinkTarget = std::filesystem::read_symlink(path).u8string(); + } + else + { + portableFile.FileType = Schema::IPortableIndex::PortableFileType::File; + + std::ifstream inStream{ path, std::ifstream::binary }; + const Utility::SHA256::HashBuffer& targetFileHash = Utility::SHA256::ComputeHash(inStream); + inStream.close(); + portableFile.SHA256 = Utility::SHA256::ConvertToString(targetFileHash); + } + + return portableFile; + } + PortableIndex::IdType PortableIndex::AddPortableFile(const Schema::IPortableIndex::PortableFile& file) { std::lock_guard lockInterface{ *m_interfaceLock }; diff --git a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h index 30bd0151cc..50fe2c27ae 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h @@ -29,6 +29,8 @@ namespace AppInstaller::Repository::Microsoft return { filePath, disposition, std::move(indexFile) }; } + static Schema::IPortableIndex::PortableFile CreatePortableFileFromPath(const std::filesystem::path& path); + IdType AddPortableFile(const Schema::IPortableIndex::PortableFile& file); void RemovePortableFile(const Schema::IPortableIndex::PortableFile& file); From 06d3d00657bb48052186196d96169f3c4f926baf Mon Sep 17 00:00:00 2001 From: Ryan Fu Date: Tue, 6 Sep 2022 18:07:25 -0700 Subject: [PATCH 07/22] fix unit tests --- src/AppInstallerCLITests/WorkFlow.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp index acd11c2210..a659a781ab 100644 --- a/src/AppInstallerCLITests/WorkFlow.cpp +++ b/src/AppInstallerCLITests/WorkFlow.cpp @@ -1079,7 +1079,7 @@ TEST_CASE("InstallFlow_Zip_BadRelativePath", "[InstallFlow][workflow]") auto previousThreadGlobals = context.SetForCurrentThread(); OverrideForShellExecute(context); OverrideForExtractInstallerFromArchive(context); - context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_ZipWithExe.yaml").GetPath().u8string()); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_Zip_Exe.yaml").GetPath().u8string()); InstallCommand install({}); install.Execute(context); @@ -1163,7 +1163,7 @@ 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")); + 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. @@ -1716,7 +1716,7 @@ TEST_CASE("ShowFlow_NestedInstallerType", "[ShowFlow][workflow]") std::ostringstream showOutput; TestContext context{ showOutput, std::cin }; auto previousThreadGlobals = context.SetForCurrentThread(); - context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_ZipWithExe.yaml").GetPath().u8string()); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_Zip_Exe.yaml").GetPath().u8string()); ShowCommand show({}); show.Execute(context); @@ -1930,7 +1930,7 @@ TEST_CASE("UpdateFlow_UpdateExe", "[UpdateFlow][workflow]") REQUIRE(updateResultStr.find("/ver3.0.0.0") != std::string::npos); } -TEST_CASE("UpdateFlow_UpdateZipWithExe", "[UpdateFlow][workflow]") +TEST_CASE("UpdateFlow_UpdateZip_Exe", "[UpdateFlow][workflow]") { TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); From e857f8421334694be64c2118c1ec36ff3ceb0052 Mon Sep 17 00:00:00 2001 From: Ryan Fu Date: Wed, 7 Sep 2022 16:30:39 -0700 Subject: [PATCH 08/22] fix hashing --- src/AppInstallerCLICore/PortableInstaller.cpp | 53 ++++++++----------- src/AppInstallerCLICore/PortableInstaller.h | 8 +-- .../Workflows/PortableFlow.cpp | 6 +-- src/AppInstallerCLITests/Archive.cpp | 2 +- .../PortableInstaller.cpp | 12 ++--- src/AppInstallerCommonCore/Archive.cpp | 3 +- .../Public/AppInstallerSHA256.h | 3 ++ src/AppInstallerCommonCore/SHA256.cpp | 9 ++++ .../Microsoft/PortableIndex.cpp | 6 +-- 9 files changed, 50 insertions(+), 52 deletions(-) diff --git a/src/AppInstallerCLICore/PortableInstaller.cpp b/src/AppInstallerCLICore/PortableInstaller.cpp index 55a107f2be..fff93e8468 100644 --- a/src/AppInstallerCLICore/PortableInstaller.cpp +++ b/src/AppInstallerCLICore/PortableInstaller.cpp @@ -26,15 +26,6 @@ namespace AppInstaller::CLI::Portable { constexpr std::wstring_view c_PortableIndexFileName = L"portable.db"; - bool VerifyPortableExeHash(const std::filesystem::path& targetPath, const std::string& hashValue) - { - std::ifstream inStream{ targetPath, std::ifstream::binary }; - const Utility::SHA256::HashBuffer& targetFileHash = Utility::SHA256::ComputeHash(inStream); - inStream.close(); - - return Utility::SHA256::AreEqual(Utility::SHA256::ConvertToBytes(hashValue), targetFileHash); - } - void RemoveItemsFromIndex(const std::filesystem::path& indexPath) { bool deleteIndex = false; @@ -113,7 +104,7 @@ namespace AppInstaller::CLI::Portable if (fileType == IPortableIndex::PortableFileType::File) { - if (std::filesystem::exists(filePath) && !VerifyPortableExeHash(filePath, file.SHA256)) + if (std::filesystem::exists(filePath) && !SHA256::AreEqual(SHA256::ComputeHashFromFile(filePath), SHA256::ConvertToBytes(file.SHA256))) { return false; } @@ -131,7 +122,7 @@ namespace AppInstaller::CLI::Portable } else { - if (std::filesystem::exists(PortableTargetFullPath) && !VerifyPortableExeHash(PortableTargetFullPath, SHA256)) + if (std::filesystem::exists(PortableTargetFullPath) && !SHA256::AreEqual(SHA256::ComputeHashFromFile(PortableTargetFullPath), SHA256::ConvertToBytes(SHA256))) { return false; } @@ -146,7 +137,7 @@ namespace AppInstaller::CLI::Portable } } - HRESULT PortableInstaller::SingleInstall(const std::filesystem::path& installerPath) + HRESULT PortableInstaller::InstallSingle(const std::filesystem::path& installerPath) { InitializeRegistryEntry(); @@ -155,7 +146,7 @@ namespace AppInstaller::CLI::Portable if (!InstallDirectoryAddedToPath) { const std::filesystem::path& symlinkPath = PortableSymlinkFullPath; - Commit(PortableValueName::PortableSymlinkFullPath, symlinkPath); + CommitToARPEntry(PortableValueName::PortableSymlinkFullPath, symlinkPath); CreatePortableSymlink(PortableTargetFullPath, PortableSymlinkFullPath); } else @@ -170,7 +161,7 @@ namespace AppInstaller::CLI::Portable return ERROR_SUCCESS; } - HRESULT PortableInstaller::MultipleInstall(const std::vector& nestedInstallerFiles, const std::vector& extractedItems) + HRESULT PortableInstaller::InstallMultiple(const std::vector& nestedInstallerFiles, const std::vector& extractedItems) { InitializeRegistryEntry(); @@ -246,8 +237,8 @@ namespace AppInstaller::CLI::Portable RemoveFromPathVariable(); - AICLI_LOG(CLI, Info, << "PortableARPEntry deleted."); m_portableARPEntry.Delete(); + AICLI_LOG(CLI, Info, << "PortableARPEntry deleted."); return ERROR_SUCCESS; } @@ -261,8 +252,8 @@ namespace AppInstaller::CLI::Portable RemoveFromPathVariable(); - AICLI_LOG(CLI, Info, << "PortableARPEntry deleted."); m_portableARPEntry.Delete(); + AICLI_LOG(CLI, Info, << "PortableARPEntry deleted."); return ERROR_SUCCESS; } @@ -285,7 +276,7 @@ namespace AppInstaller::CLI::Portable AICLI_LOG(Core, Info, << "Portable exe moved to: " << PortableTargetFullPath); - Commit(PortableValueName::PortableTargetFullPath, PortableTargetFullPath); + CommitToARPEntry(PortableValueName::PortableTargetFullPath, PortableTargetFullPath); Filesystem::RenameFile(installerPath, PortableTargetFullPath); } @@ -315,7 +306,7 @@ namespace AppInstaller::CLI::Portable // Symlink creation should only fail if the user executes without admin rights or developer mode. // Resort to adding install directory to PATH directly. AICLI_LOG(Core, Info, << "Portable install executed in user mode. Adding package directory to PATH."); - Commit(PortableValueName::InstallDirectoryAddedToPath, InstallDirectoryAddedToPath = true); + CommitToARPEntry(PortableValueName::InstallDirectoryAddedToPath, InstallDirectoryAddedToPath = true); return false; } } @@ -413,7 +404,7 @@ namespace AppInstaller::CLI::Portable PortableInstaller::PortableInstaller(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode) : m_portableARPEntry(PortableARPEntry(scope, arch, productCode)) { - if (Exists()) + if (ARPEntryExists()) { DisplayName = GetStringValue(PortableValueName::DisplayName); DisplayVersion = GetStringValue(PortableValueName::DisplayVersion); @@ -436,22 +427,22 @@ namespace AppInstaller::CLI::Portable void PortableInstaller::InitializeRegistryEntry() { - Commit(PortableValueName::WinGetPackageIdentifier, WinGetPackageIdentifier); - Commit(PortableValueName::WinGetSourceIdentifier, WinGetSourceIdentifier); - Commit(PortableValueName::UninstallString, "winget uninstall --product-code " + GetProductCode()); - Commit(PortableValueName::WinGetInstallerType, InstallerTypeToString(Manifest::InstallerTypeEnum::Portable)); - Commit(PortableValueName::SHA256, SHA256); - Commit(PortableValueName::InstallLocation, InstallLocation); + CommitToARPEntry(PortableValueName::WinGetPackageIdentifier, WinGetPackageIdentifier); + CommitToARPEntry(PortableValueName::WinGetSourceIdentifier, WinGetSourceIdentifier); + CommitToARPEntry(PortableValueName::UninstallString, "winget uninstall --product-code " + GetProductCode()); + CommitToARPEntry(PortableValueName::WinGetInstallerType, InstallerTypeToString(Manifest::InstallerTypeEnum::Portable)); + CommitToARPEntry(PortableValueName::SHA256, SHA256); + CommitToARPEntry(PortableValueName::InstallLocation, InstallLocation); } void PortableInstaller::FinalizeRegistryEntry() { - Commit(PortableValueName::DisplayName, DisplayName); - Commit(PortableValueName::DisplayVersion, DisplayVersion); - Commit(PortableValueName::Publisher, Publisher); - Commit(PortableValueName::InstallDate, InstallDate); - Commit(PortableValueName::URLInfoAbout, URLInfoAbout); - Commit(PortableValueName::HelpLink, HelpLink); + CommitToARPEntry(PortableValueName::DisplayName, DisplayName); + CommitToARPEntry(PortableValueName::DisplayVersion, DisplayVersion); + CommitToARPEntry(PortableValueName::Publisher, Publisher); + CommitToARPEntry(PortableValueName::InstallDate, InstallDate); + CommitToARPEntry(PortableValueName::URLInfoAbout, URLInfoAbout); + CommitToARPEntry(PortableValueName::HelpLink, HelpLink); } std::filesystem::path PortableInstaller::GetPortableIndexPath() diff --git a/src/AppInstallerCLICore/PortableInstaller.h b/src/AppInstallerCLICore/PortableInstaller.h index 2400486fe7..e9c7c32e29 100644 --- a/src/AppInstallerCLICore/PortableInstaller.h +++ b/src/AppInstallerCLICore/PortableInstaller.h @@ -36,9 +36,9 @@ namespace AppInstaller::CLI::Portable PortableInstaller(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode); - HRESULT SingleInstall(const std::filesystem::path& installerPath); + HRESULT InstallSingle(const std::filesystem::path& installerPath); - HRESULT MultipleInstall( + HRESULT InstallMultiple( const std::vector& nestedInstallerFiles, const std::vector& extractedItems); @@ -55,7 +55,7 @@ namespace AppInstaller::CLI::Portable } template - void Commit(PortableValueName valueName, T value) + void CommitToARPEntry(PortableValueName valueName, T value) { m_portableARPEntry.SetValue(valueName, value); } @@ -75,7 +75,7 @@ namespace AppInstaller::CLI::Portable std::string GetProductCode() { return m_portableARPEntry.GetProductCode(); }; - bool Exists() { return m_portableARPEntry.Exists(); }; + bool ARPEntryExists() { return m_portableARPEntry.Exists(); }; std::string GetOutputMessage() { diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp index 0d9b188942..940e0daa7c 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -162,7 +162,7 @@ namespace AppInstaller::CLI::Workflow } PortableInstaller& portableInstaller = context.Get(); - if (portableInstaller.Exists()) + if (portableInstaller.ARPEntryExists()) { if (packageIdentifier != portableInstaller.WinGetPackageIdentifier || sourceIdentifier != portableInstaller.WinGetSourceIdentifier) { @@ -234,12 +234,12 @@ namespace AppInstaller::CLI::Workflow { const std::vector& extractedItems = context.Get(); const std::vector& nestedInstallerFiles = context.Get()->NestedInstallerFiles; - result = portableInstaller.MultipleInstall(nestedInstallerFiles, extractedItems); + result = portableInstaller.InstallMultiple(nestedInstallerFiles, extractedItems); } else { const std::filesystem::path& installerPath = context.Get(); - result = portableInstaller.SingleInstall(installerPath); + result = portableInstaller.InstallSingle(installerPath); } context.Add(result); diff --git a/src/AppInstallerCLITests/Archive.cpp b/src/AppInstallerCLITests/Archive.cpp index e1ac99688b..029fcc5b95 100644 --- a/src/AppInstallerCLITests/Archive.cpp +++ b/src/AppInstallerCLITests/Archive.cpp @@ -22,5 +22,5 @@ TEST_CASE("Extract_ZipArchive", "[archive]") REQUIRE(SUCCEEDED(hr)); REQUIRE(std::filesystem::exists(tempDirectoryPath / "test.txt")); - REQUIRE(extractedItems[0] == "test.txt"); + REQUIRE(extractedItems.size() == 1); } \ No newline at end of file diff --git a/src/AppInstallerCLITests/PortableInstaller.cpp b/src/AppInstallerCLITests/PortableInstaller.cpp index 6b1f7f7f47..5a1b0b4aa5 100644 --- a/src/AppInstallerCLITests/PortableInstaller.cpp +++ b/src/AppInstallerCLITests/PortableInstaller.cpp @@ -118,7 +118,7 @@ TEST_CASE("PortableInstaller_SingleInstall", "[PortableInstaller]") std::ofstream file2(testPortable, std::ofstream::out); file2.close(); - HRESULT installResult = portableInstaller.SingleInstall(testPortable.GetPath()); + HRESULT installResult = portableInstaller.InstallSingle(testPortable.GetPath()); REQUIRE(SUCCEEDED(installResult)); } @@ -129,7 +129,7 @@ TEST_CASE("PortableInstaller_SingleInstall", "[PortableInstaller]") REQUIRE(std::filesystem::exists(portableInstaller.PortableTargetFullPath)); REQUIRE(SymlinkExists(portableInstaller.PortableSymlinkFullPath)); REQUIRE(VerifySymlink(portableInstaller.PortableSymlinkFullPath, portableInstaller.PortableTargetFullPath)); - REQUIRE(portableInstaller.Exists()); + REQUIRE(portableInstaller.ARPEntryExists()); HRESULT uninstallResult = portableInstaller.Uninstall(); @@ -153,9 +153,9 @@ TEST_CASE("PortableInstaller_InstallDirectoryAddedToPath", "[PortableInstaller]" // This value is only set to true if we fail to create a symlink. // If true no symlink should be created and InstallDirectory is added to PATH variable. - portableInstaller.Commit(PortableValueName::InstallDirectoryAddedToPath, portableInstaller.InstallDirectoryAddedToPath = true); + portableInstaller.CommitToARPEntry(PortableValueName::InstallDirectoryAddedToPath, portableInstaller.InstallDirectoryAddedToPath = true); - HRESULT installResult = portableInstaller.SingleInstall(testPortable.GetPath()); + HRESULT installResult = portableInstaller.InstallSingle(testPortable.GetPath()); REQUIRE(SUCCEEDED(installResult)); } @@ -165,7 +165,7 @@ TEST_CASE("PortableInstaller_InstallDirectoryAddedToPath", "[PortableInstaller]" REQUIRE(std::filesystem::exists(portableInstaller.PortableTargetFullPath)); REQUIRE_FALSE(SymlinkExists(portableInstaller.PortableSymlinkFullPath)); REQUIRE(PathVariable(ScopeEnum::User).Contains(portableInstaller.InstallLocation)); - REQUIRE(portableInstaller.Exists()); + REQUIRE(portableInstaller.ARPEntryExists()); HRESULT uninstallResult = portableInstaller.Uninstall(); @@ -191,7 +191,7 @@ TEST_CASE("PortableInstaller_MultipleInstall", "[PortableInstaller]") std::vector nestedInstallerFiles = CreateTestNestedInstallerFiles(); std::vector extractedItems = CreateExtractedItemsFromArchive(tempDirectoryPath); - HRESULT installResult = portableInstaller.MultipleInstall(nestedInstallerFiles, extractedItems); + HRESULT installResult = portableInstaller.InstallMultiple(nestedInstallerFiles, extractedItems); REQUIRE(SUCCEEDED(installResult)); const auto& indexPath = portableInstaller.GetPortableIndexPath(); diff --git a/src/AppInstallerCommonCore/Archive.cpp b/src/AppInstallerCommonCore/Archive.cpp index f8c122842e..ac125c13a5 100644 --- a/src/AppInstallerCommonCore/Archive.cpp +++ b/src/AppInstallerCommonCore/Archive.cpp @@ -36,8 +36,7 @@ namespace AppInstaller::Archive 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)); - // Append the full path location of the extracted item - extractedItems.emplace_back(szFolderName); + extractedItems.emplace_back(destPath / szFolderName); } RETURN_IF_FAILED(pFileOperation->PerformOperations()); diff --git a/src/AppInstallerCommonCore/Public/AppInstallerSHA256.h b/src/AppInstallerCommonCore/Public/AppInstallerSHA256.h index 0f6cd4dae3..959231abe2 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerSHA256.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerSHA256.h @@ -53,6 +53,9 @@ namespace AppInstaller::Utility { // Computes the hash from a given stream. static HashBuffer ComputeHash(std::istream& in); + // Computes the hash from a given file path. + static HashBuffer ComputeHashFromFile(const std::filesystem::path& path); + static std::string ConvertToString(const HashBuffer& hashBuffer); static std::wstring ConvertToWideString(const HashBuffer& hashBuffer); diff --git a/src/AppInstallerCommonCore/SHA256.cpp b/src/AppInstallerCommonCore/SHA256.cpp index 9aa0d68eda..adcf1441c1 100644 --- a/src/AppInstallerCommonCore/SHA256.cpp +++ b/src/AppInstallerCommonCore/SHA256.cpp @@ -148,6 +148,15 @@ namespace AppInstaller::Utility { } } + + SHA256::HashBuffer SHA256::ComputeHashFromFile(const std::filesystem::path& path) + { + std::ifstream inStream{ path, std::ifstream::binary }; + const Utility::SHA256::HashBuffer& targetFileHash = Utility::SHA256::ComputeHash(inStream); + inStream.close(); + return targetFileHash; + } + void SHA256::SHA256ContextDeleter::operator()(SHA256Context* context) { delete context; diff --git a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp index 2bee392801..8b4a17be17 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp @@ -43,11 +43,7 @@ namespace AppInstaller::Repository::Microsoft else { portableFile.FileType = Schema::IPortableIndex::PortableFileType::File; - - std::ifstream inStream{ path, std::ifstream::binary }; - const Utility::SHA256::HashBuffer& targetFileHash = Utility::SHA256::ComputeHash(inStream); - inStream.close(); - portableFile.SHA256 = Utility::SHA256::ConvertToString(targetFileHash); + portableFile.SHA256 = Utility::SHA256::ConvertToString(Utility::SHA256::ComputeHashFromFile(path)); } return portableFile; From cdbb8c0d56985912d5778b7713352129688c804a Mon Sep 17 00:00:00 2001 From: Ryan Fu Date: Thu, 8 Sep 2022 11:10:16 -0700 Subject: [PATCH 09/22] fix E2E tests --- src/AppInstallerCLICore/PortableInstaller.cpp | 20 ++++++++++--------- .../Workflows/PortableFlow.cpp | 3 +-- src/AppInstallerCLIE2ETests/InstallCommand.cs | 16 ++++++--------- .../UninstallCommand.cs | 14 +++++-------- 4 files changed, 23 insertions(+), 30 deletions(-) diff --git a/src/AppInstallerCLICore/PortableInstaller.cpp b/src/AppInstallerCLICore/PortableInstaller.cpp index fff93e8468..819d0d114d 100644 --- a/src/AppInstallerCLICore/PortableInstaller.cpp +++ b/src/AppInstallerCLICore/PortableInstaller.cpp @@ -37,8 +37,9 @@ namespace AppInstaller::CLI::Portable { const auto& filePath = file.GetFilePath(); - if (std::filesystem::exists(filePath)) + if (std::filesystem::exists(filePath) || Filesystem::SymlinkExists(filePath)) { + std::cout << filePath << std::endl; std::filesystem::remove_all(filePath); } @@ -147,6 +148,14 @@ namespace AppInstaller::CLI::Portable { const std::filesystem::path& symlinkPath = PortableSymlinkFullPath; CommitToARPEntry(PortableValueName::PortableSymlinkFullPath, symlinkPath); + + std::filesystem::file_status status = std::filesystem::status(symlinkPath); + if (std::filesystem::is_directory(status)) + { + AICLI_LOG(CLI, Info, << "Unable to create symlink. '" << symlinkPath << "points to an existing directory."); + return APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY; + } + CreatePortableSymlink(PortableTargetFullPath, PortableSymlinkFullPath); } else @@ -230,7 +239,7 @@ namespace AppInstaller::CLI::Portable RemoveInstallDirectory(purge); - if (!std::filesystem::remove(PortableSymlinkFullPath)) + if (Filesystem::SymlinkExists(PortableSymlinkFullPath) && !std::filesystem::remove(PortableSymlinkFullPath)) { AICLI_LOG(CLI, Info, << "Portable symlink not found; Unable to delete portable symlink: " << PortableSymlinkFullPath); } @@ -283,13 +292,6 @@ namespace AppInstaller::CLI::Portable bool PortableInstaller::CreatePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath) { - std::filesystem::file_status status = std::filesystem::status(symlinkPath); - if (std::filesystem::is_directory(status)) - { - AICLI_LOG(CLI, Info, << "Unable to create symlink. '" << symlinkPath << "points to an existing directory."); - throw APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY; - } - if (std::filesystem::remove(symlinkPath)) { AICLI_LOG(CLI, Info, << "Removed existing file at " << symlinkPath); diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp index 940e0daa7c..aa204f60c7 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -253,8 +253,7 @@ namespace AppInstaller::CLI::Workflow if (result != ERROR_SUCCESS && !portableInstaller.IsUpdate) { context.Reporter.Warn() << Resource::String::PortableInstallFailed << std::endl; - result = portableInstaller.Uninstall(); - context.Add(result); + portableInstaller.Uninstall(); } } diff --git a/src/AppInstallerCLIE2ETests/InstallCommand.cs b/src/AppInstallerCLIE2ETests/InstallCommand.cs index f5a4456d77..29460676cb 100644 --- a/src/AppInstallerCLIE2ETests/InstallCommand.cs +++ b/src/AppInstallerCLIE2ETests/InstallCommand.cs @@ -174,14 +174,14 @@ public void InstallPortableExeWithCommand() var installDir = TestCommon.GetRandomTestDir(); string packageId, commandAlias, fileName, productCode; packageId = "AppInstallerTest.TestPortableExeWithCommand"; - productCode = packageId + "_" + Constants.TestSourceIdentifier; + productCode = packageId + "_" + Constants.TestSourceIdentifier; fileName = "AppInstallerTestExeInstaller.exe"; commandAlias = "testCommand.exe"; var result = TestCommon.RunAICLICommand("install", $"{packageId} -l {installDir}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(result.StdOut.Contains("Successfully installed")); - TestCommon.VerifyPortablePackage(installDir, commandAlias, fileName, productCode, true); + TestCommon.VerifyPortablePackage(Path.Combine(installDir, productCode), commandAlias, fileName, productCode, true); } [Test] @@ -196,7 +196,7 @@ public void InstallPortableExeWithRename() var result = TestCommon.RunAICLICommand("install", $"{packageId} -l {installDir} --rename {renameArgValue}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(result.StdOut.Contains("Successfully installed")); - TestCommon.VerifyPortablePackage(installDir, renameArgValue, renameArgValue, productCode, true); + TestCommon.VerifyPortablePackage(Path.Combine(installDir, productCode), renameArgValue, renameArgValue, productCode, true); } [Test] @@ -240,18 +240,15 @@ public void InstallPortableToExistingDirectory() var result = TestCommon.RunAICLICommand("install", $"{packageId} -l {existingDir}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(result.StdOut.Contains("Successfully installed")); - TestCommon.VerifyPortablePackage(existingDir, commandAlias, fileName, productCode, true); + TestCommon.VerifyPortablePackage(Path.Combine(existingDir, productCode), commandAlias, fileName, productCode, true); } [Test] public void InstallPortableFailsWithCleanup() { - string installDir = TestCommon.GetPortablePackagesDirectory(); - string winGetDir = Directory.GetParent(installDir).FullName; - string packageId, commandAlias, fileName, packageDirName, productCode; + string packageId, commandAlias; packageId = "AppInstallerTest.TestPortableExe"; - packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier; - commandAlias = fileName = "AppInstallerTestExeInstaller.exe"; + commandAlias = "AppInstallerTestExeInstaller.exe"; // Create a directory with the same name as the symlink in order to cause install to fail. string symlinkDirectory = TestCommon.GetPortableSymlinkDirectory(TestCommon.Scope.User); @@ -265,7 +262,6 @@ public void InstallPortableFailsWithCleanup() Assert.AreNotEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(result.StdOut.Contains("Unable to create symlink, path points to a directory.")); - TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, false); } [Test] diff --git a/src/AppInstallerCLIE2ETests/UninstallCommand.cs b/src/AppInstallerCLIE2ETests/UninstallCommand.cs index 16e04d2ec8..61a15dacbb 100644 --- a/src/AppInstallerCLIE2ETests/UninstallCommand.cs +++ b/src/AppInstallerCLIE2ETests/UninstallCommand.cs @@ -111,21 +111,17 @@ public void UninstallPortableModifiedSymlink() // Replace symlink with modified symlink File.Delete(symlinkPath); FileSystemInfo modifiedSymlinkInfo = File.CreateSymbolicLink(symlinkPath, "fakeTargetExe"); - var result = TestCommon.RunAICLICommand("uninstall", $"{packageId}"); - - // Remove modified symlink as to not interfere with other tests - bool modifiedSymlinkExists = modifiedSymlinkInfo.Exists; - modifiedSymlinkInfo.Delete(); + var result = TestCommon.RunAICLICommand("uninstall", $"{packageId}"); Assert.AreEqual(Constants.ErrorCode.ERROR_PORTABLE_UNINSTALL_FAILED, result.ExitCode); Assert.True(result.StdOut.Contains("Unable to remove Portable package as it has been modified; to override this check use --force")); - Assert.True(modifiedSymlinkExists, "Modified symlink should still exist"); + Assert.True(modifiedSymlinkInfo.Exists, "Modified symlink should still exist"); // Try again with --force var result2 = TestCommon.RunAICLICommand("uninstall", $"{packageId} --force"); Assert.AreEqual(Constants.ErrorCode.S_OK, result2.ExitCode); - Assert.True(result.StdOut.Contains("Portable package has been modified; proceeding due to --force")); - Assert.True(result.StdOut.Contains("Successfully uninstalled")); + Assert.True(result2.StdOut.Contains("Portable package has been modified; proceeding due to --force")); + Assert.True(result2.StdOut.Contains("Successfully uninstalled")); TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, false); } @@ -140,7 +136,7 @@ public void UninstallZip_Portable() commandAlias = "TestPortable.exe"; fileName = "AppInstallerTestExeInstaller.exe"; - TestCommon.RunAICLICommand("install", $"{packageId}"); + var testreuslt = TestCommon.RunAICLICommand("install", $"{packageId}"); var result = TestCommon.RunAICLICommand("uninstall", $"{packageId}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(result.StdOut.Contains("Successfully uninstalled")); From 2b8d33b5718f8d34d719377a988e73f04c4d88dc Mon Sep 17 00:00:00 2001 From: Ryan Fu Date: Thu, 8 Sep 2022 16:19:57 -0700 Subject: [PATCH 10/22] Additional logging and existence check --- src/AppInstallerCLICore/PortableInstaller.cpp | 12 ++++++++++-- .../Microsoft/PortableIndex.cpp | 3 ++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/AppInstallerCLICore/PortableInstaller.cpp b/src/AppInstallerCLICore/PortableInstaller.cpp index 819d0d114d..8843c9a829 100644 --- a/src/AppInstallerCLICore/PortableInstaller.cpp +++ b/src/AppInstallerCLICore/PortableInstaller.cpp @@ -184,8 +184,16 @@ namespace AppInstaller::CLI::Portable for (auto& item : extractedItems) { const auto& itemPath = InstallLocation / item; - IPortableIndex::PortableFile portableFile = PortableIndex::CreatePortableFileFromPath(itemPath); - portableIndex.AddOrUpdatePortableFile(portableFile); + AICLI_LOG(CLI, Info, << "Adding extracted item to index: " << itemPath); + if (std::filesystem::exists(itemPath)) + { + IPortableIndex::PortableFile portableFile = PortableIndex::CreatePortableFileFromPath(itemPath); + portableIndex.AddOrUpdatePortableFile(portableFile); + } + else + { + AICLI_LOG(CLI, Info, << "Extracted item does not exist: " << itemPath); + } } for (const auto& nestedInstallerFile : nestedInstallerFiles) diff --git a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp index 8b4a17be17..c851c44063 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp @@ -4,6 +4,7 @@ #include "PortableIndex.h" #include "SQLiteStorageBase.h" #include "Schema/Portable_1_0/PortableIndexInterface.h" +#include "winget/Filesystem.h" namespace AppInstaller::Repository::Microsoft { @@ -35,7 +36,7 @@ namespace AppInstaller::Repository::Microsoft { portableFile.FileType = Schema::IPortableIndex::PortableFileType::Directory; } - else if (std::filesystem::is_symlink(path)) + else if (Filesystem::SymlinkExists(path)) { portableFile.FileType = Schema::IPortableIndex::PortableFileType::Symlink; portableFile.SymlinkTarget = std::filesystem::read_symlink(path).u8string(); From 7dbdab48fba3b7abbf43fec1d1e5405e524a9362 Mon Sep 17 00:00:00 2001 From: Ryan Fu Date: Thu, 8 Sep 2022 18:11:55 -0700 Subject: [PATCH 11/22] get parsing path from extracted files --- src/AppInstallerCLICore/PortableInstaller.cpp | 14 +++----------- src/AppInstallerCLITests/Archive.cpp | 4 +++- src/AppInstallerCommonCore/Archive.cpp | 2 +- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/AppInstallerCLICore/PortableInstaller.cpp b/src/AppInstallerCLICore/PortableInstaller.cpp index 8843c9a829..c1c30d0a6e 100644 --- a/src/AppInstallerCLICore/PortableInstaller.cpp +++ b/src/AppInstallerCLICore/PortableInstaller.cpp @@ -183,17 +183,9 @@ namespace AppInstaller::CLI::Portable PortableIndex portableIndex = PortableIndex::Open(indexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); for (auto& item : extractedItems) { - const auto& itemPath = InstallLocation / item; - AICLI_LOG(CLI, Info, << "Adding extracted item to index: " << itemPath); - if (std::filesystem::exists(itemPath)) - { - IPortableIndex::PortableFile portableFile = PortableIndex::CreatePortableFileFromPath(itemPath); - portableIndex.AddOrUpdatePortableFile(portableFile); - } - else - { - AICLI_LOG(CLI, Info, << "Extracted item does not exist: " << itemPath); - } + AICLI_LOG(CLI, Info, << "Adding extracted item to index: " << item); + IPortableIndex::PortableFile portableFile = PortableIndex::CreatePortableFileFromPath(item); + portableIndex.AddOrUpdatePortableFile(portableFile); } for (const auto& nestedInstallerFile : nestedInstallerFiles) diff --git a/src/AppInstallerCLITests/Archive.cpp b/src/AppInstallerCLITests/Archive.cpp index 029fcc5b95..e9f5ea619f 100644 --- a/src/AppInstallerCLITests/Archive.cpp +++ b/src/AppInstallerCLITests/Archive.cpp @@ -20,7 +20,9 @@ TEST_CASE("Extract_ZipArchive", "[archive]") std::vector extractedItems; HRESULT hr = TryExtractArchive(testZipPath, tempDirectoryPath, extractedItems); + std::filesystem::path expectedPath = tempDirectoryPath / "test.txt"; REQUIRE(SUCCEEDED(hr)); - REQUIRE(std::filesystem::exists(tempDirectoryPath / "test.txt")); + REQUIRE(std::filesystem::exists(expectedPath)); REQUIRE(extractedItems.size() == 1); + REQUIRE(extractedItems[0] == expectedPath); } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Archive.cpp b/src/AppInstallerCommonCore/Archive.cpp index ac125c13a5..f262720933 100644 --- a/src/AppInstallerCommonCore/Archive.cpp +++ b/src/AppInstallerCommonCore/Archive.cpp @@ -32,7 +32,7 @@ namespace AppInstaller::Archive wil::com_ptr pShellItemFrom; STRRET strFolderName; WCHAR szFolderName[MAX_PATH]; - RETURN_IF_FAILED(pArchiveShellFolder->GetDisplayNameOf(pidlChild.get(), SHGDN_INFOLDER, &strFolderName)); + RETURN_IF_FAILED(pArchiveShellFolder->GetDisplayNameOf(pidlChild.get(), SHGDN_INFOLDER | SHGDN_FORPARSING, &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)); From e0ebb9d3914eda03a1830d5c1123923cda5efb2e Mon Sep 17 00:00:00 2001 From: Ryan Fu Date: Mon, 12 Sep 2022 13:21:55 -0700 Subject: [PATCH 12/22] fix COM test install dir --- src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs b/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs index 23aa75e842..4f315714a3 100644 --- a/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs +++ b/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs @@ -283,7 +283,7 @@ public async Task InstallPortableExeWithCommand() // Assert Assert.AreEqual(InstallResultStatus.Ok, installResult.Status); - TestCommon.VerifyPortablePackage(installDir, commandAlias, fileName, productCode, true); + TestCommon.VerifyPortablePackage(Path.Combine(installDir, Constants.PortableExePackageDirName), commandAlias, fileName, productCode, true); } [Test] @@ -308,7 +308,7 @@ public async Task InstallPortableToExistingDirectory() // Assert Assert.AreEqual(InstallResultStatus.Ok, installResult.Status); - TestCommon.VerifyPortablePackage(existingDir, commandAlias, fileName, productCode, true); + TestCommon.VerifyPortablePackage(Path.Combine(existingDir, Constants.PortableExePackageDirName), commandAlias, fileName, productCode, true); } [Test] From 8d1af15221fdccd141c78f9843db5b54156733a4 Mon Sep 17 00:00:00 2001 From: Ryan Fu Date: Fri, 16 Sep 2022 12:37:07 -0700 Subject: [PATCH 13/22] initial refactor for portable state --- src/AppInstallerCLICore/PortableInstaller.cpp | 343 ++++++++++----- src/AppInstallerCLICore/PortableInstaller.h | 43 +- .../Workflows/ArchiveFlow.cpp | 46 +- .../Workflows/PortableFlow.cpp | 177 +++++++- src/AppInstallerCLITests/Archive.cpp | 5 +- src/AppInstallerCLITests/PortableIndex.cpp | 24 +- .../PortableInstaller.cpp | 415 +++++++++--------- .../AppInstallerCommonCore.vcxproj | 1 + .../AppInstallerCommonCore.vcxproj.filters | 3 + src/AppInstallerCommonCore/Archive.cpp | 3 +- .../Public/winget/Archive.h | 2 +- .../Public/winget/PortableFileEntry.h | 62 +++ .../Microsoft/PortableIndex.cpp | 35 +- .../Microsoft/PortableIndex.h | 17 +- .../Microsoft/Schema/IPortableIndex.h | 36 +- .../Portable_1_0/PortableIndexInterface.h | 10 +- .../PortableIndexInterface_1_0.cpp | 12 +- .../Schema/Portable_1_0/PortableTable.cpp | 20 +- .../Schema/Portable_1_0/PortableTable.h | 8 +- 19 files changed, 761 insertions(+), 501 deletions(-) create mode 100644 src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h diff --git a/src/AppInstallerCLICore/PortableInstaller.cpp b/src/AppInstallerCLICore/PortableInstaller.cpp index c1c30d0a6e..0974d5c3cf 100644 --- a/src/AppInstallerCLICore/PortableInstaller.cpp +++ b/src/AppInstallerCLICore/PortableInstaller.cpp @@ -31,7 +31,7 @@ namespace AppInstaller::CLI::Portable bool deleteIndex = false; { PortableIndex portableIndex = PortableIndex::Open(indexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); - std::vector portableFiles = portableIndex.GetAllPortableFiles(); + std::vector portableFiles = portableIndex.GetAllPortableFiles(); for (const auto& file : portableFiles) { @@ -91,179 +91,313 @@ namespace AppInstaller::CLI::Portable } } - bool PortableInstaller::VerifyPortableFilesForUninstall() + bool VerifyPortableFile(AppInstaller::Portable::PortableFileEntry& entry) { - const auto& indexPath = GetPortableIndexPath(); - if (std::filesystem::exists(indexPath)) - { - PortableIndex portableIndex = PortableIndex::Open(indexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); - std::vector portableFiles = portableIndex.GetAllPortableFiles(); - for (const auto& file : portableFiles) - { - IPortableIndex::PortableFileType fileType = file.FileType; - const auto& filePath = file.GetFilePath(); + std::filesystem::path filePath = entry.GetFilePath(); + PortableFileType fileType = entry.FileType; - if (fileType == IPortableIndex::PortableFileType::File) - { - if (std::filesystem::exists(filePath) && !SHA256::AreEqual(SHA256::ComputeHashFromFile(filePath), SHA256::ConvertToBytes(file.SHA256))) - { - return false; - } - } - else if (fileType == IPortableIndex::PortableFileType::Symlink) - { - if (Filesystem::SymlinkExists(filePath) && !Filesystem::VerifySymlink(filePath, file.SymlinkTarget)) - { - return false; - } - } - } - - return true; - } - else + if (fileType == PortableFileType::File) { - if (std::filesystem::exists(PortableTargetFullPath) && !SHA256::AreEqual(SHA256::ComputeHashFromFile(PortableTargetFullPath), SHA256::ConvertToBytes(SHA256))) + if (std::filesystem::exists(filePath) && !SHA256::AreEqual(SHA256::ComputeHashFromFile(filePath), SHA256::ConvertToBytes(entry.SHA256))) { return false; } - else if (Filesystem::SymlinkExists(PortableSymlinkFullPath) && !Filesystem::VerifySymlink(PortableSymlinkFullPath, PortableTargetFullPath)) + } + else if (fileType == PortableFileType::Symlink) + { + if (Filesystem::SymlinkExists(filePath) && !Filesystem::VerifySymlink(filePath, entry.SymlinkTarget)) { return false; } - else - { - return true; - } } + + return true; } - HRESULT PortableInstaller::InstallSingle(const std::filesystem::path& installerPath) + void PortableInstaller::InstallFile(AppInstaller::Portable::PortableFileEntry& entry) { - InitializeRegistryEntry(); + PortableFileType fileType = entry.FileType; + std::filesystem::path filePath = entry.GetFilePath(); + + if (entry.FileType == PortableFileType::File) + { + if (std::filesystem::exists(filePath)) + { + AICLI_LOG(Core, Info, << "Removing existing portable exe at: " << filePath); + std::filesystem::remove(filePath); + } + + AICLI_LOG(Core, Info, << "Portable exe moved to: " << filePath); + + if (!RecordToIndex) + { + CommitToARPEntry(PortableValueName::PortableTargetFullPath, filePath); + CommitToARPEntry(PortableValueName::SHA256, entry.SHA256); + } - MovePortableExe(installerPath); + Filesystem::RenameFile(entry.CurrentPath, filePath); + } + else if (fileType == PortableFileType::Directory) + { + if (!RecordToIndex) + { + CommitToARPEntry(PortableValueName::InstallLocation, filePath); + } - if (!InstallDirectoryAddedToPath) + if (std::filesystem::create_directories(filePath)) + { + AICLI_LOG(Core, Info, << "Created target install directory: " << filePath); + } + } + else if (entry.FileType == PortableFileType::Symlink) { - const std::filesystem::path& symlinkPath = PortableSymlinkFullPath; - CommitToARPEntry(PortableValueName::PortableSymlinkFullPath, symlinkPath); + const std::filesystem::path& symlinkPath = entry.SymlinkTarget; std::filesystem::file_status status = std::filesystem::status(symlinkPath); if (std::filesystem::is_directory(status)) { AICLI_LOG(CLI, Info, << "Unable to create symlink. '" << symlinkPath << "points to an existing directory."); - return APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY; + //return APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY; + } + + if (!RecordToIndex) + { + CommitToARPEntry(PortableValueName::PortableSymlinkFullPath, filePath); } - CreatePortableSymlink(PortableTargetFullPath, PortableSymlinkFullPath); + PortableInstaller::CreatePortableSymlink(entry.SymlinkTarget, filePath); } - else + + if (RecordToIndex) { - AICLI_LOG(CLI, Info, << "Package directory was previously added to PATH. Skipping symlink creation."); + std::filesystem::path indexPath = GetPortableIndexPath(); + if (std::filesystem::exists(indexPath)) + { + AppInstaller::Repository::Microsoft::PortableIndex portableIndex = AppInstaller::Repository::Microsoft::PortableIndex::CreateNew(indexPath.u8string()); + portableIndex.AddOrUpdatePortableFile(entry); + } + else + { + AppInstaller::Repository::Microsoft::PortableIndex portableIndex = AppInstaller::Repository::Microsoft::PortableIndex::Open(indexPath.u8string(), AppInstaller::Repository::Microsoft::SQLiteStorageBase::OpenDisposition::ReadWrite); + portableIndex.AddOrUpdatePortableFile(entry); + } } + } - AddToPathVariable(); + void PortableInstaller::RemoveFile(AppInstaller::Portable::PortableFileEntry& desiredState) + { + const auto& filePath = desiredState.GetFilePath(); - FinalizeRegistryEntry(); + if (std::filesystem::exists(filePath) || Filesystem::SymlinkExists(filePath)) + { + if (desiredState.FileType == PortableFileType::Directory && filePath == InstallLocation) + { + if (Purge) + { + m_stream << Resource::String::PurgeInstallDirectory << std::endl; + const auto& removedFilesCount = std::filesystem::remove_all(InstallLocation); + AICLI_LOG(CLI, Info, << "Purged install location directory. Deleted " << removedFilesCount << " files or directories"); + } + else + { + std::filesystem::path indexPath = GetPortableIndexPath(); + bool isDirectoryEmpty = true; + for (const auto& item : std::filesystem::directory_iterator(filePath)) + { + if (item.path() != indexPath) + { + isDirectoryEmpty = false; + } + } - return ERROR_SUCCESS; + if (isDirectoryEmpty) + { + AICLI_LOG(CLI, Info, << "Removing empty install directory: " << filePath); + std::filesystem::remove(filePath); + } + else + { + AICLI_LOG(CLI, Info, << "Unable to remove install directory as there are remaining files in: " << filePath); + m_stream << Resource::String::FilesRemainInInstallDirectory << filePath << std::endl; + } + } + } + else + { + std::filesystem::remove_all(filePath); + + if (RecordToIndex) + { + std::filesystem::path indexPath = GetPortableIndexPath(); + PortableIndex index = PortableIndex::Open(indexPath.u8string(), AppInstaller::Repository::Microsoft::SQLiteStorageBase::OpenDisposition::Read); + index.RemovePortableFile(desiredState); +; } + } + } } - HRESULT PortableInstaller::InstallMultiple(const std::vector& nestedInstallerFiles, const std::vector& extractedItems) + void PortableInstaller::ResolutionEngine(std::vector& desiredState, std::vector& expectedState) { - InitializeRegistryEntry(); - - const auto& indexPath = InstallLocation / c_PortableIndexFileName; - if (!std::filesystem::exists(indexPath)) + // Remove all entries from expected state first, then perform install. If this is an uninstall, desiredState will be empty + for (auto entry : expectedState) { - PortableIndex::CreateNew(indexPath.u8string(), Schema::Version::Latest()); + RemoveFile(entry); } - PortableIndex portableIndex = PortableIndex::Open(indexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); - for (auto& item : extractedItems) + for (auto entry : desiredState) { - AICLI_LOG(CLI, Info, << "Adding extracted item to index: " << item); - IPortableIndex::PortableFile portableFile = PortableIndex::CreatePortableFileFromPath(item); - portableIndex.AddOrUpdatePortableFile(portableFile); + // for each one, install and record + InstallFile(entry); } + } - for (const auto& nestedInstallerFile : nestedInstallerFiles) + bool PortableInstaller::VerifyResolution(std::vector& expectedState) + { + for (auto entry : expectedState) { - const std::filesystem::path& portableTargetPath = InstallLocation / ConvertToUTF16(nestedInstallerFile.RelativeFilePath); - if (!InstallDirectoryAddedToPath) + if (!VerifyPortableFile(entry)) { - std::filesystem::path commandAlias; - if (!nestedInstallerFile.PortableCommandAlias.empty()) - { - commandAlias = ConvertToUTF16(nestedInstallerFile.PortableCommandAlias); - } - else - { - commandAlias = portableTargetPath.filename(); - } + AICLI_LOG(CLI, Info, << "Portable file has been modified: " << entry.GetFilePath()); + return false; + } + } - Filesystem::AppendExtension(commandAlias, ".exe"); - const std::filesystem::path& symlinkFullPath = GetPortableLinksLocation(GetScope()) / commandAlias; - if (CreatePortableSymlink(portableTargetPath, symlinkFullPath)) - { - IPortableIndex::PortableFile symlinkPortableFile = PortableIndex::CreatePortableFileFromPath(symlinkFullPath); - portableIndex.AddOrUpdatePortableFile(symlinkPortableFile); - } + return true; + } + + // get desired state and expected state from context + // + + std::vector PortableInstaller::GetDesiredState(std::vector files) + { + std::vector entries; + + for (const auto file : files) + { + AppInstaller::Portable::PortableFileEntry portableFile; + + if (std::filesystem::is_directory(file)) + { + portableFile.SetFilePath(file); + portableFile.FileType = PortableFileType::Directory; } else { - AICLI_LOG(CLI, Info, << "Package directory was previously added to PATH. Skipping symlink creation."); - break; + std::filesystem::path relativePath = std::filesystem::relative(file, file.parent_path()); + portableFile.CurrentPath = file; + portableFile.SetFilePath(InstallLocation / relativePath); + portableFile.FileType = PortableFileType::File; + portableFile.SHA256 = Utility::SHA256::ConvertToString(Utility::SHA256::ComputeHashFromFile(file)); + } + + entries.emplace_back(std::move(portableFile)); + + if (portableFile.FileType == PortableFileType::File) + { + AppInstaller::Portable::PortableFileEntry symlinkEntry; + symlinkEntry.SetFilePath(PortableSymlinkFullPath); + symlinkEntry.SymlinkTarget = PortableTargetFullPath.u8string(); + symlinkEntry.FileType = PortableFileType::Symlink; + entries.emplace_back(symlinkEntry); } } - AddToPathVariable(); - - FinalizeRegistryEntry(); + return entries; + } - return ERROR_SUCCESS; + std::vector PortableInstaller::GetExpectedState() + { + std::vector entries; + const auto& indexPath = GetPortableIndexPath(); + if (std::filesystem::exists(indexPath)) + { + PortableIndex portableIndex = PortableIndex::Open(indexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); + entries = portableIndex.GetAllPortableFiles(); + } + else + { + if (!PortableSymlinkFullPath.empty()) + { + AppInstaller::Portable::PortableFileEntry symlinkEntry; + symlinkEntry.SetFilePath(PortableSymlinkFullPath); + symlinkEntry.SymlinkTarget = PortableTargetFullPath.u8string(); + symlinkEntry.FileType = PortableFileType::Symlink; + entries.emplace_back(symlinkEntry); + } + + if (!PortableTargetFullPath.empty()) + { + AppInstaller::Portable::PortableFileEntry exeEntry; + exeEntry.SetFilePath(PortableTargetFullPath); + exeEntry.SHA256 = SHA256; + exeEntry.FileType = PortableFileType::File; + entries.emplace_back(exeEntry); + } + + if (!InstallLocation.empty() && InstallDirectoryCreated) + { + AppInstaller::Portable::PortableFileEntry directoryEntry; + directoryEntry.SetFilePath(InstallLocation); + directoryEntry.FileType = PortableFileType::Directory; + entries.emplace_back(directoryEntry); + } + } + + return entries; } - HRESULT PortableInstaller::UninstallSingle(bool purge) + HRESULT PortableInstaller::Install(const std::filesystem::path& installerPath) { - if (std::filesystem::exists(PortableTargetFullPath)) + InitializeRegistryEntry(); + + std::vector installFiles; + if (std::filesystem::is_directory(installerPath)) { - AICLI_LOG(CLI, Info, << "Successfully deleted portable exe:" << PortableTargetFullPath); - std::filesystem::remove(PortableTargetFullPath); + for (const auto& entry : std::filesystem::directory_iterator(installerPath)) + { + installFiles.emplace_back(entry.path()); + } } else { - AICLI_LOG(CLI, Info, << "Portable exe not found; Unable to delete portable exe: " << PortableTargetFullPath); + installFiles.emplace_back(installerPath); } - RemoveInstallDirectory(purge); - - if (Filesystem::SymlinkExists(PortableSymlinkFullPath) && !std::filesystem::remove(PortableSymlinkFullPath)) + + std::vector desiredEntries = GetDesiredState(installFiles); + std::vector expectedEntries = GetExpectedState(); + + if (VerifyResolution(expectedEntries)) { - AICLI_LOG(CLI, Info, << "Portable symlink not found; Unable to delete portable symlink: " << PortableSymlinkFullPath); + // Return that files have been modified } - RemoveFromPathVariable(); + ResolutionEngine(desiredEntries, expectedEntries); + + AddToPathVariable(); + + FinalizeRegistryEntry(); - m_portableARPEntry.Delete(); - AICLI_LOG(CLI, Info, << "PortableARPEntry deleted."); - return ERROR_SUCCESS; } - HRESULT PortableInstaller::UninstallFromIndex(bool purge) + HRESULT PortableInstaller::Uninstall() { - const auto& indexPath = GetPortableIndexPath(); - RemoveItemsFromIndex(indexPath); + std::vector desiredEntries; + std::vector expectedEntries = GetExpectedState(); + + if (VerifyResolution(expectedEntries)) + { + // Return that files have been modified + } - RemoveInstallDirectory(purge); + ResolutionEngine(desiredEntries, expectedEntries); RemoveFromPathVariable(); m_portableARPEntry.Delete(); AICLI_LOG(CLI, Info, << "PortableARPEntry deleted."); - + return ERROR_SUCCESS; } @@ -276,7 +410,7 @@ namespace AppInstaller::CLI::Portable AICLI_LOG(Core, Info, << "Created target install directory: " << InstallLocation); isDirectoryCreated = true; } - + if (std::filesystem::exists(PortableTargetFullPath)) { AICLI_LOG(Core, Info, << "Removing existing portable exe at: " << PortableTargetFullPath); @@ -424,6 +558,7 @@ namespace AppInstaller::CLI::Portable PortableTargetFullPath = GetPathValue(PortableValueName::PortableTargetFullPath); InstallLocation = GetPathValue(PortableValueName::InstallLocation); InstallDirectoryAddedToPath = GetBoolValue(PortableValueName::InstallDirectoryAddedToPath); + InstallDirectoryCreated = GetBoolValue(PortableValueName::InstallDirectoryCreated); } } diff --git a/src/AppInstallerCLICore/PortableInstaller.h b/src/AppInstallerCLICore/PortableInstaller.h index e9c7c32e29..4c1f2e6c3b 100644 --- a/src/AppInstallerCLICore/PortableInstaller.h +++ b/src/AppInstallerCLICore/PortableInstaller.h @@ -2,13 +2,13 @@ // Licensed under the MIT License. #pragma once #include "winget/PortableARPEntry.h" +#include "winget/PortableFileEntry.h" #include using namespace AppInstaller::Registry::Portable; namespace AppInstaller::CLI::Portable { - std::filesystem::path GetPortableLinksLocation(Manifest::ScopeEnum scope); std::filesystem::path GetPortableInstallRoot(Manifest::ScopeEnum scope, Utility::Architecture arch); @@ -31,28 +31,34 @@ namespace AppInstaller::CLI::Portable std::string WinGetPackageIdentifier; std::string WinGetSourceIdentifier; bool IsUpdate = false; + bool Purge = false; + bool InstallDirectoryCreated = false; // If we fail to create a symlink, add install directory to PATH variable bool InstallDirectoryAddedToPath = false; + bool RecordToIndex = false; + + std::string Rename; + std::string CommandAlias; + + std::filesystem::path installerPathDirectory; + + void InstallFile(AppInstaller::Portable::PortableFileEntry& desiredState); + + void RemoveFile(AppInstaller::Portable::PortableFileEntry& desiredState); PortableInstaller(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode); - HRESULT InstallSingle(const std::filesystem::path& installerPath); + std::vector GetDesiredState(std::vector files); - HRESULT InstallMultiple( - const std::vector& nestedInstallerFiles, - const std::vector& extractedItems); + std::vector GetExpectedState(); - HRESULT Uninstall(bool purge = false) - { - if (std::filesystem::exists(GetPortableIndexPath())) - { - return UninstallFromIndex(purge); - } - else - { - return UninstallSingle(purge); - } - } + void ResolutionEngine(std::vector& desiredState, std::vector& expectedState); + + bool VerifyResolution(std::vector& expectedState); + + HRESULT Install(const std::filesystem::path& installerPath); + + HRESULT Uninstall(); template void CommitToARPEntry(PortableValueName valueName, T value) @@ -67,8 +73,6 @@ namespace AppInstaller::CLI::Portable std::filesystem::path GetPortableIndexPath(); - bool VerifyPortableFilesForUninstall(); - Manifest::ScopeEnum GetScope() { return m_portableARPEntry.GetScope(); }; Utility::Architecture GetArch() { return m_portableARPEntry.GetArchitecture(); }; @@ -97,9 +101,6 @@ namespace AppInstaller::CLI::Portable void InitializeRegistryEntry(); void FinalizeRegistryEntry(); - HRESULT UninstallSingle(bool purge = false); - HRESULT UninstallFromIndex(bool purge = false); - void MovePortableExe(const std::filesystem::path& installerPath); bool CreatePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath); diff --git a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp index c875e2fe4f..ace627f14e 100644 --- a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp @@ -10,38 +10,26 @@ using namespace AppInstaller::Manifest; namespace AppInstaller::CLI::Workflow { + namespace + { + constexpr std::wstring_view s_Extracted = L"extracted"; + } + void ExtractFilesFromArchive(Execution::Context& context) { const auto& installerPath = context.Get(); - std::filesystem::path destinationFolder; - std::vector extractedItems; - bool isDirectoryCreated = false; - - if (context.Get()->NestedInstallerType == InstallerTypeEnum::Portable) - { - destinationFolder = GetPortableTargetDirectory(context); - isDirectoryCreated = std::filesystem::create_directory(destinationFolder); - } - else - { - destinationFolder = installerPath.parent_path(); - } + std::filesystem::path destinationFolder = installerPath.parent_path() / s_Extracted; + std::filesystem::create_directory(destinationFolder); AICLI_LOG(CLI, Info, << "Extracting archive to: " << destinationFolder); - HRESULT result = AppInstaller::Archive::TryExtractArchive(installerPath, destinationFolder, extractedItems); + HRESULT result = AppInstaller::Archive::TryExtractArchive(installerPath, destinationFolder); if (SUCCEEDED(result)) { AICLI_LOG(CLI, Info, << "Successfully extracted archive"); - context.Add(extractedItems); } else { - if (isDirectoryCreated) - { - std::filesystem::remove(destinationFolder); - } - 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); @@ -58,21 +46,13 @@ namespace AppInstaller::CLI::Workflow AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INVALID_MANIFEST); } - std::filesystem::path destinationFolder; - if (IsPortableType(installer.NestedInstallerType)) - { - destinationFolder = GetPortableTargetDirectory(context); - } - else - { - destinationFolder = context.Get().parent_path(); - } + std::filesystem::path targetInstallerPath = context.Get().parent_path() / s_Extracted; for (const auto& nestedInstallerFile : installer.NestedInstallerFiles) { - const std::filesystem::path& nestedInstallerPath = destinationFolder / ConvertToUTF16(nestedInstallerFile.RelativeFilePath); + const std::filesystem::path& nestedInstallerPath = targetInstallerPath / ConvertToUTF16(nestedInstallerFile.RelativeFilePath); - if (Filesystem::PathEscapesBaseDirectory(nestedInstallerPath, destinationFolder)) + if (Filesystem::PathEscapesBaseDirectory(nestedInstallerPath, targetInstallerPath)) { AICLI_LOG(CLI, Error, << "Path points to a location outside of the install directory: " << nestedInstallerPath); context.Reporter.Error() << Resource::String::InvalidPathToNestedInstaller << std::endl; @@ -88,9 +68,11 @@ namespace AppInstaller::CLI::Workflow { // Update the installerPath to the extracted non-portable installer. AICLI_LOG(CLI, Info, << "Setting installerPath to: " << nestedInstallerPath); - context.Add(nestedInstallerPath); + targetInstallerPath = nestedInstallerPath; } } + + context.Add(targetInstallerPath); } void EnsureValidNestedInstallerMetadataForArchiveInstall(Execution::Context& context) diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp index aa204f60c7..5d4a64521b 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -5,11 +5,14 @@ #include "PortableInstaller.h" #include "WorkflowBase.h" #include "winget/Filesystem.h" +#include "winget/PortableFileEntry.h" +#include using namespace AppInstaller::Manifest; using namespace AppInstaller::Repository; using namespace AppInstaller::Utility; using namespace AppInstaller::CLI::Portable; +using namespace AppInstaller::Portable; namespace AppInstaller::CLI::Workflow { @@ -221,6 +224,126 @@ namespace AppInstaller::CLI::Workflow context.Add(std::move(portableInstaller)); } + std::vector GetDesiredStateForPortableInstall(Execution::Context& context) + { + std::filesystem::path& installerPath = context.Get(); + PortableInstaller& portableInstaller = context.Get(); + std::vector entries; + + // The order that we create these state matters. For install the order should be: + // 1. Install Root Directory + // 2. Files and Directories + // 3. Symlinks + // Uninstall should process these portable files in the reverse order for proper cleanup. + + const std::filesystem::path& installLocation = GetPortableTargetDirectory(context); + entries.emplace_back(std::move(PortableFileEntry::CreateDirectoryEntry(installLocation))); + + if (std::filesystem::is_directory(installerPath)) + { + std::vector directoryEntries; + std::vector fileEntries; + + for (const auto& entry : std::filesystem::directory_iterator(installerPath)) + { + std::filesystem::path entryPath = entry.path(); + PortableFileEntry portableFile; + if (std::filesystem::is_directory(entryPath)) + { + directoryEntries.emplace_back(std::move(PortableFileEntry::CreateDirectoryEntry(entryPath))); + } + else + { + std::filesystem::path relativePath = std::filesystem::relative(entryPath, entryPath.parent_path()); + fileEntries.emplace_back(std::move(PortableFileEntry::CreateFileEntry(entryPath, installLocation / relativePath))); + } + } + + // If there is more than one portable file, record to index. + if (fileEntries.size() > 1) + { + portableInstaller.RecordToIndex = true; + } + + const std::vector& nestedInstallerFiles = context.Get()->NestedInstallerFiles; + + for (const auto& nestedInstallerFile : nestedInstallerFiles) + { + const std::filesystem::path& targetPath = installLocation / ConvertToUTF16(nestedInstallerFile.RelativeFilePath); + + std::filesystem::path commandAlias; + if (!nestedInstallerFile.PortableCommandAlias.empty()) + { + commandAlias = ConvertToUTF16(nestedInstallerFile.PortableCommandAlias); + } + else + { + commandAlias = targetPath.filename(); + } + + Filesystem::AppendExtension(commandAlias, ".exe"); + const std::filesystem::path& symlinkFullPath = GetPortableLinksLocation(portableInstaller.GetScope()) / commandAlias; + entries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkFullPath, targetPath))); + } + } + else + { + const std::filesystem::path& targetFullPath = GetPortableTargetFullPath(context); + const std::filesystem::path& symlinkFullPath = GetPortableSymlinkFullPath(context); + entries.emplace_back(std::move(PortableFileEntry::CreateFileEntry(installerPath, targetFullPath))); + entries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkFullPath, targetFullPath))); + } + + return entries; + } + + std::vector GetExpectedState(Execution::Context& context) + { + PortableInstaller& portableInstaller = context.Get(); + + std::vector entries; + const auto& indexPath = portableInstaller.GetPortableIndexPath(); + if (std::filesystem::exists(indexPath)) + { + AppInstaller::Repository::Microsoft::PortableIndex portableIndex = AppInstaller::Repository::Microsoft::PortableIndex::Open(indexPath.u8string(), AppInstaller::Repository::Microsoft::SQLiteStorageBase::OpenDisposition::ReadWrite); + entries = portableIndex.GetAllPortableFiles(); + } + else + { + std::filesystem::path targetFullPath = portableInstaller.PortableTargetFullPath; + std::filesystem::path symlinkFullPath = portableInstaller.PortableSymlinkFullPath; + std::filesystem::path installLocation = portableInstaller.InstallLocation; + + if (!portableInstaller.PortableSymlinkFullPath.empty()) + { + AppInstaller::Portable::PortableFileEntry symlinkEntry; + symlinkEntry.SetFilePath(symlinkFullPath); + symlinkEntry.SymlinkTarget = targetFullPath.u8string(); + symlinkEntry.FileType = PortableFileType::Symlink; + entries.emplace_back(symlinkEntry); + } + + if (!targetFullPath.empty()) + { + AppInstaller::Portable::PortableFileEntry exeEntry; + exeEntry.SetFilePath(targetFullPath); + exeEntry.SHA256 = portableInstaller.SHA256; + exeEntry.FileType = PortableFileType::File; + entries.emplace_back(exeEntry); + } + + if (!installLocation.empty() && portableInstaller.InstallDirectoryCreated) + { + AppInstaller::Portable::PortableFileEntry directoryEntry; + directoryEntry.SetFilePath(installLocation); + directoryEntry.FileType = PortableFileType::Directory; + entries.emplace_back(directoryEntry); + } + } + + return entries; + } + void PortableInstallImpl(Execution::Context& context) { HRESULT result = ERROR_SUCCESS; @@ -230,18 +353,24 @@ namespace AppInstaller::CLI::Workflow { context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl; - if (IsArchiveType(context.Get()->BaseInstallerType)) - { - const std::vector& extractedItems = context.Get(); - const std::vector& nestedInstallerFiles = context.Get()->NestedInstallerFiles; - result = portableInstaller.InstallMultiple(nestedInstallerFiles, extractedItems); - } - else + // Use context to get desired state and pass it to the portable install resolution engine + std::vector desiredState = GetDesiredStateForPortableInstall(context); + std::vector expectedState = GetExpectedState(context); + + //portableInstaller.InitializeRegistryEntry(); + + if (portableInstaller.VerifyResolution(expectedState)) { - const std::filesystem::path& installerPath = context.Get(); - result = portableInstaller.InstallSingle(installerPath); + // Return that files have been modified } + portableInstaller.ResolutionEngine(desiredState, expectedState); + + //portableInstaller.AddToPathVariable(); + + //FinalizeRegistryEntry(); + + context.Add(result); context.Reporter.Warn() << portableInstaller.GetOutputMessage(); } @@ -265,24 +394,24 @@ namespace AppInstaller::CLI::Workflow { context.Reporter.Info() << Resource::String::UninstallFlowStartingPackageUninstall << std::endl; - if (!portableInstaller.VerifyPortableFilesForUninstall()) - { - // TODO: replace with appropriate --force argument when available. - if (context.Args.Contains(Execution::Args::Type::HashOverride)) - { - context.Reporter.Warn() << Resource::String::PortableHashMismatchOverridden << std::endl; - } - else - { - context.Reporter.Warn() << Resource::String::PortableHashMismatchOverrideRequired << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED); - } - } + //if (!portableInstaller.VerifyResolution()) + //{ + // // TODO: replace with appropriate --force argument when available. + // if (context.Args.Contains(Execution::Args::Type::HashOverride)) + // { + // context.Reporter.Warn() << Resource::String::PortableHashMismatchOverridden << std::endl; + // } + // else + // { + // context.Reporter.Warn() << Resource::String::PortableHashMismatchOverrideRequired << std::endl; + // AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED); + // } + //} - bool purge = context.Args.Contains(Execution::Args::Type::Purge) || + portableInstaller.Purge = context.Args.Contains(Execution::Args::Type::Purge) || (!portableInstaller.IsUpdate && Settings::User().Get() && !context.Args.Contains(Execution::Args::Type::Preserve)); - HRESULT result = portableInstaller.Uninstall(purge); + HRESULT result = portableInstaller.Uninstall(); context.Add(result); context.Reporter.Warn() << portableInstaller.GetOutputMessage(); } diff --git a/src/AppInstallerCLITests/Archive.cpp b/src/AppInstallerCLITests/Archive.cpp index e9f5ea619f..7354c3ed9f 100644 --- a/src/AppInstallerCLITests/Archive.cpp +++ b/src/AppInstallerCLITests/Archive.cpp @@ -17,12 +17,9 @@ TEST_CASE("Extract_ZipArchive", "[archive]") const auto& testZipPath = testZip.GetPath(); const auto& tempDirectoryPath = tempDirectory.GetPath(); - std::vector extractedItems; - HRESULT hr = TryExtractArchive(testZipPath, tempDirectoryPath, extractedItems); + HRESULT hr = TryExtractArchive(testZipPath, tempDirectoryPath); std::filesystem::path expectedPath = tempDirectoryPath / "test.txt"; REQUIRE(SUCCEEDED(hr)); REQUIRE(std::filesystem::exists(expectedPath)); - REQUIRE(extractedItems.size() == 1); - REQUIRE(extractedItems[0] == expectedPath); } \ No newline at end of file diff --git a/src/AppInstallerCLITests/PortableIndex.cpp b/src/AppInstallerCLITests/PortableIndex.cpp index 8f5f69634e..06410e8cc0 100644 --- a/src/AppInstallerCLITests/PortableIndex.cpp +++ b/src/AppInstallerCLITests/PortableIndex.cpp @@ -7,17 +7,19 @@ #include #include #include +#include using namespace std::string_literals; using namespace TestCommon; +using namespace AppInstaller::Portable; using namespace AppInstaller::Repository::Microsoft; using namespace AppInstaller::Repository::SQLite; using namespace AppInstaller::Repository::Microsoft::Schema; -void CreateFakePortableFile(IPortableIndex::PortableFile& file) +void CreateFakePortableFile(PortableFileEntry& file) { file.SetFilePath("testPortableFile.exe"); - file.FileType = IPortableIndex::PortableFileType::File; + file.FileType = PortableFileType::File; file.SHA256 = "f0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b"; file.SymlinkTarget = "testSymlinkTarget.exe"; } @@ -65,7 +67,7 @@ TEST_CASE("PortableIndexAddEntryToTable", "[portableIndex]") TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; INFO("Using temporary file named: " << tempFile.GetPath()); - IPortableIndex::PortableFile portableFile; + PortableFileEntry portableFile; CreateFakePortableFile(portableFile); { @@ -96,7 +98,7 @@ TEST_CASE("PortableIndex_AddUpdateRemove", "[portableIndex]") TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; INFO("Using temporary file named: " << tempFile.GetPath()); - IPortableIndex::PortableFile portableFile; + PortableFileEntry portableFile; CreateFakePortableFile(portableFile); PortableIndex index = PortableIndex::CreateNew(tempFile, { 1, 0 }); @@ -104,7 +106,7 @@ TEST_CASE("PortableIndex_AddUpdateRemove", "[portableIndex]") // Apply changes to portable file std::string updatedHash = "2db8ae7657c6622b04700137740002c51c36588e566651c9f67b4b096c8ad18b"; - portableFile.FileType = IPortableIndex::PortableFileType::Symlink; + portableFile.FileType = PortableFileType::Symlink; portableFile.SHA256 = updatedHash; portableFile.SymlinkTarget = "fakeSymlinkTarget.exe"; @@ -115,7 +117,7 @@ TEST_CASE("PortableIndex_AddUpdateRemove", "[portableIndex]") auto fileFromIndex = Schema::Portable_V1_0::PortableTable::GetPortableFileById(connection, 1); REQUIRE(fileFromIndex.has_value()); REQUIRE(fileFromIndex->GetFilePath() == portableFile.GetFilePath()); - REQUIRE(fileFromIndex->FileType == IPortableIndex::PortableFileType::Symlink); + REQUIRE(fileFromIndex->FileType == PortableFileType::Symlink); REQUIRE(fileFromIndex->SHA256 == updatedHash); REQUIRE(fileFromIndex->SymlinkTarget == "fakeSymlinkTarget.exe"); } @@ -137,7 +139,7 @@ TEST_CASE("PortableIndex_UpdateFile_CaseInsensitive", "[portableIndex]") TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; INFO("Using temporary file named: " << tempFile.GetPath()); - IPortableIndex::PortableFile portableFile; + PortableFileEntry portableFile; CreateFakePortableFile(portableFile); PortableIndex index = PortableIndex::CreateNew(tempFile, { 1, 0 }); @@ -147,7 +149,7 @@ TEST_CASE("PortableIndex_UpdateFile_CaseInsensitive", "[portableIndex]") // Change file path to all upper case should still successfully update. portableFile.SetFilePath("TESTPORTABLEFILE.exe"); std::string updatedHash = "2db8ae7657c6622b04700137740002c51c36588e566651c9f67b4b096c8ad18b"; - portableFile.FileType = IPortableIndex::PortableFileType::Symlink; + portableFile.FileType = PortableFileType::Symlink; portableFile.SHA256 = updatedHash; portableFile.SymlinkTarget = "fakeSymlinkTarget.exe"; @@ -158,7 +160,7 @@ TEST_CASE("PortableIndex_UpdateFile_CaseInsensitive", "[portableIndex]") auto fileFromIndex = Schema::Portable_V1_0::PortableTable::GetPortableFileById(connection, 1); REQUIRE(fileFromIndex.has_value()); REQUIRE(fileFromIndex->GetFilePath() == portableFile.GetFilePath()); - REQUIRE(fileFromIndex->FileType == IPortableIndex::PortableFileType::Symlink); + REQUIRE(fileFromIndex->FileType == PortableFileType::Symlink); REQUIRE(fileFromIndex->SHA256 == updatedHash); REQUIRE(fileFromIndex->SymlinkTarget == "fakeSymlinkTarget.exe"); } @@ -169,7 +171,7 @@ TEST_CASE("PortableIndex_AddDuplicateFile", "[portableIndex]") TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; INFO("Using temporary file named: " << tempFile.GetPath()); - IPortableIndex::PortableFile portableFile; + PortableFileEntry portableFile; CreateFakePortableFile(portableFile); PortableIndex index = PortableIndex::CreateNew(tempFile, { 1, 0 }); @@ -185,7 +187,7 @@ TEST_CASE("PortableIndex_RemoveWithId", "[portableIndex]") TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; INFO("Using temporary file named: " << tempFile.GetPath()); - IPortableIndex::PortableFile portableFile; + PortableFileEntry portableFile; CreateFakePortableFile(portableFile); PortableIndex index = PortableIndex::CreateNew(tempFile, { 1, 0 }); diff --git a/src/AppInstallerCLITests/PortableInstaller.cpp b/src/AppInstallerCLITests/PortableInstaller.cpp index 5a1b0b4aa5..8fc825f84c 100644 --- a/src/AppInstallerCLITests/PortableInstaller.cpp +++ b/src/AppInstallerCLITests/PortableInstaller.cpp @@ -74,7 +74,6 @@ std::vector CreateTestNestedInstallerFiles() // Ensures that the portable exes and symlinks all got recorded in the index. void VerifyPortableFilesTrackedByIndex( const std::filesystem::path& indexPath, - const std::vector& extractedItems, const std::vector& nestedInstallerFiles) { { @@ -82,11 +81,6 @@ void VerifyPortableFilesTrackedByIndex( Connection connection = Connection::Create(indexPath.u8string(), Connection::OpenDisposition::ReadWrite); REQUIRE(!Schema::Portable_V1_0::PortableTable::IsEmpty(connection)); - for (const auto& item : extractedItems) - { - REQUIRE(Schema::Portable_V1_0::PortableTable::SelectByFilePath(connection, item).has_value()); - } - // Verify that all symlinks were added to the index. for (const auto& item : nestedInstallerFiles) { @@ -107,208 +101,207 @@ void VerifyPortableFilesTrackedByIndex( } } -TEST_CASE("PortableInstaller_SingleInstall", "[PortableInstaller]") -{ - TempDirectory tempDirectory = TestCommon::TempDirectory("TempDirectory", false); - - { - PortableInstaller portableInstaller = CreateTestPortableInstaller(tempDirectory.GetPath()); - - TestCommon::TempFile testPortable("testPortable.txt"); - std::ofstream file2(testPortable, std::ofstream::out); - file2.close(); - - HRESULT installResult = portableInstaller.InstallSingle(testPortable.GetPath()); - REQUIRE(SUCCEEDED(installResult)); - } - - { - // Create a new portable installer instance and verify that values from ARP are loaded correctly. - PortableInstaller portableInstaller = PortableInstaller(ScopeEnum::User, Architecture::X64, "testProductCode"); - - REQUIRE(std::filesystem::exists(portableInstaller.PortableTargetFullPath)); - REQUIRE(SymlinkExists(portableInstaller.PortableSymlinkFullPath)); - REQUIRE(VerifySymlink(portableInstaller.PortableSymlinkFullPath, portableInstaller.PortableTargetFullPath)); - REQUIRE(portableInstaller.ARPEntryExists()); - - HRESULT uninstallResult = portableInstaller.Uninstall(); - - REQUIRE(SUCCEEDED(uninstallResult)); - REQUIRE_FALSE(SymlinkExists(portableInstaller.PortableSymlinkFullPath)); - REQUIRE_FALSE(std::filesystem::exists(portableInstaller.PortableTargetFullPath)); - REQUIRE_FALSE(std::filesystem::exists(portableInstaller.InstallLocation)); - } -} - -TEST_CASE("PortableInstaller_InstallDirectoryAddedToPath", "[PortableInstaller]") -{ - TempDirectory tempDirectory = TestCommon::TempDirectory("TempDirectory", false); - - { - PortableInstaller portableInstaller = CreateTestPortableInstaller(tempDirectory.GetPath()); - - TestCommon::TempFile testPortable("testPortable.txt"); - std::ofstream file2(testPortable, std::ofstream::out); - file2.close(); - - // This value is only set to true if we fail to create a symlink. - // If true no symlink should be created and InstallDirectory is added to PATH variable. - portableInstaller.CommitToARPEntry(PortableValueName::InstallDirectoryAddedToPath, portableInstaller.InstallDirectoryAddedToPath = true); - - HRESULT installResult = portableInstaller.InstallSingle(testPortable.GetPath()); - REQUIRE(SUCCEEDED(installResult)); - } - - { - PortableInstaller portableInstaller = PortableInstaller(ScopeEnum::User, Architecture::X64, "testProductCode"); - - REQUIRE(std::filesystem::exists(portableInstaller.PortableTargetFullPath)); - REQUIRE_FALSE(SymlinkExists(portableInstaller.PortableSymlinkFullPath)); - REQUIRE(PathVariable(ScopeEnum::User).Contains(portableInstaller.InstallLocation)); - REQUIRE(portableInstaller.ARPEntryExists()); - - HRESULT uninstallResult = portableInstaller.Uninstall(); - - REQUIRE(SUCCEEDED(uninstallResult)); - REQUIRE(portableInstaller.InstallDirectoryAddedToPath); - REQUIRE_FALSE(std::filesystem::exists(portableInstaller.PortableTargetFullPath)); - REQUIRE_FALSE(std::filesystem::exists(portableInstaller.InstallLocation)); - REQUIRE_FALSE(PathVariable(ScopeEnum::User).Contains(portableInstaller.InstallLocation)); - } -} - -TEST_CASE("PortableInstaller_MultipleInstall", "[PortableInstaller]") -{ - PortableInstaller portableInstaller = PortableInstaller( - ScopeEnum::User, - Architecture::X64, - "testProductCode"); - - TempDirectory tempDirectory = TestCommon::TempDirectory("TempDirectory", true); - const auto& tempDirectoryPath = tempDirectory.GetPath(); - portableInstaller.InstallLocation = tempDirectoryPath; - - std::vector nestedInstallerFiles = CreateTestNestedInstallerFiles(); - std::vector extractedItems = CreateExtractedItemsFromArchive(tempDirectoryPath); - - HRESULT installResult = portableInstaller.InstallMultiple(nestedInstallerFiles, extractedItems); - REQUIRE(SUCCEEDED(installResult)); - - const auto& indexPath = portableInstaller.GetPortableIndexPath(); - REQUIRE(std::filesystem::exists(indexPath)); - - VerifyPortableFilesTrackedByIndex(indexPath, extractedItems, nestedInstallerFiles); - - REQUIRE(portableInstaller.VerifyPortableFilesForUninstall()); - - // Perform uninstall - HRESULT uninstallResult = portableInstaller.Uninstall(); - REQUIRE(SUCCEEDED(uninstallResult)); - - REQUIRE_FALSE(std::filesystem::exists(indexPath)); - REQUIRE_FALSE(std::filesystem::exists(portableInstaller.InstallLocation)); -} - -TEST_CASE("PortableInstaller_VerifyFilesFromIndex", "[PortableInstaller]") -{ - // Create installer and set install location to temp directory - // Create portable index and add files - PortableInstaller portableInstaller = PortableInstaller( - ScopeEnum::User, - Architecture::X64, - "testProductCode"); - - TempDirectory tempDirectory = TestCommon::TempDirectory("TempDirectory", true); - const auto& tempDirectoryPath = tempDirectory.GetPath(); - portableInstaller.InstallLocation = tempDirectoryPath; - - std::filesystem::path indexPath = tempDirectoryPath / "portable.db"; - - PortableIndex index = PortableIndex::CreateNew(indexPath.u8string(), Schema::Version::Latest()); - - std::filesystem::path symlinkPath = tempDirectoryPath / "symlink.exe"; - std::filesystem::path exePath = tempDirectoryPath / "testExe.txt"; - - // Create exe and symlink - TestCommon::TempFile testFile(exePath); - std::ofstream file2(testFile, std::ofstream::out); - file2.close(); - - std::filesystem::create_symlink(exePath, symlinkPath); - - // Add files to index - IPortableIndex::PortableFile exeFile = PortableIndex::CreatePortableFileFromPath(exePath); - IPortableIndex::PortableFile symlinkFile = PortableIndex::CreatePortableFileFromPath(symlinkPath); - - index.AddPortableFile(exeFile); - index.AddPortableFile(symlinkFile); - - REQUIRE(portableInstaller.VerifyPortableFilesForUninstall()); - - // Modify symlink target - std::filesystem::remove(symlinkPath); - std::filesystem::create_symlink("badPath", symlinkPath); - - REQUIRE_FALSE(portableInstaller.VerifyPortableFilesForUninstall()); - - std::filesystem::remove(symlinkPath); - - // Modify exe hash in index - exeFile.SHA256 = "2413fb3709b05939f04cf2e92f7d0897fc2596f9ad0b8a9ea855c7bfebaae892"; - index.UpdatePortableFile(exeFile); - - REQUIRE_FALSE(portableInstaller.VerifyPortableFilesForUninstall()); - - std::filesystem::remove(exePath); - - // Files that do not exist should still pass as they have already been removed. - REQUIRE(portableInstaller.VerifyPortableFilesForUninstall()); -} - -TEST_CASE("PortableInstaller_VerifyFiles", "[PortableInstaller]") -{ - PortableInstaller portableInstaller = PortableInstaller( - ScopeEnum::User, - Architecture::X64, - "testProductCode"); - - TempDirectory tempDirectory = TestCommon::TempDirectory("TempDirectory", true); - const auto& tempDirectoryPath = tempDirectory.GetPath(); - portableInstaller.InstallLocation = tempDirectoryPath; - - std::filesystem::path symlinkPath = tempDirectoryPath / "symlink.exe"; - std::filesystem::path exePath = tempDirectoryPath / "testExe.txt"; - - // Create and set test file, symlink, and hash - TestCommon::TempFile testFile(exePath); - std::ofstream file(testFile, std::ofstream::out); - file.close(); - - std::filesystem::create_symlink(exePath, symlinkPath); - - std::ifstream inStream{ exePath, std::ifstream::binary }; - const SHA256::HashBuffer& targetFileHash = SHA256::ComputeHash(inStream); - inStream.close(); - portableInstaller.SHA256 = SHA256::ConvertToString(targetFileHash); - portableInstaller.PortableTargetFullPath = exePath; - portableInstaller.PortableSymlinkFullPath = symlinkPath; - - REQUIRE(portableInstaller.VerifyPortableFilesForUninstall()); - - // Modify symlink target - std::filesystem::remove(symlinkPath); - std::filesystem::create_symlink("badPath", symlinkPath); - - REQUIRE_FALSE(portableInstaller.VerifyPortableFilesForUninstall()); - - // Modify exe hash - std::filesystem::remove(symlinkPath); - portableInstaller.SHA256 = "2413fb3709b05939f04cf2e92f7d0897fc2596f9ad0b8a9ea855c7bfebaae892"; - - REQUIRE_FALSE(portableInstaller.VerifyPortableFilesForUninstall()); - - std::filesystem::remove(exePath); - - // Files that do not exist should still pass as they have already been removed. - REQUIRE(portableInstaller.VerifyPortableFilesForUninstall()); -} \ No newline at end of file +//TEST_CASE("PortableInstaller_SingleInstall", "[PortableInstaller]") +//{ +// TempDirectory tempDirectory = TestCommon::TempDirectory("TempDirectory", false); +// +// { +// PortableInstaller portableInstaller = CreateTestPortableInstaller(tempDirectory.GetPath()); +// +// TestCommon::TempFile testPortable("testPortable.txt"); +// std::ofstream file2(testPortable, std::ofstream::out); +// file2.close(); +// +// HRESULT installResult = portableInstaller.InstallSingle(testPortable.GetPath()); +// REQUIRE(SUCCEEDED(installResult)); +// } +// +// { +// // Create a new portable installer instance and verify that values from ARP are loaded correctly. +// PortableInstaller portableInstaller = PortableInstaller(ScopeEnum::User, Architecture::X64, "testProductCode"); +// +// REQUIRE(std::filesystem::exists(portableInstaller.PortableTargetFullPath)); +// REQUIRE(SymlinkExists(portableInstaller.PortableSymlinkFullPath)); +// REQUIRE(VerifySymlink(portableInstaller.PortableSymlinkFullPath, portableInstaller.PortableTargetFullPath)); +// REQUIRE(portableInstaller.ARPEntryExists()); +// +// HRESULT uninstallResult = portableInstaller.Uninstall(); +// +// REQUIRE(SUCCEEDED(uninstallResult)); +// REQUIRE_FALSE(SymlinkExists(portableInstaller.PortableSymlinkFullPath)); +// REQUIRE_FALSE(std::filesystem::exists(portableInstaller.PortableTargetFullPath)); +// REQUIRE_FALSE(std::filesystem::exists(portableInstaller.InstallLocation)); +// } +//} +// +//TEST_CASE("PortableInstaller_InstallDirectoryAddedToPath", "[PortableInstaller]") +//{ +// TempDirectory tempDirectory = TestCommon::TempDirectory("TempDirectory", false); +// +// { +// PortableInstaller portableInstaller = CreateTestPortableInstaller(tempDirectory.GetPath()); +// +// TestCommon::TempFile testPortable("testPortable.txt"); +// std::ofstream file2(testPortable, std::ofstream::out); +// file2.close(); +// +// // This value is only set to true if we fail to create a symlink. +// // If true no symlink should be created and InstallDirectory is added to PATH variable. +// portableInstaller.CommitToARPEntry(PortableValueName::InstallDirectoryAddedToPath, portableInstaller.InstallDirectoryAddedToPath = true); +// +// HRESULT installResult = portableInstaller.InstallSingle(testPortable.GetPath()); +// REQUIRE(SUCCEEDED(installResult)); +// } +// +// { +// PortableInstaller portableInstaller = PortableInstaller(ScopeEnum::User, Architecture::X64, "testProductCode"); +// +// REQUIRE(std::filesystem::exists(portableInstaller.PortableTargetFullPath)); +// REQUIRE_FALSE(SymlinkExists(portableInstaller.PortableSymlinkFullPath)); +// REQUIRE(PathVariable(ScopeEnum::User).Contains(portableInstaller.InstallLocation)); +// REQUIRE(portableInstaller.ARPEntryExists()); +// +// HRESULT uninstallResult = portableInstaller.Uninstall(); +// +// REQUIRE(SUCCEEDED(uninstallResult)); +// REQUIRE(portableInstaller.InstallDirectoryAddedToPath); +// REQUIRE_FALSE(std::filesystem::exists(portableInstaller.PortableTargetFullPath)); +// REQUIRE_FALSE(std::filesystem::exists(portableInstaller.InstallLocation)); +// REQUIRE_FALSE(PathVariable(ScopeEnum::User).Contains(portableInstaller.InstallLocation)); +// } +//} +// +//TEST_CASE("PortableInstaller_MultipleInstall", "[PortableInstaller]") +//{ +// PortableInstaller portableInstaller = PortableInstaller( +// ScopeEnum::User, +// Architecture::X64, +// "testProductCode"); +// +// TempDirectory tempDirectory = TestCommon::TempDirectory("TempDirectory", true); +// const auto& tempDirectoryPath = tempDirectory.GetPath(); +// portableInstaller.InstallLocation = tempDirectoryPath; +// +// std::vector nestedInstallerFiles = CreateTestNestedInstallerFiles(); +// +// HRESULT installResult = portableInstaller.InstallMultiple(nestedInstallerFiles); +// REQUIRE(SUCCEEDED(installResult)); +// +// const auto& indexPath = portableInstaller.GetPortableIndexPath(); +// REQUIRE(std::filesystem::exists(indexPath)); +// +// VerifyPortableFilesTrackedByIndex(indexPath, nestedInstallerFiles); +// +// REQUIRE(portableInstaller.VerifyPortableFilesForUninstall()); +// +// // Perform uninstall +// HRESULT uninstallResult = portableInstaller.Uninstall(); +// REQUIRE(SUCCEEDED(uninstallResult)); +// +// REQUIRE_FALSE(std::filesystem::exists(indexPath)); +// REQUIRE_FALSE(std::filesystem::exists(portableInstaller.InstallLocation)); +//} +// +//TEST_CASE("PortableInstaller_VerifyFilesFromIndex", "[PortableInstaller]") +//{ +// // Create installer and set install location to temp directory +// // Create portable index and add files +// PortableInstaller portableInstaller = PortableInstaller( +// ScopeEnum::User, +// Architecture::X64, +// "testProductCode"); +// +// TempDirectory tempDirectory = TestCommon::TempDirectory("TempDirectory", true); +// const auto& tempDirectoryPath = tempDirectory.GetPath(); +// portableInstaller.InstallLocation = tempDirectoryPath; +// +// std::filesystem::path indexPath = tempDirectoryPath / "portable.db"; +// +// PortableIndex index = PortableIndex::CreateNew(indexPath.u8string(), Schema::Version::Latest()); +// +// std::filesystem::path symlinkPath = tempDirectoryPath / "symlink.exe"; +// std::filesystem::path exePath = tempDirectoryPath / "testExe.txt"; +// +// // Create exe and symlink +// TestCommon::TempFile testFile(exePath); +// std::ofstream file2(testFile, std::ofstream::out); +// file2.close(); +// +// std::filesystem::create_symlink(exePath, symlinkPath); +// +// // Add files to index +// IPortableIndex::PortableFile exeFile = PortableIndex::CreatePortableFileFromPath(exePath); +// IPortableIndex::PortableFile symlinkFile = PortableIndex::CreatePortableFileFromPath(symlinkPath); +// +// index.AddPortableFile(exeFile); +// index.AddPortableFile(symlinkFile); +// +// REQUIRE(portableInstaller.VerifyPortableFilesForUninstall()); +// +// // Modify symlink target +// std::filesystem::remove(symlinkPath); +// std::filesystem::create_symlink("badPath", symlinkPath); +// +// REQUIRE_FALSE(portableInstaller.VerifyPortableFilesForUninstall()); +// +// std::filesystem::remove(symlinkPath); +// +// // Modify exe hash in index +// exeFile.SHA256 = "2413fb3709b05939f04cf2e92f7d0897fc2596f9ad0b8a9ea855c7bfebaae892"; +// index.UpdatePortableFile(exeFile); +// +// REQUIRE_FALSE(portableInstaller.VerifyPortableFilesForUninstall()); +// +// std::filesystem::remove(exePath); +// +// // Files that do not exist should still pass as they have already been removed. +// REQUIRE(portableInstaller.VerifyPortableFilesForUninstall()); +//} +// +//TEST_CASE("PortableInstaller_VerifyFiles", "[PortableInstaller]") +//{ +// PortableInstaller portableInstaller = PortableInstaller( +// ScopeEnum::User, +// Architecture::X64, +// "testProductCode"); +// +// TempDirectory tempDirectory = TestCommon::TempDirectory("TempDirectory", true); +// const auto& tempDirectoryPath = tempDirectory.GetPath(); +// portableInstaller.InstallLocation = tempDirectoryPath; +// +// std::filesystem::path symlinkPath = tempDirectoryPath / "symlink.exe"; +// std::filesystem::path exePath = tempDirectoryPath / "testExe.txt"; +// +// // Create and set test file, symlink, and hash +// TestCommon::TempFile testFile(exePath); +// std::ofstream file(testFile, std::ofstream::out); +// file.close(); +// +// std::filesystem::create_symlink(exePath, symlinkPath); +// +// std::ifstream inStream{ exePath, std::ifstream::binary }; +// const SHA256::HashBuffer& targetFileHash = SHA256::ComputeHash(inStream); +// inStream.close(); +// portableInstaller.SHA256 = SHA256::ConvertToString(targetFileHash); +// portableInstaller.PortableTargetFullPath = exePath; +// portableInstaller.PortableSymlinkFullPath = symlinkPath; +// +// REQUIRE(portableInstaller.VerifyPortableFilesForUninstall()); +// +// // Modify symlink target +// std::filesystem::remove(symlinkPath); +// std::filesystem::create_symlink("badPath", symlinkPath); +// +// REQUIRE_FALSE(portableInstaller.VerifyPortableFilesForUninstall()); +// +// // Modify exe hash +// std::filesystem::remove(symlinkPath); +// portableInstaller.SHA256 = "2413fb3709b05939f04cf2e92f7d0897fc2596f9ad0b8a9ea855c7bfebaae892"; +// +// REQUIRE_FALSE(portableInstaller.VerifyPortableFilesForUninstall()); +// +// std::filesystem::remove(exePath); +// +// // Files that do not exist should still pass as they have already been removed. +// REQUIRE(portableInstaller.VerifyPortableFilesForUninstall()); +//} \ No newline at end of file diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj index fb09a10e6d..54e88e180c 100644 --- a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj +++ b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -329,6 +329,7 @@ + diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters index 11eeff0e44..95dbf56006 100644 --- a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters +++ b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -192,6 +192,9 @@ Public\winget + + Public\winget + Public\winget diff --git a/src/AppInstallerCommonCore/Archive.cpp b/src/AppInstallerCommonCore/Archive.cpp index f262720933..a699e12fea 100644 --- a/src/AppInstallerCommonCore/Archive.cpp +++ b/src/AppInstallerCommonCore/Archive.cpp @@ -7,7 +7,7 @@ namespace AppInstaller::Archive using unique_pidlist_absolute = wil::unique_any; using unique_lpitemidlist = wil::unique_any; - HRESULT TryExtractArchive(const std::filesystem::path& archivePath, const std::filesystem::path& destPath, std::vector& extractedItems) + HRESULT TryExtractArchive(const std::filesystem::path& archivePath, const std::filesystem::path& destPath) { wil::com_ptr pFileOperation; RETURN_IF_FAILED(CoCreateInstance(CLSID_FileOperation, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pFileOperation))); @@ -36,7 +36,6 @@ namespace AppInstaller::Archive 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)); - extractedItems.emplace_back(destPath / szFolderName); } RETURN_IF_FAILED(pFileOperation->PerformOperations()); diff --git a/src/AppInstallerCommonCore/Public/winget/Archive.h b/src/AppInstallerCommonCore/Public/winget/Archive.h index 2b75fbff37..bee409e6f3 100644 --- a/src/AppInstallerCommonCore/Public/winget/Archive.h +++ b/src/AppInstallerCommonCore/Public/winget/Archive.h @@ -5,5 +5,5 @@ namespace AppInstaller::Archive { - HRESULT TryExtractArchive(const std::filesystem::path& archivePath, const std::filesystem::path& destPath, std::vector& extractedItems); + HRESULT TryExtractArchive(const std::filesystem::path& archivePath, const std::filesystem::path& destPath); } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h b/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h new file mode 100644 index 0000000000..91f17f7e16 --- /dev/null +++ b/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "AppInstallerSHA256.h" +#include +#include + +namespace AppInstaller::Portable +{ + // File type enum of the portable file + enum class PortableFileType + { + Unknown, + File, + Directory, + Symlink + }; + + // Metadata representation of a portable file placed down during installation + struct PortableFileEntry + { + // Version 1.0 + PortableFileType FileType = PortableFileType::Unknown; + std::string SHA256; + std::string SymlinkTarget; + std::filesystem::path CurrentPath; + + void SetFilePath(const std::filesystem::path& path) { m_filePath = std::filesystem::weakly_canonical(path); }; + + std::filesystem::path GetFilePath() const { return m_filePath; }; + + static PortableFileEntry CreateFileEntry(const std::filesystem::path& currentPath, const std::filesystem::path& targetPath) + { + PortableFileEntry fileEntry; + fileEntry.CurrentPath = currentPath; + fileEntry.SetFilePath(targetPath); + fileEntry.FileType = PortableFileType::File; + fileEntry.SHA256 = Utility::SHA256::ConvertToString(Utility::SHA256::ComputeHashFromFile(currentPath)); + return fileEntry; + } + + static PortableFileEntry CreateSymlinkEntry(const std::filesystem::path& symlinkPath, const std::filesystem::path& targetPath) + { + PortableFileEntry symlinkEntry; + symlinkEntry.SetFilePath(symlinkPath); + symlinkEntry.SymlinkTarget = targetPath.u8string(); + symlinkEntry.FileType = PortableFileType::Symlink; + return symlinkEntry; + } + + static PortableFileEntry CreateDirectoryEntry(const std::filesystem::path& directoryPath) + { + PortableFileEntry directoryEntry; + directoryEntry.SetFilePath(directoryPath); + directoryEntry.FileType = PortableFileType::Directory; + return directoryEntry; + } + + private: + std::filesystem::path m_filePath; + }; +} \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp index c851c44063..1b8caaff41 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp @@ -27,30 +27,7 @@ namespace AppInstaller::Repository::Microsoft return result; } - Schema::IPortableIndex::PortableFile PortableIndex::CreatePortableFileFromPath(const std::filesystem::path& path) - { - Schema::IPortableIndex::PortableFile portableFile; - portableFile.SetFilePath(path); - - if (std::filesystem::is_directory(path)) - { - portableFile.FileType = Schema::IPortableIndex::PortableFileType::Directory; - } - else if (Filesystem::SymlinkExists(path)) - { - portableFile.FileType = Schema::IPortableIndex::PortableFileType::Symlink; - portableFile.SymlinkTarget = std::filesystem::read_symlink(path).u8string(); - } - else - { - portableFile.FileType = Schema::IPortableIndex::PortableFileType::File; - portableFile.SHA256 = Utility::SHA256::ConvertToString(Utility::SHA256::ComputeHashFromFile(path)); - } - - return portableFile; - } - - PortableIndex::IdType PortableIndex::AddPortableFile(const Schema::IPortableIndex::PortableFile& file) + PortableIndex::IdType PortableIndex::AddPortableFile(const Portable::PortableFileEntry& file) { std::lock_guard lockInterface{ *m_interfaceLock }; AICLI_LOG(Repo, Verbose, << "Adding portable file for [" << file.GetFilePath() << "]"); @@ -66,7 +43,7 @@ namespace AppInstaller::Repository::Microsoft return result; } - void PortableIndex::RemovePortableFile(const Schema::IPortableIndex::PortableFile& file) + void PortableIndex::RemovePortableFile(const Portable::PortableFileEntry& file) { AICLI_LOG(Repo, Verbose, << "Removing portable file [" << file.GetFilePath() << "]"); std::lock_guard lockInterface{ *m_interfaceLock }; @@ -80,7 +57,7 @@ namespace AppInstaller::Repository::Microsoft savepoint.Commit(); } - bool PortableIndex::UpdatePortableFile(const Schema::IPortableIndex::PortableFile& file) + bool PortableIndex::UpdatePortableFile(const Portable::PortableFileEntry& file) { AICLI_LOG(Repo, Verbose, << "Updating portable file [" << file.GetFilePath() << "]"); std::lock_guard lockInterface{ *m_interfaceLock }; @@ -98,7 +75,7 @@ namespace AppInstaller::Repository::Microsoft return result; } - bool PortableIndex::Exists(const Schema::IPortableIndex::PortableFile& file) + bool PortableIndex::Exists(const Portable::PortableFileEntry& file) { AICLI_LOG(Repo, Verbose, << "Checking if portable file exists [" << file.GetFilePath() << "]"); return m_interface->Exists(m_dbconn, file); @@ -109,7 +86,7 @@ namespace AppInstaller::Repository::Microsoft return m_interface->IsEmpty(m_dbconn); } - void PortableIndex::AddOrUpdatePortableFile(const Schema::IPortableIndex::PortableFile& file) + void PortableIndex::AddOrUpdatePortableFile(const Portable::PortableFileEntry& file) { if (Exists(file)) { @@ -121,7 +98,7 @@ namespace AppInstaller::Repository::Microsoft } } - std::vector PortableIndex::GetAllPortableFiles() + std::vector PortableIndex::GetAllPortableFiles() { return m_interface->GetAllPortableFiles(m_dbconn); } diff --git a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h index 50fe2c27ae..2e4e63f639 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h @@ -5,8 +5,11 @@ #include "Microsoft/Schema/IPortableIndex.h" #include "Microsoft/Schema/Portable_1_0/PortableTable.h" #include "Microsoft/SQLiteStorageBase.h" +#include "winget/PortableFileEntry.h" #include +using namespace AppInstaller::Portable; + namespace AppInstaller::Repository::Microsoft { struct PortableIndex : SQLiteStorageBase @@ -29,19 +32,19 @@ namespace AppInstaller::Repository::Microsoft return { filePath, disposition, std::move(indexFile) }; } - static Schema::IPortableIndex::PortableFile CreatePortableFileFromPath(const std::filesystem::path& path); + static Portable::PortableFileEntry CreatePortableFileFromPath(const std::filesystem::path& path); - IdType AddPortableFile(const Schema::IPortableIndex::PortableFile& file); + IdType AddPortableFile(const Portable::PortableFileEntry& file); - void RemovePortableFile(const Schema::IPortableIndex::PortableFile& file); + void RemovePortableFile(const Portable::PortableFileEntry& file); - bool UpdatePortableFile(const Schema::IPortableIndex::PortableFile& file); + bool UpdatePortableFile(const Portable::PortableFileEntry& file); - void AddOrUpdatePortableFile(const Schema::IPortableIndex::PortableFile& file); + void AddOrUpdatePortableFile(const Portable::PortableFileEntry& file); - std::vector GetAllPortableFiles(); + std::vector GetAllPortableFiles(); - bool Exists(const Schema::IPortableIndex::PortableFile& file); + bool Exists(const Portable::PortableFileEntry& file); bool IsEmpty(); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/IPortableIndex.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/IPortableIndex.h index e0618d06d4..b14f278e56 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/IPortableIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/IPortableIndex.h @@ -3,37 +3,13 @@ #pragma once #include "SQLiteWrapper.h" #include "Microsoft/Schema/Version.h" +#include "winget/PortableFileEntry.h" #include namespace AppInstaller::Repository::Microsoft::Schema { struct IPortableIndex { - // File type enum of the portable file - enum class PortableFileType - { - Unknown, - File, - Directory, - Symlink - }; - - // Metadata representation of a portable file placed down during installation - struct PortableFile - { - // Version 1.0 - PortableFileType FileType = PortableFileType::Unknown; - std::string SHA256; - std::string SymlinkTarget; - - void SetFilePath(const std::filesystem::path& path) { m_filePath = std::filesystem::absolute(path); }; - - std::filesystem::path GetFilePath() const { return m_filePath; }; - - private: - std::filesystem::path m_filePath; - }; - virtual ~IPortableIndex() = default; // Gets the schema version that this index interface is built for. @@ -44,22 +20,22 @@ namespace AppInstaller::Repository::Microsoft::Schema // Version 1.0 // Adds a portable file to the index. - virtual SQLite::rowid_t AddPortableFile(SQLite::Connection& connection, const PortableFile& file) = 0; + virtual SQLite::rowid_t AddPortableFile(SQLite::Connection& connection, const Portable::PortableFileEntry& file) = 0; // Removes a portable file from the index. - virtual SQLite::rowid_t RemovePortableFile(SQLite::Connection& connection, const PortableFile& file) = 0; + virtual SQLite::rowid_t RemovePortableFile(SQLite::Connection& connection, const Portable::PortableFileEntry& file) = 0; // Updates the file with matching FilePath in the index. // The return value indicates whether the index was modified by the function. - virtual std::pair UpdatePortableFile(SQLite::Connection& connection, const PortableFile& file) = 0; + virtual std::pair UpdatePortableFile(SQLite::Connection& connection, const Portable::PortableFileEntry& file) = 0; // Returns a bool value indicating whether the PortableFile already exists in the index. - virtual bool Exists(SQLite::Connection& connection, const PortableFile& file) = 0; + virtual bool Exists(SQLite::Connection& connection, const Portable::PortableFileEntry& file) = 0; // Returns a bool value indicating whether the index is empty. virtual bool IsEmpty(SQLite::Connection& connection) = 0; // Returns a vector including all the portable files recorded in the index. - virtual std::vector GetAllPortableFiles(SQLite::Connection& connection) = 0; + virtual std::vector GetAllPortableFiles(SQLite::Connection& connection) = 0; }; } \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface.h index bc35cd8cca..ce97b2538f 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface.h @@ -13,11 +13,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 void CreateTable(SQLite::Connection& connection) override; private: - SQLite::rowid_t AddPortableFile(SQLite::Connection& connection, const PortableFile& file) override; - SQLite::rowid_t RemovePortableFile(SQLite::Connection& connection, const PortableFile& file) override; - std::pair UpdatePortableFile(SQLite::Connection& connection, const PortableFile& file) override; - bool Exists(SQLite::Connection& connection, const PortableFile& file) override; + SQLite::rowid_t AddPortableFile(SQLite::Connection& connection, const Portable::PortableFileEntry& file) override; + SQLite::rowid_t RemovePortableFile(SQLite::Connection& connection, const Portable::PortableFileEntry& file) override; + std::pair UpdatePortableFile(SQLite::Connection& connection, const Portable::PortableFileEntry& file) override; + bool Exists(SQLite::Connection& connection, const Portable::PortableFileEntry& file) override; bool IsEmpty(SQLite::Connection& connection) override; - std::vector GetAllPortableFiles(SQLite::Connection& connection) override; + std::vector GetAllPortableFiles(SQLite::Connection& connection) override; }; } \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface_1_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface_1_0.cpp index 5c78821117..a5ffac47db 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface_1_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface_1_0.cpp @@ -8,7 +8,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 { namespace { - std::optional GetExistingPortableFileId(const SQLite::Connection& connection, const IPortableIndex::PortableFile& file) + std::optional GetExistingPortableFileId(const SQLite::Connection& connection, const Portable::PortableFileEntry& file) { auto result = PortableTable::SelectByFilePath(connection, file.GetFilePath()); @@ -33,7 +33,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 savepoint.Commit(); } - SQLite::rowid_t PortableIndexInterface::AddPortableFile(SQLite::Connection& connection, const PortableFile& file) + SQLite::rowid_t PortableIndexInterface::AddPortableFile(SQLite::Connection& connection, const Portable::PortableFileEntry& file) { auto portableEntryResult = GetExistingPortableFileId(connection, file); @@ -46,7 +46,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 return portableFileId; } - SQLite::rowid_t PortableIndexInterface::RemovePortableFile(SQLite::Connection& connection, const PortableFile& file) + SQLite::rowid_t PortableIndexInterface::RemovePortableFile(SQLite::Connection& connection, const Portable::PortableFileEntry& file) { auto portableEntryResult = GetExistingPortableFileId(connection, file); @@ -60,7 +60,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 return portableEntryResult.value(); } - std::pair PortableIndexInterface::UpdatePortableFile(SQLite::Connection& connection, const PortableFile& file) + std::pair PortableIndexInterface::UpdatePortableFile(SQLite::Connection& connection, const Portable::PortableFileEntry& file) { auto portableEntryResult = GetExistingPortableFileId(connection, file); @@ -74,7 +74,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 return { status, portableEntryResult.value() }; } - bool PortableIndexInterface::Exists(SQLite::Connection& connection, const PortableFile& file) + bool PortableIndexInterface::Exists(SQLite::Connection& connection, const Portable::PortableFileEntry& file) { return GetExistingPortableFileId(connection, file).has_value(); } @@ -84,7 +84,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 return PortableTable::IsEmpty(connection); } - std::vector PortableIndexInterface::GetAllPortableFiles(SQLite::Connection& connection) + std::vector PortableIndexInterface::GetAllPortableFiles(SQLite::Connection& connection) { return PortableTable::GetAllPortableFiles(connection); } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp index 4442d53148..74538da2b5 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp @@ -64,7 +64,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 builder.Execute(connection); } - SQLite::rowid_t PortableTable::AddPortableFile(SQLite::Connection& connection, const IPortableIndex::PortableFile& file) + SQLite::rowid_t PortableTable::AddPortableFile(SQLite::Connection& connection, const Portable::PortableFileEntry& file) { SQLite::Builder::StatementBuilder builder; builder.InsertInto(s_PortableTable_Table_Name) @@ -78,7 +78,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 return connection.GetLastInsertRowID(); } - bool PortableTable::UpdatePortableFileById(SQLite::Connection& connection, SQLite::rowid_t id, const IPortableIndex::PortableFile& file) + bool PortableTable::UpdatePortableFileById(SQLite::Connection& connection, SQLite::rowid_t id, const Portable::PortableFileEntry& file) { SQLite::Builder::StatementBuilder builder; builder.Update(s_PortableTable_Table_Name).Set() @@ -92,7 +92,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 return connection.GetChanges() != 0; } - std::optional PortableTable::GetPortableFileById(const SQLite::Connection& connection, SQLite::rowid_t id) + std::optional PortableTable::GetPortableFileById(const SQLite::Connection& connection, SQLite::rowid_t id) { SQLite::Builder::StatementBuilder builder; builder.Select({ s_PortableTable_FilePath_Column, @@ -103,10 +103,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 SQLite::Statement select = builder.Prepare(connection); - IPortableIndex::PortableFile portableFile; + Portable::PortableFileEntry portableFile; if (select.Step()) { - auto [filePath, fileType, sha256, symlinkTarget] = select.GetRow(); + auto [filePath, fileType, sha256, symlinkTarget] = select.GetRow(); portableFile.SetFilePath(std::move(filePath)); portableFile.FileType = fileType; portableFile.SHA256 = std::move(sha256); @@ -151,7 +151,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 return (countStatement.GetColumn(0) == 0); } - std::vector PortableTable::GetAllPortableFiles(SQLite::Connection& connection) + std::vector PortableTable::GetAllPortableFiles(SQLite::Connection& connection) { SQLite::Builder::StatementBuilder builder; builder.Select({ s_PortableTable_FilePath_Column, @@ -161,16 +161,16 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 .From(s_PortableTable_Table_Name); SQLite::Statement select = builder.Prepare(connection); - std::vector result; + std::vector result; while (select.Step()) { - IPortableIndex::PortableFile portableFile; - auto [filePath, fileType, sha256, symlinkTarget] = select.GetRow(); + Portable::PortableFileEntry portableFile; + auto [filePath, fileType, sha256, symlinkTarget] = select.GetRow(); portableFile.SetFilePath(std::move(filePath)); portableFile.FileType = fileType; portableFile.SHA256 = std::move(sha256); portableFile.SymlinkTarget = std::move(symlinkTarget); - result.emplace_back(portableFile); + result.emplace_back(std::move(portableFile)); } return result; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.h index 479c2cc3be..3bd5c6a23c 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.h @@ -30,18 +30,18 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 static std::optional SelectByFilePath(const SQLite::Connection& connection, const std::filesystem::path& path); // Selects the portable file by rowid from the table, returning the portable file object if it exists. - static std::optional GetPortableFileById(const SQLite::Connection& connection, SQLite::rowid_t id); + static std::optional GetPortableFileById(const SQLite::Connection& connection, SQLite::rowid_t id); // Adds the portable file into the table. - static SQLite::rowid_t AddPortableFile(SQLite::Connection& connection, const IPortableIndex::PortableFile& file); + static SQLite::rowid_t AddPortableFile(SQLite::Connection& connection, const Portable::PortableFileEntry& file); // Removes the portable file from the table by id. static void RemovePortableFileById(SQLite::Connection& connection, SQLite::rowid_t id); // Updates the portable file in the table by id. - static bool UpdatePortableFileById(SQLite::Connection& connection, SQLite::rowid_t id, const IPortableIndex::PortableFile& file); + static bool UpdatePortableFileById(SQLite::Connection& connection, SQLite::rowid_t id, const Portable::PortableFileEntry& file); // Gets all portable files recorded in the index. - static std::vector GetAllPortableFiles(SQLite::Connection& connection); + static std::vector GetAllPortableFiles(SQLite::Connection& connection); }; } \ No newline at end of file From dee2a3bf689c9517d0f23b8a19799b034c566fb4 Mon Sep 17 00:00:00 2001 From: Ryan Fu Date: Fri, 16 Sep 2022 17:39:11 -0700 Subject: [PATCH 14/22] save work --- src/AppInstallerCLICore/PortableInstaller.cpp | 242 ++++-------------- src/AppInstallerCLICore/PortableInstaller.h | 32 ++- .../Workflows/PortableFlow.cpp | 223 +++++++--------- .../Workflows/PortableFlow.h | 6 - .../Public/winget/PortableFileEntry.h | 4 +- .../Schema/Portable_1_0/PortableTable.cpp | 20 +- 6 files changed, 171 insertions(+), 356 deletions(-) diff --git a/src/AppInstallerCLICore/PortableInstaller.cpp b/src/AppInstallerCLICore/PortableInstaller.cpp index 0974d5c3cf..1f6135f77d 100644 --- a/src/AppInstallerCLICore/PortableInstaller.cpp +++ b/src/AppInstallerCLICore/PortableInstaller.cpp @@ -25,39 +25,6 @@ namespace AppInstaller::CLI::Portable namespace { constexpr std::wstring_view c_PortableIndexFileName = L"portable.db"; - - void RemoveItemsFromIndex(const std::filesystem::path& indexPath) - { - bool deleteIndex = false; - { - PortableIndex portableIndex = PortableIndex::Open(indexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); - std::vector portableFiles = portableIndex.GetAllPortableFiles(); - - for (const auto& file : portableFiles) - { - const auto& filePath = file.GetFilePath(); - - if (std::filesystem::exists(filePath) || Filesystem::SymlinkExists(filePath)) - { - std::cout << filePath << std::endl; - std::filesystem::remove_all(filePath); - } - - portableIndex.RemovePortableFile(file); - } - - if (portableIndex.IsEmpty()) - { - deleteIndex = true; - }; - } - - if (deleteIndex) - { - AICLI_LOG(CLI, Info, << "Removing portable index: " << indexPath); - std::filesystem::remove(indexPath); - } - } } std::filesystem::path GetPortableLinksLocation(Manifest::ScopeEnum scope) @@ -127,7 +94,7 @@ namespace AppInstaller::CLI::Portable std::filesystem::remove(filePath); } - AICLI_LOG(Core, Info, << "Portable exe moved to: " << filePath); + AICLI_LOG(Core, Info, << "Moving portable exe to: " << filePath); if (!RecordToIndex) { @@ -139,14 +106,20 @@ namespace AppInstaller::CLI::Portable } else if (fileType == PortableFileType::Directory) { - if (!RecordToIndex) + if (entry.IsInstallDirectory) { CommitToARPEntry(PortableValueName::InstallLocation, filePath); - } - if (std::filesystem::create_directories(filePath)) + if (std::filesystem::create_directories(filePath)) + { + AICLI_LOG(Core, Info, << "Created target install directory: " << filePath); + CommitToARPEntry(PortableValueName::InstallDirectoryCreated, true); + } + } + else { - AICLI_LOG(Core, Info, << "Created target install directory: " << filePath); + AICLI_LOG(Core, Info, << "Moving directory to: " << filePath); + Filesystem::RenameFile(entry.CurrentPath, filePath); } } else if (entry.FileType == PortableFileType::Symlink) @@ -157,7 +130,7 @@ namespace AppInstaller::CLI::Portable if (std::filesystem::is_directory(status)) { AICLI_LOG(CLI, Info, << "Unable to create symlink. '" << symlinkPath << "points to an existing directory."); - //return APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY; + throw APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY; } if (!RecordToIndex) @@ -167,35 +140,31 @@ namespace AppInstaller::CLI::Portable PortableInstaller::CreatePortableSymlink(entry.SymlinkTarget, filePath); } - - if (RecordToIndex) - { - std::filesystem::path indexPath = GetPortableIndexPath(); - if (std::filesystem::exists(indexPath)) - { - AppInstaller::Repository::Microsoft::PortableIndex portableIndex = AppInstaller::Repository::Microsoft::PortableIndex::CreateNew(indexPath.u8string()); - portableIndex.AddOrUpdatePortableFile(entry); - } - else - { - AppInstaller::Repository::Microsoft::PortableIndex portableIndex = AppInstaller::Repository::Microsoft::PortableIndex::Open(indexPath.u8string(), AppInstaller::Repository::Microsoft::SQLiteStorageBase::OpenDisposition::ReadWrite); - portableIndex.AddOrUpdatePortableFile(entry); - } - } } - void PortableInstaller::RemoveFile(AppInstaller::Portable::PortableFileEntry& desiredState) + void PortableInstaller::RemoveFile(AppInstaller::Portable::PortableFileEntry& entry) { - const auto& filePath = desiredState.GetFilePath(); + const auto& filePath = entry.GetFilePath(); + PortableFileType fileType = entry.FileType; - if (std::filesystem::exists(filePath) || Filesystem::SymlinkExists(filePath)) + if (fileType == PortableFileType::File && std::filesystem::exists(filePath)) { - if (desiredState.FileType == PortableFileType::Directory && filePath == InstallLocation) + AICLI_LOG(CLI, Info, << "Deleting portable exe at: " << filePath); + std::filesystem::remove(filePath); + } + else if (fileType == PortableFileType::Symlink && Filesystem::SymlinkExists(filePath)) + { + AICLI_LOG(CLI, Info, << "Deleting portable symlink at: " << filePath); + std::filesystem::remove(filePath); + } + else if (fileType == PortableFileType::Directory && std::filesystem::exists(filePath)) + { + if (entry.IsInstallDirectory) { if (Purge) { m_stream << Resource::String::PurgeInstallDirectory << std::endl; - const auto& removedFilesCount = std::filesystem::remove_all(InstallLocation); + const auto& removedFilesCount = std::filesystem::remove_all(filePath); AICLI_LOG(CLI, Info, << "Purged install location directory. Deleted " << removedFilesCount << " files or directories"); } else @@ -207,6 +176,7 @@ namespace AppInstaller::CLI::Portable if (item.path() != indexPath) { isDirectoryEmpty = false; + break; } } @@ -224,36 +194,30 @@ namespace AppInstaller::CLI::Portable } else { + AICLI_LOG(CLI, Info, << "Removing directory at " << filePath); std::filesystem::remove_all(filePath); - - if (RecordToIndex) - { - std::filesystem::path indexPath = GetPortableIndexPath(); - PortableIndex index = PortableIndex::Open(indexPath.u8string(), AppInstaller::Repository::Microsoft::SQLiteStorageBase::OpenDisposition::Read); - index.RemovePortableFile(desiredState); -; } } } } - void PortableInstaller::ResolutionEngine(std::vector& desiredState, std::vector& expectedState) + void PortableInstaller::ApplyDesiredState() { - // Remove all entries from expected state first, then perform install. If this is an uninstall, desiredState will be empty - for (auto entry : expectedState) + // TODO: Record to index if applicable. + + for (auto expectedEntry : m_expectedEntries) { - RemoveFile(entry); + RemoveFile(expectedEntry); } - for (auto entry : desiredState) + for (auto desiredEntry : m_desiredEntries) { - // for each one, install and record - InstallFile(entry); + InstallFile(desiredEntry); } } - bool PortableInstaller::VerifyResolution(std::vector& expectedState) + bool PortableInstaller::VerifyExpectedState() { - for (auto entry : expectedState) + for (auto entry : m_expectedEntries) { if (!VerifyPortableFile(entry)) { @@ -265,133 +229,20 @@ namespace AppInstaller::CLI::Portable return true; } - // get desired state and expected state from context - // - - std::vector PortableInstaller::GetDesiredState(std::vector files) - { - std::vector entries; - - for (const auto file : files) - { - AppInstaller::Portable::PortableFileEntry portableFile; - - if (std::filesystem::is_directory(file)) - { - portableFile.SetFilePath(file); - portableFile.FileType = PortableFileType::Directory; - } - else - { - std::filesystem::path relativePath = std::filesystem::relative(file, file.parent_path()); - portableFile.CurrentPath = file; - portableFile.SetFilePath(InstallLocation / relativePath); - portableFile.FileType = PortableFileType::File; - portableFile.SHA256 = Utility::SHA256::ConvertToString(Utility::SHA256::ComputeHashFromFile(file)); - } - - entries.emplace_back(std::move(portableFile)); - - if (portableFile.FileType == PortableFileType::File) - { - AppInstaller::Portable::PortableFileEntry symlinkEntry; - symlinkEntry.SetFilePath(PortableSymlinkFullPath); - symlinkEntry.SymlinkTarget = PortableTargetFullPath.u8string(); - symlinkEntry.FileType = PortableFileType::Symlink; - entries.emplace_back(symlinkEntry); - } - } - - return entries; - } - - std::vector PortableInstaller::GetExpectedState() - { - std::vector entries; - const auto& indexPath = GetPortableIndexPath(); - if (std::filesystem::exists(indexPath)) - { - PortableIndex portableIndex = PortableIndex::Open(indexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); - entries = portableIndex.GetAllPortableFiles(); - } - else - { - if (!PortableSymlinkFullPath.empty()) - { - AppInstaller::Portable::PortableFileEntry symlinkEntry; - symlinkEntry.SetFilePath(PortableSymlinkFullPath); - symlinkEntry.SymlinkTarget = PortableTargetFullPath.u8string(); - symlinkEntry.FileType = PortableFileType::Symlink; - entries.emplace_back(symlinkEntry); - } - - if (!PortableTargetFullPath.empty()) - { - AppInstaller::Portable::PortableFileEntry exeEntry; - exeEntry.SetFilePath(PortableTargetFullPath); - exeEntry.SHA256 = SHA256; - exeEntry.FileType = PortableFileType::File; - entries.emplace_back(exeEntry); - } - - if (!InstallLocation.empty() && InstallDirectoryCreated) - { - AppInstaller::Portable::PortableFileEntry directoryEntry; - directoryEntry.SetFilePath(InstallLocation); - directoryEntry.FileType = PortableFileType::Directory; - entries.emplace_back(directoryEntry); - } - } - - return entries; - } - - HRESULT PortableInstaller::Install(const std::filesystem::path& installerPath) + HRESULT PortableInstaller::Install() { InitializeRegistryEntry(); - - std::vector installFiles; - if (std::filesystem::is_directory(installerPath)) - { - for (const auto& entry : std::filesystem::directory_iterator(installerPath)) - { - installFiles.emplace_back(entry.path()); - } - } - else - { - installFiles.emplace_back(installerPath); - } - - - std::vector desiredEntries = GetDesiredState(installFiles); - std::vector expectedEntries = GetExpectedState(); - - if (VerifyResolution(expectedEntries)) - { - // Return that files have been modified - } - - ResolutionEngine(desiredEntries, expectedEntries); + + ApplyDesiredState(); AddToPathVariable(); - FinalizeRegistryEntry(); - return ERROR_SUCCESS; } HRESULT PortableInstaller::Uninstall() { - std::vector desiredEntries; - std::vector expectedEntries = GetExpectedState(); - - if (VerifyResolution(expectedEntries)) - { - // Return that files have been modified - } - - ResolutionEngine(desiredEntries, expectedEntries); + ApplyDesiredState(); RemoveFromPathVariable(); @@ -556,7 +407,6 @@ namespace AppInstaller::CLI::Portable InstallLocation = GetPathValue(PortableValueName::InstallLocation); PortableSymlinkFullPath = GetPathValue(PortableValueName::PortableSymlinkFullPath); PortableTargetFullPath = GetPathValue(PortableValueName::PortableTargetFullPath); - InstallLocation = GetPathValue(PortableValueName::InstallLocation); InstallDirectoryAddedToPath = GetBoolValue(PortableValueName::InstallDirectoryAddedToPath); InstallDirectoryCreated = GetBoolValue(PortableValueName::InstallDirectoryCreated); } @@ -568,12 +418,6 @@ namespace AppInstaller::CLI::Portable CommitToARPEntry(PortableValueName::WinGetSourceIdentifier, WinGetSourceIdentifier); CommitToARPEntry(PortableValueName::UninstallString, "winget uninstall --product-code " + GetProductCode()); CommitToARPEntry(PortableValueName::WinGetInstallerType, InstallerTypeToString(Manifest::InstallerTypeEnum::Portable)); - CommitToARPEntry(PortableValueName::SHA256, SHA256); - CommitToARPEntry(PortableValueName::InstallLocation, InstallLocation); - } - - void PortableInstaller::FinalizeRegistryEntry() - { CommitToARPEntry(PortableValueName::DisplayName, DisplayName); CommitToARPEntry(PortableValueName::DisplayVersion, DisplayVersion); CommitToARPEntry(PortableValueName::Publisher, Publisher); diff --git a/src/AppInstallerCLICore/PortableInstaller.h b/src/AppInstallerCLICore/PortableInstaller.h index 4c1f2e6c3b..d2d55ad149 100644 --- a/src/AppInstallerCLICore/PortableInstaller.h +++ b/src/AppInstallerCLICore/PortableInstaller.h @@ -16,6 +16,7 @@ namespace AppInstaller::CLI::Portable // Object representation of the metadata and functionality required for installing a Portable package. struct PortableInstaller { + // These values are initialized based on the values from the entry in ARP std::string DisplayName; std::string DisplayVersion; std::string HelpLink; @@ -30,17 +31,15 @@ namespace AppInstaller::CLI::Portable std::string WinGetInstallerType; std::string WinGetPackageIdentifier; std::string WinGetSourceIdentifier; - bool IsUpdate = false; - bool Purge = false; bool InstallDirectoryCreated = false; // If we fail to create a symlink, add install directory to PATH variable bool InstallDirectoryAddedToPath = false; - bool RecordToIndex = false; - std::string Rename; - std::string CommandAlias; + bool IsUpdate = false; + bool Purge = false; + bool RecordToIndex = false; - std::filesystem::path installerPathDirectory; + std::filesystem::path TargetInstallDirectory; void InstallFile(AppInstaller::Portable::PortableFileEntry& desiredState); @@ -48,15 +47,20 @@ namespace AppInstaller::CLI::Portable PortableInstaller(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode); - std::vector GetDesiredState(std::vector files); - - std::vector GetExpectedState(); + // Ensures no modifications have been made that conflict with the expected state. + bool VerifyExpectedState(); - void ResolutionEngine(std::vector& desiredState, std::vector& expectedState); + void SetDesiredState(std::vector& desiredEntries) + { + m_desiredEntries = desiredEntries; + }; - bool VerifyResolution(std::vector& expectedState); + void SetExpectedState(std::vector& expectedEntries) + { + m_expectedEntries = expectedEntries; + }; - HRESULT Install(const std::filesystem::path& installerPath); + HRESULT Install(); HRESULT Uninstall(); @@ -92,6 +96,8 @@ namespace AppInstaller::CLI::Portable private: PortableARPEntry m_portableARPEntry; + std::vector m_desiredEntries; + std::vector m_expectedEntries; std::stringstream m_stream; std::string GetStringValue(PortableValueName valueName); @@ -101,6 +107,8 @@ namespace AppInstaller::CLI::Portable void InitializeRegistryEntry(); void FinalizeRegistryEntry(); + void ApplyDesiredState(); + void MovePortableExe(const std::filesystem::path& installerPath); bool CreatePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath); diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp index 5d4a64521b..0eaa58a487 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -13,6 +13,7 @@ using namespace AppInstaller::Repository; using namespace AppInstaller::Utility; using namespace AppInstaller::CLI::Portable; using namespace AppInstaller::Portable; +using namespace AppInstaller::Repository::Microsoft; namespace AppInstaller::CLI::Workflow { @@ -37,54 +38,6 @@ namespace AppInstaller::CLI::Workflow return MakeSuitablePathPart(packageId + "_" + source); } - std::filesystem::path GetPortableTargetFullPath(Execution::Context& context) - { - const std::filesystem::path& installerPath = context.Get(); - const std::filesystem::path& targetInstallDirectory = GetPortableTargetDirectory(context); - std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); - - std::filesystem::path fileName; - if (!renameArg.empty()) - { - fileName = ConvertToUTF16(renameArg); - } - else - { - fileName = installerPath.filename(); - } - - AppInstaller::Filesystem::AppendExtension(fileName, ".exe"); - return targetInstallDirectory / fileName; - } - - std::filesystem::path GetPortableSymlinkFullPath(Execution::Context& context) - { - const std::filesystem::path& installerPath = context.Get(); - const std::vector& commands = context.Get()->Commands; - Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); - std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); - - std::filesystem::path commandAlias; - if (!renameArg.empty()) - { - commandAlias = ConvertToUTF16(renameArg); - } - else - { - if (!commands.empty()) - { - commandAlias = ConvertToUTF16(commands[0]); - } - else - { - commandAlias = installerPath.filename(); - } - } - - AppInstaller::Filesystem::AppendExtension(commandAlias, ".exe"); - return GetPortableLinksLocation(scope) / commandAlias; - } - void EnsureValidArgsForPortableInstall(Execution::Context& context) { std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); @@ -127,29 +80,6 @@ namespace AppInstaller::CLI::Workflow } } - std::filesystem::path GetPortableTargetDirectory(Execution::Context& context) - { - Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); - Utility::Architecture arch = context.Get()->Arch; - std::string_view locationArg = context.Args.GetArg(Execution::Args::Type::InstallLocation); - std::filesystem::path targetInstallDirectory; - - if (!locationArg.empty()) - { - targetInstallDirectory = std::filesystem::path{ ConvertToUTF16(locationArg) }; - } - else - { - - targetInstallDirectory = GetPortableInstallRoot(scope, arch); - } - - const std::string& productCode = GetPortableProductCode(context); - targetInstallDirectory /= ConvertToUTF16(productCode); - - return targetInstallDirectory; - } - void VerifyPackageAndSourceMatch(Execution::Context& context) { const std::string& packageIdentifier = context.Get().Id; @@ -210,16 +140,23 @@ namespace AppInstaller::CLI::Workflow const std::string& productCode = GetPortableProductCode(context); PortableInstaller portableInstaller = PortableInstaller(scope, arch, productCode); - portableInstaller.SHA256 = Utility::SHA256::ConvertToString(context.Get().second); - portableInstaller.InstallLocation = GetPortableTargetDirectory(context); portableInstaller.IsUpdate = isUpdate; - if (!IsArchiveType(context.Get()->BaseInstallerType)) - { - portableInstaller.PortableTargetFullPath = GetPortableTargetFullPath(context); - portableInstaller.PortableSymlinkFullPath = GetPortableSymlinkFullPath(context); + // Set target install directory + std::string_view locationArg = context.Args.GetArg(Execution::Args::Type::InstallLocation); + std::filesystem::path targetInstallDirectory; + + if (!locationArg.empty()) + { + targetInstallDirectory = std::filesystem::path{ ConvertToUTF16(locationArg) }; + } + else + { + targetInstallDirectory = GetPortableInstallRoot(scope, arch); + targetInstallDirectory /= ConvertToUTF16(productCode); } + portableInstaller.TargetInstallDirectory = targetInstallDirectory; portableInstaller.SetAppsAndFeaturesMetadata(context.Get(), context.Get()->AppsAndFeaturesEntries); context.Add(std::move(portableInstaller)); } @@ -236,31 +173,32 @@ namespace AppInstaller::CLI::Workflow // 3. Symlinks // Uninstall should process these portable files in the reverse order for proper cleanup. - const std::filesystem::path& installLocation = GetPortableTargetDirectory(context); - entries.emplace_back(std::move(PortableFileEntry::CreateDirectoryEntry(installLocation))); + const std::filesystem::path& symlinkDirectory = GetPortableLinksLocation(portableInstaller.GetScope()); + const std::filesystem::path& targetInstallDirectory = portableInstaller.TargetInstallDirectory; + entries.emplace_back(std::move(PortableFileEntry::CreateDirectoryEntry(targetInstallDirectory, true))); + // InstallerPath will point to a directory if it is extracted from an archive. if (std::filesystem::is_directory(installerPath)) { - std::vector directoryEntries; - std::vector fileEntries; - + int entryCount = 0; for (const auto& entry : std::filesystem::directory_iterator(installerPath)) { std::filesystem::path entryPath = entry.path(); PortableFileEntry portableFile; if (std::filesystem::is_directory(entryPath)) { - directoryEntries.emplace_back(std::move(PortableFileEntry::CreateDirectoryEntry(entryPath))); + entries.emplace_back(std::move(PortableFileEntry::CreateDirectoryEntry(entryPath))); } else { std::filesystem::path relativePath = std::filesystem::relative(entryPath, entryPath.parent_path()); - fileEntries.emplace_back(std::move(PortableFileEntry::CreateFileEntry(entryPath, installLocation / relativePath))); + entries.emplace_back(std::move(PortableFileEntry::CreateFileEntry(entryPath, targetInstallDirectory / relativePath))); } + + entryCount++; } - // If there is more than one portable file, record to index. - if (fileEntries.size() > 1) + if (entryCount > 1) { portableInstaller.RecordToIndex = true; } @@ -269,27 +207,54 @@ namespace AppInstaller::CLI::Workflow for (const auto& nestedInstallerFile : nestedInstallerFiles) { - const std::filesystem::path& targetPath = installLocation / ConvertToUTF16(nestedInstallerFile.RelativeFilePath); + const std::filesystem::path& targetPath = targetInstallDirectory / ConvertToUTF16(nestedInstallerFile.RelativeFilePath); std::filesystem::path commandAlias; - if (!nestedInstallerFile.PortableCommandAlias.empty()) + if (nestedInstallerFile.PortableCommandAlias.empty()) { - commandAlias = ConvertToUTF16(nestedInstallerFile.PortableCommandAlias); + commandAlias = targetPath.filename(); } else { - commandAlias = targetPath.filename(); + commandAlias = ConvertToUTF16(nestedInstallerFile.PortableCommandAlias); } Filesystem::AppendExtension(commandAlias, ".exe"); - const std::filesystem::path& symlinkFullPath = GetPortableLinksLocation(portableInstaller.GetScope()) / commandAlias; + const std::filesystem::path& symlinkFullPath = symlinkDirectory / commandAlias; entries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkFullPath, targetPath))); } } else { - const std::filesystem::path& targetFullPath = GetPortableTargetFullPath(context); - const std::filesystem::path& symlinkFullPath = GetPortableSymlinkFullPath(context); + std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); + const std::vector& commands = context.Get()->Commands; + + std::filesystem::path fileName; + std::filesystem::path commandAlias; + + if (!renameArg.empty()) + { + fileName = commandAlias = ConvertToUTF16(renameArg); + } + else + { + if (!commands.empty()) + { + commandAlias = ConvertToUTF16(commands[0]); + } + else + { + commandAlias = installerPath.filename(); + } + + fileName = installerPath.filename(); + } + + AppInstaller::Filesystem::AppendExtension(fileName, ".exe"); + AppInstaller::Filesystem::AppendExtension(commandAlias, ".exe"); + + const std::filesystem::path& targetFullPath = targetInstallDirectory / fileName; + const std::filesystem::path& symlinkFullPath = symlinkDirectory / commandAlias; entries.emplace_back(std::move(PortableFileEntry::CreateFileEntry(installerPath, targetFullPath))); entries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkFullPath, targetFullPath))); } @@ -300,12 +265,13 @@ namespace AppInstaller::CLI::Workflow std::vector GetExpectedState(Execution::Context& context) { PortableInstaller& portableInstaller = context.Get(); - std::vector entries; + const auto& indexPath = portableInstaller.GetPortableIndexPath(); + if (std::filesystem::exists(indexPath)) { - AppInstaller::Repository::Microsoft::PortableIndex portableIndex = AppInstaller::Repository::Microsoft::PortableIndex::Open(indexPath.u8string(), AppInstaller::Repository::Microsoft::SQLiteStorageBase::OpenDisposition::ReadWrite); + PortableIndex portableIndex = PortableIndex::Open(indexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); entries = portableIndex.GetAllPortableFiles(); } else @@ -314,30 +280,19 @@ namespace AppInstaller::CLI::Workflow std::filesystem::path symlinkFullPath = portableInstaller.PortableSymlinkFullPath; std::filesystem::path installLocation = portableInstaller.InstallLocation; - if (!portableInstaller.PortableSymlinkFullPath.empty()) + if (!symlinkFullPath.empty()) { - AppInstaller::Portable::PortableFileEntry symlinkEntry; - symlinkEntry.SetFilePath(symlinkFullPath); - symlinkEntry.SymlinkTarget = targetFullPath.u8string(); - symlinkEntry.FileType = PortableFileType::Symlink; - entries.emplace_back(symlinkEntry); + entries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkFullPath, targetFullPath))); } if (!targetFullPath.empty()) { - AppInstaller::Portable::PortableFileEntry exeEntry; - exeEntry.SetFilePath(targetFullPath); - exeEntry.SHA256 = portableInstaller.SHA256; - exeEntry.FileType = PortableFileType::File; - entries.emplace_back(exeEntry); + entries.emplace_back(std::move(PortableFileEntry::CreateFileEntry({}, targetFullPath))); } if (!installLocation.empty() && portableInstaller.InstallDirectoryCreated) { - AppInstaller::Portable::PortableFileEntry directoryEntry; - directoryEntry.SetFilePath(installLocation); - directoryEntry.FileType = PortableFileType::Directory; - entries.emplace_back(directoryEntry); + entries.emplace_back(std::move(PortableFileEntry::CreateDirectoryEntry(installLocation))); } } @@ -357,19 +312,23 @@ namespace AppInstaller::CLI::Workflow std::vector desiredState = GetDesiredStateForPortableInstall(context); std::vector expectedState = GetExpectedState(context); - //portableInstaller.InitializeRegistryEntry(); + portableInstaller.SetDesiredState(desiredState); + portableInstaller.SetExpectedState(expectedState); - if (portableInstaller.VerifyResolution(expectedState)) + if (!portableInstaller.VerifyExpectedState()) { - // Return that files have been modified + if (context.Args.Contains(Execution::Args::Type::HashOverride)) + { + context.Reporter.Warn() << Resource::String::PortableHashMismatchOverridden << std::endl; + } + else + { + context.Reporter.Warn() << Resource::String::PortableHashMismatchOverrideRequired << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED); + } } - portableInstaller.ResolutionEngine(desiredState, expectedState); - - //portableInstaller.AddToPathVariable(); - - //FinalizeRegistryEntry(); - + portableInstaller.Install(); context.Add(result); context.Reporter.Warn() << portableInstaller.GetOutputMessage(); @@ -394,19 +353,19 @@ namespace AppInstaller::CLI::Workflow { context.Reporter.Info() << Resource::String::UninstallFlowStartingPackageUninstall << std::endl; - //if (!portableInstaller.VerifyResolution()) - //{ - // // TODO: replace with appropriate --force argument when available. - // if (context.Args.Contains(Execution::Args::Type::HashOverride)) - // { - // context.Reporter.Warn() << Resource::String::PortableHashMismatchOverridden << std::endl; - // } - // else - // { - // context.Reporter.Warn() << Resource::String::PortableHashMismatchOverrideRequired << std::endl; - // AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED); - // } - //} + if (!portableInstaller.VerifyExpectedState()) + { + // TODO: replace with appropriate --force argument when available. + if (context.Args.Contains(Execution::Args::Type::HashOverride)) + { + context.Reporter.Warn() << Resource::String::PortableHashMismatchOverridden << std::endl; + } + else + { + context.Reporter.Warn() << Resource::String::PortableHashMismatchOverrideRequired << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED); + } + } portableInstaller.Purge = context.Args.Contains(Execution::Args::Type::Purge) || (!portableInstaller.IsUpdate && Settings::User().Get() && !context.Args.Contains(Execution::Args::Type::Preserve)); diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.h b/src/AppInstallerCLICore/Workflows/PortableFlow.h index 3dc6c28bdb..4b1c1032ca 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.h +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.h @@ -40,10 +40,4 @@ namespace AppInstaller::CLI::Workflow // Inputs: Manifest, PackageVersion, PortableInstaller // Outputs: None void VerifyPackageAndSourceMatch(Execution::Context& context); - - // Returns the target install directory for portable installation. - // Required Args: None - // Inputs: Scope, Arch, InstallLocation - // Outputs: Path - std::filesystem::path GetPortableTargetDirectory(Execution::Context& context); } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h b/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h index 91f17f7e16..6b3e69ee3e 100644 --- a/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h +++ b/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h @@ -24,6 +24,7 @@ namespace AppInstaller::Portable std::string SHA256; std::string SymlinkTarget; std::filesystem::path CurrentPath; + bool IsInstallDirectory = false; void SetFilePath(const std::filesystem::path& path) { m_filePath = std::filesystem::weakly_canonical(path); }; @@ -48,11 +49,12 @@ namespace AppInstaller::Portable return symlinkEntry; } - static PortableFileEntry CreateDirectoryEntry(const std::filesystem::path& directoryPath) + static PortableFileEntry CreateDirectoryEntry(const std::filesystem::path& directoryPath, bool isInstallDirectory = false) { PortableFileEntry directoryEntry; directoryEntry.SetFilePath(directoryPath); directoryEntry.FileType = PortableFileType::Directory; + directoryEntry.IsInstallDirectory = isInstallDirectory; return directoryEntry; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp index 74538da2b5..4ffb56bfff 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp @@ -13,6 +13,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 static constexpr std::string_view s_PortableTable_FileType_Column = "filetype"sv; static constexpr std::string_view s_PortableTable_SHA256_Column = "sha256"sv; static constexpr std::string_view s_PortableTable_SymlinkTarget_Column = "symlinktarget"sv; + static constexpr std::string_view s_PortableTable_IsInstallDirectory_Column = "isinstalldirectory"sv; std::string_view PortableTable::TableName() { @@ -32,6 +33,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 createTableBuilder.Column(ColumnBuilder(s_PortableTable_FileType_Column, Type::Int64).NotNull()); createTableBuilder.Column(ColumnBuilder(s_PortableTable_SHA256_Column, Type::Blob)); createTableBuilder.Column(ColumnBuilder(s_PortableTable_SymlinkTarget_Column, Type::Text)); + createTableBuilder.Column(ColumnBuilder(s_PortableTable_IsInstallDirectory_Column, Type::Bool)); createTableBuilder.EndColumns(); createTableBuilder.Execute(connection); @@ -71,8 +73,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 .Columns({ s_PortableTable_FilePath_Column, s_PortableTable_FileType_Column, s_PortableTable_SHA256_Column, - s_PortableTable_SymlinkTarget_Column }) - .Values(file.GetFilePath().u8string(), file.FileType, file.SHA256, file.SymlinkTarget); + s_PortableTable_SymlinkTarget_Column, + s_PortableTable_IsInstallDirectory_Column}) + .Values(file.GetFilePath().u8string(), file.FileType, file.SHA256, file.SymlinkTarget, file.IsInstallDirectory); builder.Execute(connection); return connection.GetLastInsertRowID(); @@ -86,6 +89,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 .Column(s_PortableTable_FileType_Column).Equals(file.FileType) .Column(s_PortableTable_SHA256_Column).Equals(file.SHA256) .Column(s_PortableTable_SymlinkTarget_Column).Equals(file.SymlinkTarget) + .Column(s_PortableTable_IsInstallDirectory_Column).Equals(file.IsInstallDirectory) .Where(SQLite::RowIDName).Equals(id); builder.Execute(connection); @@ -98,7 +102,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 builder.Select({ s_PortableTable_FilePath_Column, s_PortableTable_FileType_Column, s_PortableTable_SHA256_Column, - s_PortableTable_SymlinkTarget_Column}) + s_PortableTable_SymlinkTarget_Column, + s_PortableTable_IsInstallDirectory_Column}) .From(s_PortableTable_Table_Name).Where(SQLite::RowIDName).Equals(id); SQLite::Statement select = builder.Prepare(connection); @@ -106,11 +111,12 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 Portable::PortableFileEntry portableFile; if (select.Step()) { - auto [filePath, fileType, sha256, symlinkTarget] = select.GetRow(); + auto [filePath, fileType, sha256, symlinkTarget, isInstallDirectory] = select.GetRow(); portableFile.SetFilePath(std::move(filePath)); portableFile.FileType = fileType; portableFile.SHA256 = std::move(sha256); portableFile.SymlinkTarget = std::move(symlinkTarget); + portableFile.IsInstallDirectory = isInstallDirectory; return portableFile; } else @@ -157,7 +163,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 builder.Select({ s_PortableTable_FilePath_Column, s_PortableTable_FileType_Column, s_PortableTable_SHA256_Column, - s_PortableTable_SymlinkTarget_Column }) + s_PortableTable_SymlinkTarget_Column, + s_PortableTable_IsInstallDirectory_Column }) .From(s_PortableTable_Table_Name); SQLite::Statement select = builder.Prepare(connection); @@ -165,11 +172,12 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 while (select.Step()) { Portable::PortableFileEntry portableFile; - auto [filePath, fileType, sha256, symlinkTarget] = select.GetRow(); + auto [filePath, fileType, sha256, symlinkTarget, isInstallDirectory] = select.GetRow(); portableFile.SetFilePath(std::move(filePath)); portableFile.FileType = fileType; portableFile.SHA256 = std::move(sha256); portableFile.SymlinkTarget = std::move(symlinkTarget); + portableFile.IsInstallDirectory = isInstallDirectory; result.emplace_back(std::move(portableFile)); } From d292891e20913e474bf6b5bd71c4098d02818a31 Mon Sep 17 00:00:00 2001 From: Ryan Fu Date: Mon, 19 Sep 2022 16:34:23 -0700 Subject: [PATCH 15/22] finish work for archive --- src/AppInstallerCLICore/PortableInstaller.cpp | 226 ++++++++------ src/AppInstallerCLICore/PortableInstaller.h | 18 +- .../Workflows/PortableFlow.cpp | 59 +--- .../PortableInstaller.cpp | 289 ++++++------------ src/AppInstallerCLITests/WorkFlow.cpp | 2 + .../Public/winget/PortableFileEntry.h | 36 ++- .../Microsoft/PortableIndex.cpp | 3 + .../Microsoft/PortableIndex.h | 2 - .../Schema/Portable_1_0/PortableTable.cpp | 24 +- 9 files changed, 265 insertions(+), 394 deletions(-) diff --git a/src/AppInstallerCLICore/PortableInstaller.cpp b/src/AppInstallerCLICore/PortableInstaller.cpp index 1f6135f77d..be61aab286 100644 --- a/src/AppInstallerCLICore/PortableInstaller.cpp +++ b/src/AppInstallerCLICore/PortableInstaller.cpp @@ -25,6 +25,15 @@ namespace AppInstaller::CLI::Portable namespace { constexpr std::wstring_view c_PortableIndexFileName = L"portable.db"; + + PortableIndex CreateOrOpenPortableIndex(const std::filesystem::path& indexPath) + { + PortableIndex index = std::filesystem::exists(indexPath) ? + PortableIndex::Open(indexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite) : + PortableIndex::CreateNew(indexPath.u8string()); + + return index; + } } std::filesystem::path GetPortableLinksLocation(Manifest::ScopeEnum scope) @@ -90,7 +99,7 @@ namespace AppInstaller::CLI::Portable { if (std::filesystem::exists(filePath)) { - AICLI_LOG(Core, Info, << "Removing existing portable exe at: " << filePath); + AICLI_LOG(Core, Info, << "Removing existing portable file at: " << filePath); std::filesystem::remove(filePath); } @@ -106,23 +115,10 @@ namespace AppInstaller::CLI::Portable } else if (fileType == PortableFileType::Directory) { - if (entry.IsInstallDirectory) - { - CommitToARPEntry(PortableValueName::InstallLocation, filePath); - - if (std::filesystem::create_directories(filePath)) - { - AICLI_LOG(Core, Info, << "Created target install directory: " << filePath); - CommitToARPEntry(PortableValueName::InstallDirectoryCreated, true); - } - } - else - { - AICLI_LOG(Core, Info, << "Moving directory to: " << filePath); - Filesystem::RenameFile(entry.CurrentPath, filePath); - } + AICLI_LOG(Core, Info, << "Moving directory to: " << filePath); + Filesystem::RenameFile(entry.CurrentPath, filePath); } - else if (entry.FileType == PortableFileType::Symlink) + else if (entry.FileType == PortableFileType::Symlink && !InstallDirectoryAddedToPath) { const std::filesystem::path& symlinkPath = entry.SymlinkTarget; @@ -159,59 +155,64 @@ namespace AppInstaller::CLI::Portable } else if (fileType == PortableFileType::Directory && std::filesystem::exists(filePath)) { - if (entry.IsInstallDirectory) + AICLI_LOG(CLI, Info, << "Removing directory at " << filePath); + std::filesystem::remove_all(filePath); + } + } + + void PortableInstaller::ApplyDesiredState() + { + std::filesystem::path existingIndexPath = InstallLocation / c_PortableIndexFileName; + if (std::filesystem::exists(existingIndexPath)) + { + bool deleteIndex = false; { - if (Purge) + PortableIndex existingIndex = CreateOrOpenPortableIndex(existingIndexPath); + for (auto expectedEntry : m_expectedEntries) { - m_stream << Resource::String::PurgeInstallDirectory << std::endl; - const auto& removedFilesCount = std::filesystem::remove_all(filePath); - AICLI_LOG(CLI, Info, << "Purged install location directory. Deleted " << removedFilesCount << " files or directories"); - } - else - { - std::filesystem::path indexPath = GetPortableIndexPath(); - bool isDirectoryEmpty = true; - for (const auto& item : std::filesystem::directory_iterator(filePath)) - { - if (item.path() != indexPath) - { - isDirectoryEmpty = false; - break; - } - } - - if (isDirectoryEmpty) - { - AICLI_LOG(CLI, Info, << "Removing empty install directory: " << filePath); - std::filesystem::remove(filePath); - } - else - { - AICLI_LOG(CLI, Info, << "Unable to remove install directory as there are remaining files in: " << filePath); - m_stream << Resource::String::FilesRemainInInstallDirectory << filePath << std::endl; - } + RemoveFile(expectedEntry); + existingIndex.RemovePortableFile(expectedEntry); } + + deleteIndex = existingIndex.IsEmpty(); } - else + + if (deleteIndex) { - AICLI_LOG(CLI, Info, << "Removing directory at " << filePath); - std::filesystem::remove_all(filePath); + std::filesystem::remove(existingIndexPath); + AICLI_LOG(CLI, Info, << "Portable index deleted: " << existingIndexPath); + } + } + else + { + for (auto expectedEntry : m_expectedEntries) + { + RemoveFile(expectedEntry); } } - } - - void PortableInstaller::ApplyDesiredState() - { - // TODO: Record to index if applicable. - for (auto expectedEntry : m_expectedEntries) + // Check if existing install location differs from the target install location for proper cleanup. + if (!InstallLocation.empty() && TargetInstallDirectory != InstallLocation) { - RemoveFile(expectedEntry); + RemoveInstallDirectory(); } - for (auto desiredEntry : m_desiredEntries) + if (RecordToIndex) + { + std::filesystem::path targetIndexPath = TargetInstallDirectory / c_PortableIndexFileName; + PortableIndex index = CreateOrOpenPortableIndex(targetIndexPath); + for (auto desiredEntry : m_desiredEntries) + { + index.AddOrUpdatePortableFile(desiredEntry); + InstallFile(desiredEntry); + } + } + else { - InstallFile(desiredEntry); + for (auto desiredEntry : m_desiredEntries) + { + InstallFile(desiredEntry); + } } } @@ -232,6 +233,8 @@ namespace AppInstaller::CLI::Portable HRESULT PortableInstaller::Install() { InitializeRegistryEntry(); + + CreateInstallDirectory(); ApplyDesiredState(); @@ -244,6 +247,8 @@ namespace AppInstaller::CLI::Portable { ApplyDesiredState(); + RemoveInstallDirectory(); + RemoveFromPathVariable(); m_portableARPEntry.Delete(); @@ -252,27 +257,41 @@ namespace AppInstaller::CLI::Portable return ERROR_SUCCESS; } - void PortableInstaller::MovePortableExe(const std::filesystem::path& installerPath) + void PortableInstaller::CreateInstallDirectory() { - bool isDirectoryCreated = false; - - if (std::filesystem::create_directories(InstallLocation)) + if (std::filesystem::create_directories(TargetInstallDirectory)) { - AICLI_LOG(Core, Info, << "Created target install directory: " << InstallLocation); - isDirectoryCreated = true; + AICLI_LOG(Core, Info, << "Created target install directory: " << TargetInstallDirectory); + CommitToARPEntry(PortableValueName::InstallDirectoryCreated, true); } - - if (std::filesystem::exists(PortableTargetFullPath)) - { - AICLI_LOG(Core, Info, << "Removing existing portable exe at: " << PortableTargetFullPath); - std::filesystem::remove(PortableTargetFullPath); - } - - AICLI_LOG(Core, Info, << "Portable exe moved to: " << PortableTargetFullPath); - CommitToARPEntry(PortableValueName::PortableTargetFullPath, PortableTargetFullPath); + CommitToARPEntry(PortableValueName::InstallLocation, TargetInstallDirectory); + } - Filesystem::RenameFile(installerPath, PortableTargetFullPath); + void PortableInstaller::RemoveInstallDirectory() + { + if (std::filesystem::exists(InstallLocation)) + { + if (Purge) + { + m_stream << Resource::String::PurgeInstallDirectory << std::endl; + const auto& removedFilesCount = std::filesystem::remove_all(InstallLocation); + AICLI_LOG(CLI, Info, << "Purged install location directory. Deleted " << removedFilesCount << " files or directories"); + } + else + { + if (std::filesystem::is_empty(InstallLocation)) + { + AICLI_LOG(CLI, Info, << "Removing empty install directory: " << InstallLocation); + std::filesystem::remove(InstallLocation); + } + else + { + AICLI_LOG(CLI, Info, << "Unable to remove install directory as there are remaining files in: " << InstallLocation); + m_stream << Resource::String::FilesRemainInInstallDirectory << InstallLocation << std::endl; + } + } + } } bool PortableInstaller::CreatePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath) @@ -298,33 +317,6 @@ namespace AppInstaller::CLI::Portable } } - void PortableInstaller::RemoveInstallDirectory(bool purge) - { - if (std::filesystem::exists(InstallLocation)) - { - if (purge) - { - m_stream << Resource::String::PurgeInstallDirectory << std::endl; - const auto& removedFilesCount = std::filesystem::remove_all(InstallLocation); - AICLI_LOG(CLI, Info, << "Purged install location directory. Deleted " << removedFilesCount << " files or directories"); - } - else if (std::filesystem::is_empty(InstallLocation)) - { - AICLI_LOG(CLI, Info, << "Removing empty install directory: " << InstallLocation); - std::filesystem::remove(InstallLocation); - } - else - { - AICLI_LOG(CLI, Info, << "Unable to remove install directory as there are remaining files in: " << InstallLocation); - m_stream << Resource::String::FilesRemainInInstallDirectory << InstallLocation << std::endl; - } - } - else - { - AICLI_LOG(CLI, Info, << "Install directory does not exist: " << InstallLocation); - } - } - void PortableInstaller::AddToPathVariable() { const std::filesystem::path& pathValue = GetInstallDirectoryForPathVariable(); @@ -388,6 +380,36 @@ namespace AppInstaller::CLI::Portable HelpLink = manifest.CurrentLocalization.Get(); } + std::vector PortableInstaller::GetExpectedState() + { + std::vector entries; + + const auto& indexPath = GetPortableIndexPath(); + + if (std::filesystem::exists(indexPath)) + { + PortableIndex portableIndex = PortableIndex::Open(indexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); + entries = portableIndex.GetAllPortableFiles(); + } + else + { + std::filesystem::path targetFullPath = PortableTargetFullPath; + std::filesystem::path symlinkFullPath = PortableSymlinkFullPath; + + if (!symlinkFullPath.empty()) + { + entries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkFullPath, targetFullPath))); + } + + if (!targetFullPath.empty()) + { + entries.emplace_back(std::move(PortableFileEntry::CreateFileEntry({}, targetFullPath, SHA256))); + } + } + + return entries; + } + PortableInstaller::PortableInstaller(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode) : m_portableARPEntry(PortableARPEntry(scope, arch, productCode)) { @@ -410,6 +432,8 @@ namespace AppInstaller::CLI::Portable InstallDirectoryAddedToPath = GetBoolValue(PortableValueName::InstallDirectoryAddedToPath); InstallDirectoryCreated = GetBoolValue(PortableValueName::InstallDirectoryCreated); } + + m_expectedEntries = GetExpectedState(); } void PortableInstaller::InitializeRegistryEntry() diff --git a/src/AppInstallerCLICore/PortableInstaller.h b/src/AppInstallerCLICore/PortableInstaller.h index d2d55ad149..06aaaece17 100644 --- a/src/AppInstallerCLICore/PortableInstaller.h +++ b/src/AppInstallerCLICore/PortableInstaller.h @@ -55,11 +55,6 @@ namespace AppInstaller::CLI::Portable m_desiredEntries = desiredEntries; }; - void SetExpectedState(std::vector& expectedEntries) - { - m_expectedEntries = expectedEntries; - }; - HRESULT Install(); HRESULT Uninstall(); @@ -72,7 +67,7 @@ namespace AppInstaller::CLI::Portable std::filesystem::path GetInstallDirectoryForPathVariable() { - return InstallDirectoryAddedToPath ? InstallLocation : GetPortableLinksLocation(GetScope()); + return InstallDirectoryAddedToPath ? TargetInstallDirectory : GetPortableLinksLocation(GetScope()); } std::filesystem::path GetPortableIndexPath(); @@ -104,17 +99,16 @@ namespace AppInstaller::CLI::Portable std::filesystem::path GetPathValue(PortableValueName valueName); bool GetBoolValue(PortableValueName valueName); + std::vector GetExpectedState(); + void InitializeRegistryEntry(); - void FinalizeRegistryEntry(); void ApplyDesiredState(); - void MovePortableExe(const std::filesystem::path& installerPath); - bool CreatePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath); + void CreateInstallDirectory(); + void RemoveInstallDirectory(); - void RemovePortableExe(const std::filesystem::path& targetPath, const std::string& hash); - void RemovePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath); - void RemoveInstallDirectory(bool purge); + bool CreatePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath); void AddToPathVariable(); void RemoveFromPathVariable(); diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp index 0eaa58a487..f219f25efb 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -161,6 +161,7 @@ namespace AppInstaller::CLI::Workflow context.Add(std::move(portableInstaller)); } + std::vector GetDesiredStateForPortableInstall(Execution::Context& context) { std::filesystem::path& installerPath = context.Get(); @@ -173,32 +174,30 @@ namespace AppInstaller::CLI::Workflow // 3. Symlinks // Uninstall should process these portable files in the reverse order for proper cleanup. - const std::filesystem::path& symlinkDirectory = GetPortableLinksLocation(portableInstaller.GetScope()); const std::filesystem::path& targetInstallDirectory = portableInstaller.TargetInstallDirectory; - entries.emplace_back(std::move(PortableFileEntry::CreateDirectoryEntry(targetInstallDirectory, true))); + const std::filesystem::path& symlinkDirectory = GetPortableLinksLocation(portableInstaller.GetScope()); // InstallerPath will point to a directory if it is extracted from an archive. if (std::filesystem::is_directory(installerPath)) { - int entryCount = 0; for (const auto& entry : std::filesystem::directory_iterator(installerPath)) { std::filesystem::path entryPath = entry.path(); PortableFileEntry portableFile; + std::filesystem::path relativePath = std::filesystem::relative(entryPath, entryPath.parent_path()); + std::filesystem::path targetPath = targetInstallDirectory / relativePath; + if (std::filesystem::is_directory(entryPath)) { - entries.emplace_back(std::move(PortableFileEntry::CreateDirectoryEntry(entryPath))); + entries.emplace_back(std::move(PortableFileEntry::CreateDirectoryEntry(entryPath, targetPath))); } else { - std::filesystem::path relativePath = std::filesystem::relative(entryPath, entryPath.parent_path()); - entries.emplace_back(std::move(PortableFileEntry::CreateFileEntry(entryPath, targetInstallDirectory / relativePath))); + entries.emplace_back(std::move(PortableFileEntry::CreateFileEntry(entryPath, targetPath, {}))); } - - entryCount++; } - if (entryCount > 1) + if (entries.size() > 1) { portableInstaller.RecordToIndex = true; } @@ -255,50 +254,13 @@ namespace AppInstaller::CLI::Workflow const std::filesystem::path& targetFullPath = targetInstallDirectory / fileName; const std::filesystem::path& symlinkFullPath = symlinkDirectory / commandAlias; - entries.emplace_back(std::move(PortableFileEntry::CreateFileEntry(installerPath, targetFullPath))); + entries.emplace_back(std::move(PortableFileEntry::CreateFileEntry(installerPath, targetFullPath, {}))); entries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkFullPath, targetFullPath))); } return entries; } - std::vector GetExpectedState(Execution::Context& context) - { - PortableInstaller& portableInstaller = context.Get(); - std::vector entries; - - const auto& indexPath = portableInstaller.GetPortableIndexPath(); - - if (std::filesystem::exists(indexPath)) - { - PortableIndex portableIndex = PortableIndex::Open(indexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); - entries = portableIndex.GetAllPortableFiles(); - } - else - { - std::filesystem::path targetFullPath = portableInstaller.PortableTargetFullPath; - std::filesystem::path symlinkFullPath = portableInstaller.PortableSymlinkFullPath; - std::filesystem::path installLocation = portableInstaller.InstallLocation; - - if (!symlinkFullPath.empty()) - { - entries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkFullPath, targetFullPath))); - } - - if (!targetFullPath.empty()) - { - entries.emplace_back(std::move(PortableFileEntry::CreateFileEntry({}, targetFullPath))); - } - - if (!installLocation.empty() && portableInstaller.InstallDirectoryCreated) - { - entries.emplace_back(std::move(PortableFileEntry::CreateDirectoryEntry(installLocation))); - } - } - - return entries; - } - void PortableInstallImpl(Execution::Context& context) { HRESULT result = ERROR_SUCCESS; @@ -308,12 +270,9 @@ namespace AppInstaller::CLI::Workflow { context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl; - // Use context to get desired state and pass it to the portable install resolution engine std::vector desiredState = GetDesiredStateForPortableInstall(context); - std::vector expectedState = GetExpectedState(context); portableInstaller.SetDesiredState(desiredState); - portableInstaller.SetExpectedState(expectedState); if (!portableInstaller.VerifyExpectedState()) { diff --git a/src/AppInstallerCLITests/PortableInstaller.cpp b/src/AppInstallerCLITests/PortableInstaller.cpp index 8fc825f84c..8e8a845315 100644 --- a/src/AppInstallerCLITests/PortableInstaller.cpp +++ b/src/AppInstallerCLITests/PortableInstaller.cpp @@ -101,207 +101,88 @@ void VerifyPortableFilesTrackedByIndex( } } -//TEST_CASE("PortableInstaller_SingleInstall", "[PortableInstaller]") -//{ -// TempDirectory tempDirectory = TestCommon::TempDirectory("TempDirectory", false); -// -// { -// PortableInstaller portableInstaller = CreateTestPortableInstaller(tempDirectory.GetPath()); -// -// TestCommon::TempFile testPortable("testPortable.txt"); -// std::ofstream file2(testPortable, std::ofstream::out); -// file2.close(); -// -// HRESULT installResult = portableInstaller.InstallSingle(testPortable.GetPath()); -// REQUIRE(SUCCEEDED(installResult)); -// } -// -// { -// // Create a new portable installer instance and verify that values from ARP are loaded correctly. -// PortableInstaller portableInstaller = PortableInstaller(ScopeEnum::User, Architecture::X64, "testProductCode"); -// -// REQUIRE(std::filesystem::exists(portableInstaller.PortableTargetFullPath)); -// REQUIRE(SymlinkExists(portableInstaller.PortableSymlinkFullPath)); -// REQUIRE(VerifySymlink(portableInstaller.PortableSymlinkFullPath, portableInstaller.PortableTargetFullPath)); -// REQUIRE(portableInstaller.ARPEntryExists()); -// -// HRESULT uninstallResult = portableInstaller.Uninstall(); -// -// REQUIRE(SUCCEEDED(uninstallResult)); -// REQUIRE_FALSE(SymlinkExists(portableInstaller.PortableSymlinkFullPath)); -// REQUIRE_FALSE(std::filesystem::exists(portableInstaller.PortableTargetFullPath)); -// REQUIRE_FALSE(std::filesystem::exists(portableInstaller.InstallLocation)); -// } -//} -// -//TEST_CASE("PortableInstaller_InstallDirectoryAddedToPath", "[PortableInstaller]") -//{ -// TempDirectory tempDirectory = TestCommon::TempDirectory("TempDirectory", false); -// -// { -// PortableInstaller portableInstaller = CreateTestPortableInstaller(tempDirectory.GetPath()); -// -// TestCommon::TempFile testPortable("testPortable.txt"); -// std::ofstream file2(testPortable, std::ofstream::out); -// file2.close(); -// -// // This value is only set to true if we fail to create a symlink. -// // If true no symlink should be created and InstallDirectory is added to PATH variable. -// portableInstaller.CommitToARPEntry(PortableValueName::InstallDirectoryAddedToPath, portableInstaller.InstallDirectoryAddedToPath = true); -// -// HRESULT installResult = portableInstaller.InstallSingle(testPortable.GetPath()); -// REQUIRE(SUCCEEDED(installResult)); -// } -// -// { -// PortableInstaller portableInstaller = PortableInstaller(ScopeEnum::User, Architecture::X64, "testProductCode"); -// -// REQUIRE(std::filesystem::exists(portableInstaller.PortableTargetFullPath)); -// REQUIRE_FALSE(SymlinkExists(portableInstaller.PortableSymlinkFullPath)); -// REQUIRE(PathVariable(ScopeEnum::User).Contains(portableInstaller.InstallLocation)); -// REQUIRE(portableInstaller.ARPEntryExists()); -// -// HRESULT uninstallResult = portableInstaller.Uninstall(); -// -// REQUIRE(SUCCEEDED(uninstallResult)); -// REQUIRE(portableInstaller.InstallDirectoryAddedToPath); -// REQUIRE_FALSE(std::filesystem::exists(portableInstaller.PortableTargetFullPath)); -// REQUIRE_FALSE(std::filesystem::exists(portableInstaller.InstallLocation)); -// REQUIRE_FALSE(PathVariable(ScopeEnum::User).Contains(portableInstaller.InstallLocation)); -// } -//} -// -//TEST_CASE("PortableInstaller_MultipleInstall", "[PortableInstaller]") -//{ -// PortableInstaller portableInstaller = PortableInstaller( -// ScopeEnum::User, -// Architecture::X64, -// "testProductCode"); -// -// TempDirectory tempDirectory = TestCommon::TempDirectory("TempDirectory", true); -// const auto& tempDirectoryPath = tempDirectory.GetPath(); -// portableInstaller.InstallLocation = tempDirectoryPath; -// -// std::vector nestedInstallerFiles = CreateTestNestedInstallerFiles(); -// -// HRESULT installResult = portableInstaller.InstallMultiple(nestedInstallerFiles); -// REQUIRE(SUCCEEDED(installResult)); -// -// const auto& indexPath = portableInstaller.GetPortableIndexPath(); -// REQUIRE(std::filesystem::exists(indexPath)); -// -// VerifyPortableFilesTrackedByIndex(indexPath, nestedInstallerFiles); -// -// REQUIRE(portableInstaller.VerifyPortableFilesForUninstall()); -// -// // Perform uninstall -// HRESULT uninstallResult = portableInstaller.Uninstall(); -// REQUIRE(SUCCEEDED(uninstallResult)); -// -// REQUIRE_FALSE(std::filesystem::exists(indexPath)); -// REQUIRE_FALSE(std::filesystem::exists(portableInstaller.InstallLocation)); -//} -// -//TEST_CASE("PortableInstaller_VerifyFilesFromIndex", "[PortableInstaller]") -//{ -// // Create installer and set install location to temp directory -// // Create portable index and add files -// PortableInstaller portableInstaller = PortableInstaller( -// ScopeEnum::User, -// Architecture::X64, -// "testProductCode"); -// -// TempDirectory tempDirectory = TestCommon::TempDirectory("TempDirectory", true); -// const auto& tempDirectoryPath = tempDirectory.GetPath(); -// portableInstaller.InstallLocation = tempDirectoryPath; -// -// std::filesystem::path indexPath = tempDirectoryPath / "portable.db"; -// -// PortableIndex index = PortableIndex::CreateNew(indexPath.u8string(), Schema::Version::Latest()); -// -// std::filesystem::path symlinkPath = tempDirectoryPath / "symlink.exe"; -// std::filesystem::path exePath = tempDirectoryPath / "testExe.txt"; -// -// // Create exe and symlink -// TestCommon::TempFile testFile(exePath); -// std::ofstream file2(testFile, std::ofstream::out); -// file2.close(); -// -// std::filesystem::create_symlink(exePath, symlinkPath); -// -// // Add files to index -// IPortableIndex::PortableFile exeFile = PortableIndex::CreatePortableFileFromPath(exePath); -// IPortableIndex::PortableFile symlinkFile = PortableIndex::CreatePortableFileFromPath(symlinkPath); -// -// index.AddPortableFile(exeFile); -// index.AddPortableFile(symlinkFile); -// -// REQUIRE(portableInstaller.VerifyPortableFilesForUninstall()); -// -// // Modify symlink target -// std::filesystem::remove(symlinkPath); -// std::filesystem::create_symlink("badPath", symlinkPath); -// -// REQUIRE_FALSE(portableInstaller.VerifyPortableFilesForUninstall()); -// -// std::filesystem::remove(symlinkPath); -// -// // Modify exe hash in index -// exeFile.SHA256 = "2413fb3709b05939f04cf2e92f7d0897fc2596f9ad0b8a9ea855c7bfebaae892"; -// index.UpdatePortableFile(exeFile); -// -// REQUIRE_FALSE(portableInstaller.VerifyPortableFilesForUninstall()); -// -// std::filesystem::remove(exePath); -// -// // Files that do not exist should still pass as they have already been removed. -// REQUIRE(portableInstaller.VerifyPortableFilesForUninstall()); -//} -// -//TEST_CASE("PortableInstaller_VerifyFiles", "[PortableInstaller]") -//{ -// PortableInstaller portableInstaller = PortableInstaller( -// ScopeEnum::User, -// Architecture::X64, -// "testProductCode"); -// -// TempDirectory tempDirectory = TestCommon::TempDirectory("TempDirectory", true); -// const auto& tempDirectoryPath = tempDirectory.GetPath(); -// portableInstaller.InstallLocation = tempDirectoryPath; -// -// std::filesystem::path symlinkPath = tempDirectoryPath / "symlink.exe"; -// std::filesystem::path exePath = tempDirectoryPath / "testExe.txt"; -// -// // Create and set test file, symlink, and hash -// TestCommon::TempFile testFile(exePath); -// std::ofstream file(testFile, std::ofstream::out); -// file.close(); -// -// std::filesystem::create_symlink(exePath, symlinkPath); -// -// std::ifstream inStream{ exePath, std::ifstream::binary }; -// const SHA256::HashBuffer& targetFileHash = SHA256::ComputeHash(inStream); -// inStream.close(); -// portableInstaller.SHA256 = SHA256::ConvertToString(targetFileHash); -// portableInstaller.PortableTargetFullPath = exePath; -// portableInstaller.PortableSymlinkFullPath = symlinkPath; -// -// REQUIRE(portableInstaller.VerifyPortableFilesForUninstall()); -// -// // Modify symlink target -// std::filesystem::remove(symlinkPath); -// std::filesystem::create_symlink("badPath", symlinkPath); -// -// REQUIRE_FALSE(portableInstaller.VerifyPortableFilesForUninstall()); -// -// // Modify exe hash -// std::filesystem::remove(symlinkPath); -// portableInstaller.SHA256 = "2413fb3709b05939f04cf2e92f7d0897fc2596f9ad0b8a9ea855c7bfebaae892"; -// -// REQUIRE_FALSE(portableInstaller.VerifyPortableFilesForUninstall()); -// -// std::filesystem::remove(exePath); -// -// // Files that do not exist should still pass as they have already been removed. -// REQUIRE(portableInstaller.VerifyPortableFilesForUninstall()); -//} \ No newline at end of file +TEST_CASE("PortableInstaller_InstallToRegistry", "[PortableInstaller]") +{ + TempDirectory tempDirectory = TestCommon::TempDirectory("TempDirectory", false); + + std::vector desiredTestState; + + TestCommon::TempFile testPortable("testPortable.txt"); + std::ofstream file(testPortable, std::ofstream::out); + file.close(); + + std::filesystem::path targetPath = tempDirectory.GetPath() / "testPortable.txt"; + std::filesystem::path symlinkPath = tempDirectory.GetPath() / "testSymlink.exe"; + + desiredTestState.emplace_back(std::move(PortableFileEntry::CreateFileEntry(testPortable.GetPath(), targetPath, {}))); + desiredTestState.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkPath, targetPath))); + + PortableInstaller portableInstaller = PortableInstaller(ScopeEnum::User, Architecture::X64, "testProductCode"); + portableInstaller.TargetInstallDirectory = tempDirectory.GetPath(); + portableInstaller.SetDesiredState(desiredTestState); + portableInstaller.Install(); + + PortableInstaller portableInstaller2 = PortableInstaller(ScopeEnum::User, Architecture::X64, "testProductCode"); + REQUIRE(portableInstaller2.ARPEntryExists()); + REQUIRE(std::filesystem::exists(portableInstaller2.PortableTargetFullPath)); + REQUIRE(AppInstaller::Filesystem::SymlinkExists(portableInstaller2.PortableSymlinkFullPath)); + + portableInstaller2.Uninstall(); + REQUIRE_FALSE(std::filesystem::exists(portableInstaller2.PortableTargetFullPath)); + REQUIRE_FALSE(AppInstaller::Filesystem::SymlinkExists(portableInstaller2.PortableSymlinkFullPath)); + REQUIRE_FALSE(std::filesystem::exists(portableInstaller2.InstallLocation)); +} + +TEST_CASE("PortableInstaller_InstallToIndex", "[PortableInstaller]") +{ + TempDirectory installRootDirectory = TestCommon::TempDirectory("PortableInstallRoot", false); + + std::vector desiredTestState; + + TestCommon::TempFile testPortable("testPortable.txt"); + std::ofstream file1(testPortable, std::ofstream::out); + file1.close(); + + TestCommon::TempFile testPortable2("testPortable2.txt"); + std::ofstream file2(testPortable2, std::ofstream::out); + file2.close(); + + TestCommon::TempDirectory testDirectoryFolder("testDirectory", true); + + std::filesystem::path installRootPath = installRootDirectory.GetPath(); + std::filesystem::path targetPath = installRootPath / "testPortable.txt"; + std::filesystem::path targetPath2 = installRootPath / "testPortable2.txt"; + std::filesystem::path symlinkPath = installRootPath / "testSymlink.exe"; + std::filesystem::path symlinkPath2 = installRootPath / "testSymlink2.exe"; + std::filesystem::path directoryPath = installRootPath / "testDirectory"; + + desiredTestState.emplace_back(std::move(PortableFileEntry::CreateFileEntry(testPortable.GetPath(), targetPath, {}))); + desiredTestState.emplace_back(std::move(PortableFileEntry::CreateFileEntry(testPortable2.GetPath(), targetPath2, {}))); + desiredTestState.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkPath, targetPath))); + desiredTestState.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkPath2, targetPath2))); + desiredTestState.emplace_back(std::move(PortableFileEntry::CreateDirectoryEntry(testDirectoryFolder.GetPath(), directoryPath))); + + PortableInstaller portableInstaller = PortableInstaller(ScopeEnum::User, Architecture::X64, "testProductCode"); + portableInstaller.TargetInstallDirectory = installRootDirectory.GetPath(); + portableInstaller.RecordToIndex = true; + portableInstaller.SetDesiredState(desiredTestState); + portableInstaller.Install(); + + REQUIRE(std::filesystem::exists(targetPath)); + REQUIRE(std::filesystem::exists(targetPath2)); + REQUIRE(AppInstaller::Filesystem::SymlinkExists(symlinkPath)); + REQUIRE(AppInstaller::Filesystem::SymlinkExists(symlinkPath2)); + REQUIRE(std::filesystem::exists(directoryPath)); + + PortableInstaller portableInstaller2 = PortableInstaller(ScopeEnum::User, Architecture::X64, "testProductCode"); + REQUIRE(portableInstaller2.ARPEntryExists()); + + portableInstaller2.Uninstall(); + REQUIRE_FALSE(std::filesystem::exists(targetPath)); + REQUIRE_FALSE(std::filesystem::exists(targetPath2)); + REQUIRE_FALSE(AppInstaller::Filesystem::SymlinkExists(symlinkPath)); + REQUIRE_FALSE(AppInstaller::Filesystem::SymlinkExists(symlinkPath2)); + REQUIRE_FALSE(std::filesystem::exists(directoryPath)); +} + + diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp index a659a781ab..b8981e86cc 100644 --- a/src/AppInstallerCLITests/WorkFlow.cpp +++ b/src/AppInstallerCLITests/WorkFlow.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -55,6 +56,7 @@ using namespace AppInstaller::Repository; using namespace AppInstaller::Settings; using namespace AppInstaller::Utility; using namespace AppInstaller::Settings; +using namespace AppInstaller::CLI::Portable; #define REQUIRE_TERMINATED_WITH(_context_,_hr_) \ diff --git a/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h b/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h index 6b3e69ee3e..06c4c2e0c5 100644 --- a/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h +++ b/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h @@ -24,37 +24,55 @@ namespace AppInstaller::Portable std::string SHA256; std::string SymlinkTarget; std::filesystem::path CurrentPath; - bool IsInstallDirectory = false; - void SetFilePath(const std::filesystem::path& path) { m_filePath = std::filesystem::weakly_canonical(path); }; + void SetFilePath(const std::filesystem::path& path) + { + if (FileType == PortableFileType::Symlink) + { + // weakly_canonical will resolve the symlink path to its target, set path directly since we generate the full path. + m_filePath = path; + } + else + { + m_filePath = std::filesystem::weakly_canonical(path); + } + }; std::filesystem::path GetFilePath() const { return m_filePath; }; - static PortableFileEntry CreateFileEntry(const std::filesystem::path& currentPath, const std::filesystem::path& targetPath) + static PortableFileEntry CreateFileEntry(const std::filesystem::path& currentPath, const std::filesystem::path& targetPath, const std::string& sha256) { PortableFileEntry fileEntry; + fileEntry.FileType = PortableFileType::File; fileEntry.CurrentPath = currentPath; fileEntry.SetFilePath(targetPath); - fileEntry.FileType = PortableFileType::File; - fileEntry.SHA256 = Utility::SHA256::ConvertToString(Utility::SHA256::ComputeHashFromFile(currentPath)); + + if (sha256.empty()) + { + fileEntry.SHA256 = Utility::SHA256::ConvertToString(Utility::SHA256::ComputeHashFromFile(currentPath)); + } + else + { + fileEntry.SHA256 = sha256; + } return fileEntry; } static PortableFileEntry CreateSymlinkEntry(const std::filesystem::path& symlinkPath, const std::filesystem::path& targetPath) { PortableFileEntry symlinkEntry; + symlinkEntry.FileType = PortableFileType::Symlink; symlinkEntry.SetFilePath(symlinkPath); symlinkEntry.SymlinkTarget = targetPath.u8string(); - symlinkEntry.FileType = PortableFileType::Symlink; return symlinkEntry; } - static PortableFileEntry CreateDirectoryEntry(const std::filesystem::path& directoryPath, bool isInstallDirectory = false) + static PortableFileEntry CreateDirectoryEntry(const std::filesystem::path& currentPath, const std::filesystem::path& directoryPath) { PortableFileEntry directoryEntry; - directoryEntry.SetFilePath(directoryPath); directoryEntry.FileType = PortableFileType::Directory; - directoryEntry.IsInstallDirectory = isInstallDirectory; + directoryEntry.CurrentPath = currentPath; + directoryEntry.SetFilePath(directoryPath); return directoryEntry; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp index 1b8caaff41..f3bb074715 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp @@ -20,6 +20,9 @@ namespace AppInstaller::Repository::Microsoft result.m_interface->CreateTable(result.m_dbconn); + const auto& filePathUTF16 = Utility::ConvertToUTF16(filePath); + SetFileAttributes(filePathUTF16.c_str(), GetFileAttributes(filePathUTF16.c_str()) | FILE_ATTRIBUTE_HIDDEN); + result.SetLastWriteTime(); savepoint.Commit(); diff --git a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h index 2e4e63f639..8319451d81 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h @@ -32,8 +32,6 @@ namespace AppInstaller::Repository::Microsoft return { filePath, disposition, std::move(indexFile) }; } - static Portable::PortableFileEntry CreatePortableFileFromPath(const std::filesystem::path& path); - IdType AddPortableFile(const Portable::PortableFileEntry& file); void RemovePortableFile(const Portable::PortableFileEntry& file); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp index 4ffb56bfff..2efca323fd 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp @@ -13,7 +13,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 static constexpr std::string_view s_PortableTable_FileType_Column = "filetype"sv; static constexpr std::string_view s_PortableTable_SHA256_Column = "sha256"sv; static constexpr std::string_view s_PortableTable_SymlinkTarget_Column = "symlinktarget"sv; - static constexpr std::string_view s_PortableTable_IsInstallDirectory_Column = "isinstalldirectory"sv; std::string_view PortableTable::TableName() { @@ -33,7 +32,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 createTableBuilder.Column(ColumnBuilder(s_PortableTable_FileType_Column, Type::Int64).NotNull()); createTableBuilder.Column(ColumnBuilder(s_PortableTable_SHA256_Column, Type::Blob)); createTableBuilder.Column(ColumnBuilder(s_PortableTable_SymlinkTarget_Column, Type::Text)); - createTableBuilder.Column(ColumnBuilder(s_PortableTable_IsInstallDirectory_Column, Type::Bool)); createTableBuilder.EndColumns(); createTableBuilder.Execute(connection); @@ -73,9 +71,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 .Columns({ s_PortableTable_FilePath_Column, s_PortableTable_FileType_Column, s_PortableTable_SHA256_Column, - s_PortableTable_SymlinkTarget_Column, - s_PortableTable_IsInstallDirectory_Column}) - .Values(file.GetFilePath().u8string(), file.FileType, file.SHA256, file.SymlinkTarget, file.IsInstallDirectory); + s_PortableTable_SymlinkTarget_Column }) + .Values(file.GetFilePath().u8string(), file.FileType, file.SHA256, file.SymlinkTarget); builder.Execute(connection); return connection.GetLastInsertRowID(); @@ -89,7 +86,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 .Column(s_PortableTable_FileType_Column).Equals(file.FileType) .Column(s_PortableTable_SHA256_Column).Equals(file.SHA256) .Column(s_PortableTable_SymlinkTarget_Column).Equals(file.SymlinkTarget) - .Column(s_PortableTable_IsInstallDirectory_Column).Equals(file.IsInstallDirectory) .Where(SQLite::RowIDName).Equals(id); builder.Execute(connection); @@ -102,8 +98,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 builder.Select({ s_PortableTable_FilePath_Column, s_PortableTable_FileType_Column, s_PortableTable_SHA256_Column, - s_PortableTable_SymlinkTarget_Column, - s_PortableTable_IsInstallDirectory_Column}) + s_PortableTable_SymlinkTarget_Column}) .From(s_PortableTable_Table_Name).Where(SQLite::RowIDName).Equals(id); SQLite::Statement select = builder.Prepare(connection); @@ -111,12 +106,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 Portable::PortableFileEntry portableFile; if (select.Step()) { - auto [filePath, fileType, sha256, symlinkTarget, isInstallDirectory] = select.GetRow(); - portableFile.SetFilePath(std::move(filePath)); + auto [filePath, fileType, sha256, symlinkTarget] = select.GetRow(); portableFile.FileType = fileType; + portableFile.SetFilePath(std::move(filePath)); portableFile.SHA256 = std::move(sha256); portableFile.SymlinkTarget = std::move(symlinkTarget); - portableFile.IsInstallDirectory = isInstallDirectory; return portableFile; } else @@ -163,8 +157,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 builder.Select({ s_PortableTable_FilePath_Column, s_PortableTable_FileType_Column, s_PortableTable_SHA256_Column, - s_PortableTable_SymlinkTarget_Column, - s_PortableTable_IsInstallDirectory_Column }) + s_PortableTable_SymlinkTarget_Column }) .From(s_PortableTable_Table_Name); SQLite::Statement select = builder.Prepare(connection); @@ -172,12 +165,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 while (select.Step()) { Portable::PortableFileEntry portableFile; - auto [filePath, fileType, sha256, symlinkTarget, isInstallDirectory] = select.GetRow(); - portableFile.SetFilePath(std::move(filePath)); + auto [filePath, fileType, sha256, symlinkTarget] = select.GetRow(); portableFile.FileType = fileType; + portableFile.SetFilePath(std::move(filePath)); portableFile.SHA256 = std::move(sha256); portableFile.SymlinkTarget = std::move(symlinkTarget); - portableFile.IsInstallDirectory = isInstallDirectory; result.emplace_back(std::move(portableFile)); } From 6882f39fcd032462a78314813ea78588991c0fd7 Mon Sep 17 00:00:00 2001 From: Ryan Fu Date: Tue, 20 Sep 2022 12:25:08 -0700 Subject: [PATCH 16/22] savework --- src/AppInstallerCLICore/PortableInstaller.cpp | 39 ++--- src/AppInstallerCLICore/PortableInstaller.h | 17 +- .../Workflows/PortableFlow.cpp | 36 ++-- .../PortableInstaller.cpp | 156 +++++++++--------- .../Public/winget/PortableFileEntry.h | 2 +- 5 files changed, 114 insertions(+), 136 deletions(-) diff --git a/src/AppInstallerCLICore/PortableInstaller.cpp b/src/AppInstallerCLICore/PortableInstaller.cpp index be61aab286..56651baab6 100644 --- a/src/AppInstallerCLICore/PortableInstaller.cpp +++ b/src/AppInstallerCLICore/PortableInstaller.cpp @@ -162,7 +162,7 @@ namespace AppInstaller::CLI::Portable void PortableInstaller::ApplyDesiredState() { - std::filesystem::path existingIndexPath = InstallLocation / c_PortableIndexFileName; + std::filesystem::path existingIndexPath = InstallLocation / GetPortableIndexFileName(); if (std::filesystem::exists(existingIndexPath)) { bool deleteIndex = false; @@ -192,14 +192,14 @@ namespace AppInstaller::CLI::Portable } // Check if existing install location differs from the target install location for proper cleanup. - if (!InstallLocation.empty() && TargetInstallDirectory != InstallLocation) + if (!TargetInstallLocation.empty() && TargetInstallLocation != InstallLocation) { RemoveInstallDirectory(); } if (RecordToIndex) { - std::filesystem::path targetIndexPath = TargetInstallDirectory / c_PortableIndexFileName; + std::filesystem::path targetIndexPath = TargetInstallLocation / GetPortableIndexFileName(); PortableIndex index = CreateOrOpenPortableIndex(targetIndexPath); for (auto desiredEntry : m_desiredEntries) { @@ -230,20 +230,18 @@ namespace AppInstaller::CLI::Portable return true; } - HRESULT PortableInstaller::Install() + void PortableInstaller::Install() { InitializeRegistryEntry(); - CreateInstallDirectory(); - + CreateTargetInstallDirectory(); + ApplyDesiredState(); AddToPathVariable(); - - return ERROR_SUCCESS; } - HRESULT PortableInstaller::Uninstall() + void PortableInstaller::Uninstall() { ApplyDesiredState(); @@ -253,24 +251,22 @@ namespace AppInstaller::CLI::Portable m_portableARPEntry.Delete(); AICLI_LOG(CLI, Info, << "PortableARPEntry deleted."); - - return ERROR_SUCCESS; } - void PortableInstaller::CreateInstallDirectory() + void PortableInstaller::CreateTargetInstallDirectory() { - if (std::filesystem::create_directories(TargetInstallDirectory)) + if (std::filesystem::create_directories(TargetInstallLocation)) { - AICLI_LOG(Core, Info, << "Created target install directory: " << TargetInstallDirectory); + AICLI_LOG(Core, Info, << "Created target install directory: " << TargetInstallLocation); CommitToARPEntry(PortableValueName::InstallDirectoryCreated, true); } - CommitToARPEntry(PortableValueName::InstallLocation, TargetInstallDirectory); + CommitToARPEntry(PortableValueName::InstallLocation, TargetInstallLocation); } void PortableInstaller::RemoveInstallDirectory() { - if (std::filesystem::exists(InstallLocation)) + if (std::filesystem::exists(InstallLocation) && InstallDirectoryCreated) { if (Purge) { @@ -319,7 +315,7 @@ namespace AppInstaller::CLI::Portable void PortableInstaller::AddToPathVariable() { - const std::filesystem::path& pathValue = GetInstallDirectoryForPathVariable(); + const std::filesystem::path& pathValue = InstallDirectoryAddedToPath ? TargetInstallLocation : GetPortableLinksLocation(GetScope()); if (PathVariable(GetScope()).Append(pathValue)) { AICLI_LOG(Core, Info, << "Appended target directory to PATH registry: " << pathValue); @@ -333,7 +329,7 @@ namespace AppInstaller::CLI::Portable void PortableInstaller::RemoveFromPathVariable() { - std::filesystem::path pathValue = GetInstallDirectoryForPathVariable(); + const std::filesystem::path& pathValue = InstallDirectoryAddedToPath ? InstallLocation : GetPortableLinksLocation(GetScope()); if (std::filesystem::exists(pathValue) && !std::filesystem::is_empty(pathValue)) { AICLI_LOG(Core, Info, << "Install directory is not empty: " << pathValue); @@ -384,7 +380,7 @@ namespace AppInstaller::CLI::Portable { std::vector entries; - const auto& indexPath = GetPortableIndexPath(); + const auto& indexPath = InstallLocation / GetPortableIndexFileName(); if (std::filesystem::exists(indexPath)) { @@ -450,11 +446,6 @@ namespace AppInstaller::CLI::Portable CommitToARPEntry(PortableValueName::HelpLink, HelpLink); } - std::filesystem::path PortableInstaller::GetPortableIndexPath() - { - return InstallLocation / c_PortableIndexFileName; - } - std::string PortableInstaller::GetStringValue(PortableValueName valueName) { if (m_portableARPEntry[valueName].has_value()) diff --git a/src/AppInstallerCLICore/PortableInstaller.h b/src/AppInstallerCLICore/PortableInstaller.h index 06aaaece17..f2ac2d310b 100644 --- a/src/AppInstallerCLICore/PortableInstaller.h +++ b/src/AppInstallerCLICore/PortableInstaller.h @@ -39,7 +39,8 @@ namespace AppInstaller::CLI::Portable bool Purge = false; bool RecordToIndex = false; - std::filesystem::path TargetInstallDirectory; + // This is the incoming target install location determined from the context args. + std::filesystem::path TargetInstallLocation; void InstallFile(AppInstaller::Portable::PortableFileEntry& desiredState); @@ -47,7 +48,6 @@ namespace AppInstaller::CLI::Portable PortableInstaller(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode); - // Ensures no modifications have been made that conflict with the expected state. bool VerifyExpectedState(); void SetDesiredState(std::vector& desiredEntries) @@ -55,9 +55,9 @@ namespace AppInstaller::CLI::Portable m_desiredEntries = desiredEntries; }; - HRESULT Install(); + void Install(); - HRESULT Uninstall(); + void Uninstall(); template void CommitToARPEntry(PortableValueName valueName, T value) @@ -67,10 +67,13 @@ namespace AppInstaller::CLI::Portable std::filesystem::path GetInstallDirectoryForPathVariable() { - return InstallDirectoryAddedToPath ? TargetInstallDirectory : GetPortableLinksLocation(GetScope()); + return InstallDirectoryAddedToPath ? InstallLocation : GetPortableLinksLocation(GetScope()); } - std::filesystem::path GetPortableIndexPath(); + std::filesystem::path GetPortableIndexFileName() + { + return Utility::ConvertToUTF16(GetProductCode() + ".db"); + } Manifest::ScopeEnum GetScope() { return m_portableARPEntry.GetScope(); }; @@ -105,7 +108,7 @@ namespace AppInstaller::CLI::Portable void ApplyDesiredState(); - void CreateInstallDirectory(); + void CreateTargetInstallDirectory(); void RemoveInstallDirectory(); bool CreatePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath); diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp index f219f25efb..d8c9de350d 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -156,7 +156,7 @@ namespace AppInstaller::CLI::Workflow targetInstallDirectory /= ConvertToUTF16(productCode); } - portableInstaller.TargetInstallDirectory = targetInstallDirectory; + portableInstaller.TargetInstallLocation = targetInstallDirectory; portableInstaller.SetAppsAndFeaturesMetadata(context.Get(), context.Get()->AppsAndFeaturesEntries); context.Add(std::move(portableInstaller)); } @@ -168,13 +168,7 @@ namespace AppInstaller::CLI::Workflow PortableInstaller& portableInstaller = context.Get(); std::vector entries; - // The order that we create these state matters. For install the order should be: - // 1. Install Root Directory - // 2. Files and Directories - // 3. Symlinks - // Uninstall should process these portable files in the reverse order for proper cleanup. - - const std::filesystem::path& targetInstallDirectory = portableInstaller.TargetInstallDirectory; + const std::filesystem::path& targetInstallDirectory = portableInstaller.TargetInstallLocation; const std::filesystem::path& symlinkDirectory = GetPortableLinksLocation(portableInstaller.GetScope()); // InstallerPath will point to a directory if it is extracted from an archive. @@ -207,7 +201,7 @@ namespace AppInstaller::CLI::Workflow for (const auto& nestedInstallerFile : nestedInstallerFiles) { const std::filesystem::path& targetPath = targetInstallDirectory / ConvertToUTF16(nestedInstallerFile.RelativeFilePath); - + std::filesystem::path commandAlias; if (nestedInstallerFile.PortableCommandAlias.empty()) { @@ -219,15 +213,13 @@ namespace AppInstaller::CLI::Workflow } Filesystem::AppendExtension(commandAlias, ".exe"); - const std::filesystem::path& symlinkFullPath = symlinkDirectory / commandAlias; - entries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkFullPath, targetPath))); + entries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkDirectory / commandAlias, targetPath))); } } else { std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); const std::vector& commands = context.Get()->Commands; - std::filesystem::path fileName; std::filesystem::path commandAlias; @@ -253,9 +245,8 @@ namespace AppInstaller::CLI::Workflow AppInstaller::Filesystem::AppendExtension(commandAlias, ".exe"); const std::filesystem::path& targetFullPath = targetInstallDirectory / fileName; - const std::filesystem::path& symlinkFullPath = symlinkDirectory / commandAlias; entries.emplace_back(std::move(PortableFileEntry::CreateFileEntry(installerPath, targetFullPath, {}))); - entries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkFullPath, targetFullPath))); + entries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkDirectory / commandAlias, targetFullPath))); } return entries; @@ -263,7 +254,6 @@ namespace AppInstaller::CLI::Workflow void PortableInstallImpl(Execution::Context& context) { - HRESULT result = ERROR_SUCCESS; PortableInstaller& portableInstaller = context.Get(); try @@ -289,18 +279,18 @@ namespace AppInstaller::CLI::Workflow portableInstaller.Install(); - context.Add(result); + context.Add(ERROR_SUCCESS); context.Reporter.Warn() << portableInstaller.GetOutputMessage(); } catch (...) { context.Add(Workflow::HandleException(context, std::current_exception())); - } - if (result != ERROR_SUCCESS && !portableInstaller.IsUpdate) - { - context.Reporter.Warn() << Resource::String::PortableInstallFailed << std::endl; - portableInstaller.Uninstall(); + if (!portableInstaller.IsUpdate) + { + context.Reporter.Warn() << Resource::String::PortableInstallFailed << std::endl; + portableInstaller.Uninstall(); + } } } @@ -329,8 +319,8 @@ namespace AppInstaller::CLI::Workflow portableInstaller.Purge = context.Args.Contains(Execution::Args::Type::Purge) || (!portableInstaller.IsUpdate && Settings::User().Get() && !context.Args.Contains(Execution::Args::Type::Preserve)); - HRESULT result = portableInstaller.Uninstall(); - context.Add(result); + portableInstaller.Uninstall(); + context.Add(ERROR_SUCCESS); context.Reporter.Warn() << portableInstaller.GetOutputMessage(); } catch (...) diff --git a/src/AppInstallerCLITests/PortableInstaller.cpp b/src/AppInstallerCLITests/PortableInstaller.cpp index 8e8a845315..8fe9a9e333 100644 --- a/src/AppInstallerCLITests/PortableInstaller.cpp +++ b/src/AppInstallerCLITests/PortableInstaller.cpp @@ -23,84 +23,6 @@ using namespace AppInstaller::Repository::Microsoft::Schema; using namespace AppInstaller::Utility; using namespace TestCommon; -PortableInstaller CreateTestPortableInstaller(const std::filesystem::path& installLocation) -{ - PortableInstaller portableInstaller = PortableInstaller( - ScopeEnum::User, - Architecture::X64, - "testProductCode"); - - const auto& filename = "testPortable.txt"; - const auto& alias = "testSymlink.exe"; - - portableInstaller.InstallLocation = installLocation; - portableInstaller.PortableTargetFullPath = installLocation / filename; - portableInstaller.PortableSymlinkFullPath = GetPortableLinksLocation(ScopeEnum::User) / alias; - return portableInstaller; -} - -std::vector CreateExtractedItemsFromArchive(const std::filesystem::path& installLocation) -{ - std::vector extractedItems; - const auto& itemPath = installLocation / "testPortable.txt"; - const auto& itemPath2 = installLocation / "testPortable2.txt"; - extractedItems.emplace_back(itemPath); - extractedItems.emplace_back(itemPath2); - - for (const auto& item : extractedItems) - { - std::ofstream file(item, std::ofstream::out); - file.close(); - } - - return extractedItems; -} - -std::vector CreateTestNestedInstallerFiles() -{ - std::vector nestedInstallerFiles; - NestedInstallerFile file; - file.RelativeFilePath = "testPortable.txt"; - nestedInstallerFiles.emplace_back(file); - - NestedInstallerFile file2; - file2.RelativeFilePath = "testPortable2.txt"; - file2.PortableCommandAlias = "testSymlink.exe"; - nestedInstallerFiles.emplace_back(file2); - - return nestedInstallerFiles; -} - -// Ensures that the portable exes and symlinks all got recorded in the index. -void VerifyPortableFilesTrackedByIndex( - const std::filesystem::path& indexPath, - const std::vector& nestedInstallerFiles) -{ - { - // Verify that files were added to index. - Connection connection = Connection::Create(indexPath.u8string(), Connection::OpenDisposition::ReadWrite); - REQUIRE(!Schema::Portable_V1_0::PortableTable::IsEmpty(connection)); - - // Verify that all symlinks were added to the index. - for (const auto& item : nestedInstallerFiles) - { - std::filesystem::path symlinkPath = GetPortableLinksLocation(ScopeEnum::User); - if (!item.PortableCommandAlias.empty()) - { - symlinkPath /= ConvertToUTF16(item.PortableCommandAlias); - } - else - { - symlinkPath /= ConvertToUTF16(item.RelativeFilePath); - } - - AppendExtension(symlinkPath, ".exe"); - - REQUIRE(Schema::Portable_V1_0::PortableTable::SelectByFilePath(connection, symlinkPath).has_value()); - } - } -} - TEST_CASE("PortableInstaller_InstallToRegistry", "[PortableInstaller]") { TempDirectory tempDirectory = TestCommon::TempDirectory("TempDirectory", false); @@ -118,8 +40,10 @@ TEST_CASE("PortableInstaller_InstallToRegistry", "[PortableInstaller]") desiredTestState.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkPath, targetPath))); PortableInstaller portableInstaller = PortableInstaller(ScopeEnum::User, Architecture::X64, "testProductCode"); - portableInstaller.TargetInstallDirectory = tempDirectory.GetPath(); + portableInstaller.TargetInstallLocation = tempDirectory.GetPath(); portableInstaller.SetDesiredState(desiredTestState); + REQUIRE(portableInstaller.VerifyExpectedState()); + portableInstaller.Install(); PortableInstaller portableInstaller2 = PortableInstaller(ScopeEnum::User, Architecture::X64, "testProductCode"); @@ -133,7 +57,7 @@ TEST_CASE("PortableInstaller_InstallToRegistry", "[PortableInstaller]") REQUIRE_FALSE(std::filesystem::exists(portableInstaller2.InstallLocation)); } -TEST_CASE("PortableInstaller_InstallToIndex", "[PortableInstaller]") +TEST_CASE("PortableInstaller_InstallToIndex_CreateInstallRoot", "[PortableInstaller]") { TempDirectory installRootDirectory = TestCommon::TempDirectory("PortableInstallRoot", false); @@ -163,11 +87,72 @@ TEST_CASE("PortableInstaller_InstallToIndex", "[PortableInstaller]") desiredTestState.emplace_back(std::move(PortableFileEntry::CreateDirectoryEntry(testDirectoryFolder.GetPath(), directoryPath))); PortableInstaller portableInstaller = PortableInstaller(ScopeEnum::User, Architecture::X64, "testProductCode"); - portableInstaller.TargetInstallDirectory = installRootDirectory.GetPath(); + portableInstaller.TargetInstallLocation = installRootDirectory.GetPath(); + portableInstaller.RecordToIndex = true; + portableInstaller.SetDesiredState(desiredTestState); + REQUIRE(portableInstaller.VerifyExpectedState()); + + portableInstaller.Install(); + + REQUIRE(std::filesystem::exists(installRootPath / portableInstaller.GetPortableIndexFileName())); + REQUIRE(std::filesystem::exists(targetPath)); + REQUIRE(std::filesystem::exists(targetPath2)); + REQUIRE(AppInstaller::Filesystem::SymlinkExists(symlinkPath)); + REQUIRE(AppInstaller::Filesystem::SymlinkExists(symlinkPath2)); + REQUIRE(std::filesystem::exists(directoryPath)); + + PortableInstaller portableInstaller2 = PortableInstaller(ScopeEnum::User, Architecture::X64, "testProductCode"); + REQUIRE(portableInstaller2.ARPEntryExists()); + + portableInstaller2.Uninstall(); + + // Install root directory should be removed since it was created. + REQUIRE_FALSE(std::filesystem::exists(installRootPath)); + REQUIRE_FALSE(std::filesystem::exists(targetPath)); + REQUIRE_FALSE(std::filesystem::exists(targetPath2)); + REQUIRE_FALSE(AppInstaller::Filesystem::SymlinkExists(symlinkPath)); + REQUIRE_FALSE(AppInstaller::Filesystem::SymlinkExists(symlinkPath2)); + REQUIRE_FALSE(std::filesystem::exists(directoryPath)); +} + +TEST_CASE("PortableInstaller_InstallToIndex_ExistingInstallRoot", "[PortableInstaller]") +{ + TempDirectory installRootDirectory = TestCommon::TempDirectory("PortableInstallRoot", true); + + std::vector desiredTestState; + + TestCommon::TempFile testPortable("testPortable.txt"); + std::ofstream file1(testPortable, std::ofstream::out); + file1.close(); + + TestCommon::TempFile testPortable2("testPortable2.txt"); + std::ofstream file2(testPortable2, std::ofstream::out); + file2.close(); + + TestCommon::TempDirectory testDirectoryFolder("testDirectory", true); + + std::filesystem::path installRootPath = installRootDirectory.GetPath(); + std::filesystem::path targetPath = installRootPath / "testPortable.txt"; + std::filesystem::path targetPath2 = installRootPath / "testPortable2.txt"; + std::filesystem::path symlinkPath = installRootPath / "testSymlink.exe"; + std::filesystem::path symlinkPath2 = installRootPath / "testSymlink2.exe"; + std::filesystem::path directoryPath = installRootPath / "testDirectory"; + + desiredTestState.emplace_back(std::move(PortableFileEntry::CreateFileEntry(testPortable.GetPath(), targetPath, {}))); + desiredTestState.emplace_back(std::move(PortableFileEntry::CreateFileEntry(testPortable2.GetPath(), targetPath2, {}))); + desiredTestState.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkPath, targetPath))); + desiredTestState.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkPath2, targetPath2))); + desiredTestState.emplace_back(std::move(PortableFileEntry::CreateDirectoryEntry(testDirectoryFolder.GetPath(), directoryPath))); + + PortableInstaller portableInstaller = PortableInstaller(ScopeEnum::User, Architecture::X64, "testProductCode"); + portableInstaller.TargetInstallLocation = installRootDirectory.GetPath(); portableInstaller.RecordToIndex = true; portableInstaller.SetDesiredState(desiredTestState); + REQUIRE(portableInstaller.VerifyExpectedState()); + portableInstaller.Install(); + REQUIRE(std::filesystem::exists(installRootPath / portableInstaller.GetPortableIndexFileName())); REQUIRE(std::filesystem::exists(targetPath)); REQUIRE(std::filesystem::exists(targetPath2)); REQUIRE(AppInstaller::Filesystem::SymlinkExists(symlinkPath)); @@ -178,6 +163,9 @@ TEST_CASE("PortableInstaller_InstallToIndex", "[PortableInstaller]") REQUIRE(portableInstaller2.ARPEntryExists()); portableInstaller2.Uninstall(); + + // Install root directory should still exist since it was created previously. + REQUIRE(std::filesystem::exists(installRootPath)); REQUIRE_FALSE(std::filesystem::exists(targetPath)); REQUIRE_FALSE(std::filesystem::exists(targetPath2)); REQUIRE_FALSE(AppInstaller::Filesystem::SymlinkExists(symlinkPath)); @@ -186,3 +174,9 @@ TEST_CASE("PortableInstaller_InstallToIndex", "[PortableInstaller]") } + +// Tests +// Add an extra file that has not been tracked and verify that target directory has not been removed +// check verify portable files returns false if one of the files has been modified +// + diff --git a/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h b/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h index 06c4c2e0c5..2c65426390 100644 --- a/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h +++ b/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h @@ -29,7 +29,7 @@ namespace AppInstaller::Portable { if (FileType == PortableFileType::Symlink) { - // weakly_canonical will resolve the symlink path to its target, set path directly since we generate the full path. + // weakly_canonical will resolve the symlink path to its target if it exists, set path directly. m_filePath = path; } else From 3b7810bb60773bbfcf0a9f78fdacd2a87a454232 Mon Sep 17 00:00:00 2001 From: Ryan Fu Date: Tue, 20 Sep 2022 13:58:50 -0700 Subject: [PATCH 17/22] clean up --- src/AppInstallerCLITests/PortableInstaller.cpp | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/AppInstallerCLITests/PortableInstaller.cpp b/src/AppInstallerCLITests/PortableInstaller.cpp index 8fe9a9e333..a9953daae9 100644 --- a/src/AppInstallerCLITests/PortableInstaller.cpp +++ b/src/AppInstallerCLITests/PortableInstaller.cpp @@ -2,12 +2,12 @@ // Licensed under the MIT License. #include "pch.h" #include "TestCommon.h" -#include -#include #include #include -#include #include +#include +#include +#include #include #include #include @@ -171,12 +171,4 @@ TEST_CASE("PortableInstaller_InstallToIndex_ExistingInstallRoot", "[PortableInst REQUIRE_FALSE(AppInstaller::Filesystem::SymlinkExists(symlinkPath)); REQUIRE_FALSE(AppInstaller::Filesystem::SymlinkExists(symlinkPath2)); REQUIRE_FALSE(std::filesystem::exists(directoryPath)); -} - - - -// Tests -// Add an extra file that has not been tracked and verify that target directory has not been removed -// check verify portable files returns false if one of the files has been modified -// - +} \ No newline at end of file From ba5fbcc685e1a9638bd903b47d959899ad48c78e Mon Sep 17 00:00:00 2001 From: Ryan Fu Date: Tue, 20 Sep 2022 18:35:13 -0700 Subject: [PATCH 18/22] fix E2e tests --- .../ExecutionContextData.h | 7 -- src/AppInstallerCLICore/PortableInstaller.cpp | 91 +++++++------------ src/AppInstallerCLICore/PortableInstaller.h | 19 ++-- .../Workflows/PortableFlow.cpp | 6 +- src/AppInstallerCLIE2ETests/InstallCommand.cs | 8 +- .../Public/winget/Filesystem.h | 1 + .../Public/winget/PortableFileEntry.h | 5 +- 7 files changed, 54 insertions(+), 83 deletions(-) diff --git a/src/AppInstallerCLICore/ExecutionContextData.h b/src/AppInstallerCLICore/ExecutionContextData.h index fa6f441f92..9ab78e1873 100644 --- a/src/AppInstallerCLICore/ExecutionContextData.h +++ b/src/AppInstallerCLICore/ExecutionContextData.h @@ -55,7 +55,6 @@ namespace AppInstaller::CLI::Execution AllowedArchitectures, AllowUnknownScope, PortableInstaller, - ExtractedItems, Max }; @@ -230,11 +229,5 @@ namespace AppInstaller::CLI::Execution { using value_t = CLI::Portable::PortableInstaller; }; - - template <> - struct DataMapping - { - using value_t = std::vector; - }; } } diff --git a/src/AppInstallerCLICore/PortableInstaller.cpp b/src/AppInstallerCLICore/PortableInstaller.cpp index 56651baab6..1e52bc5e52 100644 --- a/src/AppInstallerCLICore/PortableInstaller.cpp +++ b/src/AppInstallerCLICore/PortableInstaller.cpp @@ -22,20 +22,6 @@ using namespace AppInstaller::Repository::Microsoft::Schema; namespace AppInstaller::CLI::Portable { - namespace - { - constexpr std::wstring_view c_PortableIndexFileName = L"portable.db"; - - PortableIndex CreateOrOpenPortableIndex(const std::filesystem::path& indexPath) - { - PortableIndex index = std::filesystem::exists(indexPath) ? - PortableIndex::Open(indexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite) : - PortableIndex::CreateNew(indexPath.u8string()); - - return index; - } - } - std::filesystem::path GetPortableLinksLocation(Manifest::ScopeEnum scope) { if (scope == Manifest::ScopeEnum::Machine) @@ -120,13 +106,11 @@ namespace AppInstaller::CLI::Portable } else if (entry.FileType == PortableFileType::Symlink && !InstallDirectoryAddedToPath) { - const std::filesystem::path& symlinkPath = entry.SymlinkTarget; - - std::filesystem::file_status status = std::filesystem::status(symlinkPath); + std::filesystem::file_status status = std::filesystem::status(filePath); if (std::filesystem::is_directory(status)) { - AICLI_LOG(CLI, Info, << "Unable to create symlink. '" << symlinkPath << "points to an existing directory."); - throw APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY; + AICLI_LOG(CLI, Info, << "Unable to create symlink. '" << filePath << "points to an existing directory."); + THROW_HR(APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY); } if (!RecordToIndex) @@ -134,7 +118,23 @@ namespace AppInstaller::CLI::Portable CommitToARPEntry(PortableValueName::PortableSymlinkFullPath, filePath); } - PortableInstaller::CreatePortableSymlink(entry.SymlinkTarget, filePath); + if (std::filesystem::remove(filePath)) + { + AICLI_LOG(CLI, Info, << "Removed existing file at " << filePath); + m_stream << Resource::String::OverwritingExistingFileAtMessage << ' ' << filePath << std::endl; + } + + if (Filesystem::CreateSymlink(entry.SymlinkTarget, filePath)) + { + AICLI_LOG(Core, Info, << "Symlink created at: " << filePath); + } + else + { + // Symlink creation should only fail if the user executes without admin rights or developer mode. + // Resort to adding install directory to PATH directly. + AICLI_LOG(Core, Info, << "Portable install executed in user mode. Adding package directory to PATH."); + CommitToARPEntry(PortableValueName::InstallDirectoryAddedToPath, InstallDirectoryAddedToPath = true); + } } } @@ -167,7 +167,8 @@ namespace AppInstaller::CLI::Portable { bool deleteIndex = false; { - PortableIndex existingIndex = CreateOrOpenPortableIndex(existingIndexPath); + PortableIndex existingIndex = PortableIndex::Open(existingIndexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); + for (auto expectedEntry : m_expectedEntries) { RemoveFile(expectedEntry); @@ -200,10 +201,13 @@ namespace AppInstaller::CLI::Portable if (RecordToIndex) { std::filesystem::path targetIndexPath = TargetInstallLocation / GetPortableIndexFileName(); - PortableIndex index = CreateOrOpenPortableIndex(targetIndexPath); + PortableIndex targetIndex = std::filesystem::exists(targetIndexPath) ? + PortableIndex::Open(targetIndexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite) : + PortableIndex::CreateNew(targetIndexPath.u8string()); + for (auto desiredEntry : m_desiredEntries) { - index.AddOrUpdatePortableFile(desiredEntry); + targetIndex.AddOrUpdatePortableFile(desiredEntry); InstallFile(desiredEntry); } } @@ -232,7 +236,7 @@ namespace AppInstaller::CLI::Portable void PortableInstaller::Install() { - InitializeRegistryEntry(); + RegisterARPEntry(); CreateTargetInstallDirectory(); @@ -290,29 +294,6 @@ namespace AppInstaller::CLI::Portable } } - bool PortableInstaller::CreatePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath) - { - if (std::filesystem::remove(symlinkPath)) - { - AICLI_LOG(CLI, Info, << "Removed existing file at " << symlinkPath); - m_stream << Resource::String::OverwritingExistingFileAtMessage << ' ' << symlinkPath.u8string() << std::endl; - } - - if (Filesystem::CreateSymlink(targetPath, symlinkPath)) - { - AICLI_LOG(Core, Info, << "Symlink created at: " << symlinkPath); - return true; - } - else - { - // Symlink creation should only fail if the user executes without admin rights or developer mode. - // Resort to adding install directory to PATH directly. - AICLI_LOG(Core, Info, << "Portable install executed in user mode. Adding package directory to PATH."); - CommitToARPEntry(PortableValueName::InstallDirectoryAddedToPath, InstallDirectoryAddedToPath = true); - return false; - } - } - void PortableInstaller::AddToPathVariable() { const std::filesystem::path& pathValue = InstallDirectoryAddedToPath ? TargetInstallLocation : GetPortableLinksLocation(GetScope()); @@ -376,16 +357,14 @@ namespace AppInstaller::CLI::Portable HelpLink = manifest.CurrentLocalization.Get(); } - std::vector PortableInstaller::GetExpectedState() + void PortableInstaller::SetExpectedState() { - std::vector entries; - const auto& indexPath = InstallLocation / GetPortableIndexFileName(); if (std::filesystem::exists(indexPath)) { PortableIndex portableIndex = PortableIndex::Open(indexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); - entries = portableIndex.GetAllPortableFiles(); + m_expectedEntries = portableIndex.GetAllPortableFiles(); } else { @@ -394,16 +373,14 @@ namespace AppInstaller::CLI::Portable if (!symlinkFullPath.empty()) { - entries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkFullPath, targetFullPath))); + m_expectedEntries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkFullPath, targetFullPath))); } if (!targetFullPath.empty()) { - entries.emplace_back(std::move(PortableFileEntry::CreateFileEntry({}, targetFullPath, SHA256))); + m_expectedEntries.emplace_back(std::move(PortableFileEntry::CreateFileEntry({}, targetFullPath, SHA256))); } } - - return entries; } PortableInstaller::PortableInstaller(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode) : @@ -429,10 +406,10 @@ namespace AppInstaller::CLI::Portable InstallDirectoryCreated = GetBoolValue(PortableValueName::InstallDirectoryCreated); } - m_expectedEntries = GetExpectedState(); + SetExpectedState(); } - void PortableInstaller::InitializeRegistryEntry() + void PortableInstaller::RegisterARPEntry() { CommitToARPEntry(PortableValueName::WinGetPackageIdentifier, WinGetPackageIdentifier); CommitToARPEntry(PortableValueName::WinGetSourceIdentifier, WinGetSourceIdentifier); diff --git a/src/AppInstallerCLICore/PortableInstaller.h b/src/AppInstallerCLICore/PortableInstaller.h index f2ac2d310b..94de929d95 100644 --- a/src/AppInstallerCLICore/PortableInstaller.h +++ b/src/AppInstallerCLICore/PortableInstaller.h @@ -42,10 +42,6 @@ namespace AppInstaller::CLI::Portable // This is the incoming target install location determined from the context args. std::filesystem::path TargetInstallLocation; - void InstallFile(AppInstaller::Portable::PortableFileEntry& desiredState); - - void RemoveFile(AppInstaller::Portable::PortableFileEntry& desiredState); - PortableInstaller(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode); bool VerifyExpectedState(); @@ -55,6 +51,12 @@ namespace AppInstaller::CLI::Portable m_desiredEntries = desiredEntries; }; + void PrepareForCleanUp() + { + m_expectedEntries = m_desiredEntries; + m_desiredEntries = {}; + } + void Install(); void Uninstall(); @@ -102,17 +104,16 @@ namespace AppInstaller::CLI::Portable std::filesystem::path GetPathValue(PortableValueName valueName); bool GetBoolValue(PortableValueName valueName); - std::vector GetExpectedState(); - - void InitializeRegistryEntry(); + void SetExpectedState(); + void RegisterARPEntry(); void ApplyDesiredState(); + void InstallFile(AppInstaller::Portable::PortableFileEntry& desiredState); + void RemoveFile(AppInstaller::Portable::PortableFileEntry& desiredState); void CreateTargetInstallDirectory(); void RemoveInstallDirectory(); - bool CreatePortableSymlink(const std::filesystem::path& targetPath, const std::filesystem::path& symlinkPath); - void AddToPathVariable(); void RemoveFromPathVariable(); }; diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp index d8c9de350d..21640d8042 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -161,7 +161,6 @@ namespace AppInstaller::CLI::Workflow context.Add(std::move(portableInstaller)); } - std::vector GetDesiredStateForPortableInstall(Execution::Context& context) { std::filesystem::path& installerPath = context.Get(); @@ -278,7 +277,6 @@ namespace AppInstaller::CLI::Workflow } portableInstaller.Install(); - context.Add(ERROR_SUCCESS); context.Reporter.Warn() << portableInstaller.GetOutputMessage(); } @@ -289,7 +287,9 @@ namespace AppInstaller::CLI::Workflow if (!portableInstaller.IsUpdate) { context.Reporter.Warn() << Resource::String::PortableInstallFailed << std::endl; - portableInstaller.Uninstall(); + portableInstaller.PrepareForCleanUp(); +; portableInstaller.Uninstall(); + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED); } } } diff --git a/src/AppInstallerCLIE2ETests/InstallCommand.cs b/src/AppInstallerCLIE2ETests/InstallCommand.cs index 29460676cb..56655dfdba 100644 --- a/src/AppInstallerCLIE2ETests/InstallCommand.cs +++ b/src/AppInstallerCLIE2ETests/InstallCommand.cs @@ -181,7 +181,7 @@ public void InstallPortableExeWithCommand() var result = TestCommon.RunAICLICommand("install", $"{packageId} -l {installDir}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(result.StdOut.Contains("Successfully installed")); - TestCommon.VerifyPortablePackage(Path.Combine(installDir, productCode), commandAlias, fileName, productCode, true); + TestCommon.VerifyPortablePackage(installDir, commandAlias, fileName, productCode, true); } [Test] @@ -196,7 +196,7 @@ public void InstallPortableExeWithRename() var result = TestCommon.RunAICLICommand("install", $"{packageId} -l {installDir} --rename {renameArgValue}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(result.StdOut.Contains("Successfully installed")); - TestCommon.VerifyPortablePackage(Path.Combine(installDir, productCode), renameArgValue, renameArgValue, productCode, true); + TestCommon.VerifyPortablePackage(installDir, renameArgValue, renameArgValue, productCode, true); } [Test] @@ -240,7 +240,7 @@ public void InstallPortableToExistingDirectory() var result = TestCommon.RunAICLICommand("install", $"{packageId} -l {existingDir}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(result.StdOut.Contains("Successfully installed")); - TestCommon.VerifyPortablePackage(Path.Combine(existingDir, productCode), commandAlias, fileName, productCode, true); + TestCommon.VerifyPortablePackage(existingDir, commandAlias, fileName, productCode, true); } [Test] @@ -253,6 +253,7 @@ public void InstallPortableFailsWithCleanup() // Create a directory with the same name as the symlink in order to cause install to fail. string symlinkDirectory = TestCommon.GetPortableSymlinkDirectory(TestCommon.Scope.User); string conflictDirectory = Path.Combine(symlinkDirectory, commandAlias); + Directory.CreateDirectory(conflictDirectory); var result = TestCommon.RunAICLICommand("install", $"{packageId}"); @@ -287,7 +288,6 @@ public void ReinstallPortable() var result2 = TestCommon.RunAICLICommand("install", $"{packageId}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result2.ExitCode); Assert.True(result2.StdOut.Contains("Successfully installed")); - Assert.True(result2.StdOut.Contains($"Overwriting existing file: {symlinkPath}")); TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true); } diff --git a/src/AppInstallerCommonCore/Public/winget/Filesystem.h b/src/AppInstallerCommonCore/Public/winget/Filesystem.h index 533c8da506..e4e859891b 100644 --- a/src/AppInstallerCommonCore/Public/winget/Filesystem.h +++ b/src/AppInstallerCommonCore/Public/winget/Filesystem.h @@ -31,6 +31,7 @@ namespace AppInstaller::Filesystem // Checks if the path is a symlink and exists. bool SymlinkExists(const std::filesystem::path& symlinkPath); + bool CreateSymlink(const std::filesystem::path& path, const std::filesystem::path& target); // Get expanded file system path. std::filesystem::path GetExpandedPath(const std::string& path); diff --git a/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h b/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h index 2c65426390..a93ad9f3f0 100644 --- a/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h +++ b/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h @@ -29,12 +29,11 @@ namespace AppInstaller::Portable { if (FileType == PortableFileType::Symlink) { - // weakly_canonical will resolve the symlink path to its target if it exists, set path directly. - m_filePath = path; + m_filePath = std::filesystem::weakly_canonical(path); } else { - m_filePath = std::filesystem::weakly_canonical(path); + m_filePath = path; } }; From 65bb6f659ff23c945c4a9d48a0fb7e97f6e692cd Mon Sep 17 00:00:00 2001 From: Ryan Fu Date: Wed, 21 Sep 2022 09:45:44 -0700 Subject: [PATCH 19/22] fix unit --- src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h b/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h index a93ad9f3f0..6c7976d2db 100644 --- a/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h +++ b/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h @@ -27,7 +27,7 @@ namespace AppInstaller::Portable void SetFilePath(const std::filesystem::path& path) { - if (FileType == PortableFileType::Symlink) + if (FileType != PortableFileType::Symlink) { m_filePath = std::filesystem::weakly_canonical(path); } From 63420e2e2dceb6de28c0b44981a1c77939334ddf Mon Sep 17 00:00:00 2001 From: Ryan Fu Date: Mon, 26 Sep 2022 12:07:14 -0700 Subject: [PATCH 20/22] add duplicate command alias check and move resources.pri file --- azure-pipelines.yml | 8 +++++++ src/AppInstallerCLICore/PortableInstaller.h | 2 +- src/AppInstallerCLICore/Resources.h | 2 ++ .../Workflows/ArchiveFlow.cpp | 2 ++ .../Shared/Strings/en-us/winget.resw | 6 +++++ .../AppInstallerCLITests.vcxproj | 3 +++ .../AppInstallerCLITests.vcxproj.filters | 3 +++ ...nstallerTypeZip-DuplicateCommandAlias.yaml | 22 +++++++++++++++++++ src/AppInstallerCLITests/YamlManifest.cpp | 1 + .../Manifest/ManifestValidation.cpp | 16 ++++++++++++++ .../Public/winget/ManifestValidation.h | 1 + 11 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 src/AppInstallerCLITests/TestData/Manifest-Bad-InstallerTypeZip-DuplicateCommandAlias.yaml diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b8aed9dfaa..4c5ad54a61 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -252,6 +252,14 @@ jobs: TargetFolder: 'src\AppInstallerCLIPackage\bin\$(buildPlatform)\$(buildConfiguration)' Contents: Microsoft.Management.Deployment.winmd + # Resolves resource strings utilized by InProc E2E tests. + - task: CopyFiles@2 + displayName: 'Copy resources.pri to dotnet directory' + inputs: + SourceFolder: '$(buildOutDir)\AppInstallerCLI' + TargetFolder: '$(platformProgramFiles)\dotnet' + Contents: resources.pri + - template: templates/e2e-test.template.yml parameters: title: "COM API E2E Tests (Out-of-process)" diff --git a/src/AppInstallerCLICore/PortableInstaller.h b/src/AppInstallerCLICore/PortableInstaller.h index 94de929d95..664720db38 100644 --- a/src/AppInstallerCLICore/PortableInstaller.h +++ b/src/AppInstallerCLICore/PortableInstaller.h @@ -46,7 +46,7 @@ namespace AppInstaller::CLI::Portable bool VerifyExpectedState(); - void SetDesiredState(std::vector& desiredEntries) + void SetDesiredState(const std::vector& desiredEntries) { m_desiredEntries = desiredEntries; }; diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index ac17ce1088..e40ae0bb7e 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -71,6 +71,8 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ExportSourceArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExternalDependencies); WINGET_DEFINE_RESOURCE_STRINGID(ExtractArchiveFailed); + WINGET_DEFINE_RESOURCE_STRINGID(ExtractArchiveSucceeded); + WINGET_DEFINE_RESOURCE_STRINGID(ExtractingArchive); WINGET_DEFINE_RESOURCE_STRINGID(ExtraPositionalError); WINGET_DEFINE_RESOURCE_STRINGID(FeatureDisabledByAdminSettingMessage); WINGET_DEFINE_RESOURCE_STRINGID(FeatureDisabledMessage); diff --git a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp index ace627f14e..6b54656e5b 100644 --- a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp @@ -22,11 +22,13 @@ namespace AppInstaller::CLI::Workflow 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 { diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index af4b38343e..ff41cfe009 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -1432,4 +1432,10 @@ Please specify one of them using the `--source` option to proceed. Portable package from a different source already exists + + Successfully extracted archive + + + Extracting archive... + \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index 7f8cc6b45b..c7be683a7b 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -439,6 +439,9 @@ true + + true + true diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters index e7fb3b8f1e..ea20407d23 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -318,6 +318,9 @@ TestData + + TestData + TestData diff --git a/src/AppInstallerCLITests/TestData/Manifest-Bad-InstallerTypeZip-DuplicateCommandAlias.yaml b/src/AppInstallerCLITests/TestData/Manifest-Bad-InstallerTypeZip-DuplicateCommandAlias.yaml new file mode 100644 index 0000000000..5c721c9d74 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/Manifest-Bad-InstallerTypeZip-DuplicateCommandAlias.yaml @@ -0,0 +1,22 @@ +# Bad manifest. Installer type zip should not have any duplicate PortableCommandAlias values. +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: AppInstaller Test Installer +Publisher: Microsoft Corporation +Moniker: AICLITestExe +License: Test +ShortDescription: Test installer for zip without nestedInstallers specified +Scope: User +Installers: + - Architecture: x64 + InstallerUrl: https://ThisIsNotUsed + InstallerType: zip + InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B + NestedInstallerFiles: + - RelativeFilePath: relativeFilePath1 + PortableCommandAlias: duplicateAlias + - RelativeFilePath: relativeFilePath2 + PortableCommandAlias: duplicateAlias +ManifestType: singleton +ManifestVersion: 1.4.0 \ No newline at end of file diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp index f80e60fd2e..d7bac33274 100644 --- a/src/AppInstallerCLITests/YamlManifest.cpp +++ b/src/AppInstallerCLITests/YamlManifest.cpp @@ -275,6 +275,7 @@ TEST_CASE("ReadBadManifests", "[ManifestValidation]") { "Manifest-Bad-InstallerTypePortable-InvalidAppsAndFeatures.yaml", "Only zero or one entry for Apps and Features may be specified for InstallerType portable." }, { "Manifest-Bad-InstallerTypePortable-InvalidCommands.yaml", "Only zero or one value for Commands may be specified for InstallerType portable." }, { "Manifest-Bad-InstallerTypePortable-InvalidScope.yaml", "Scope is not supported for InstallerType portable." }, + { "Manifest-Bad-InstallerTypeZip-DuplicateCommandAlias.yaml", "Duplicate portable command alias found." }, { "Manifest-Bad-InstallerTypeZip-InvalidRelativeFilePath.yaml", "Relative file path must not point to a location outside of archive directory" }, { "Manifest-Bad-InstallerTypeZip-MissingRelativeFilePath.yaml", "Required field missing. [RelativeFilePath]" }, { "Manifest-Bad-InstallerTypeZip-MultipleNestedInstallers.yaml", "Only one entry for NestedInstallerFiles can be specified for non-portable InstallerTypes." }, diff --git a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp index c8b6b1c8aa..f459d6b649 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp @@ -32,6 +32,7 @@ namespace AppInstaller::Manifest { AppInstaller::Manifest::ManifestError::InstallerTypeDoesNotWriteAppsAndFeaturesEntry, "The specified installer type does not write to Apps and Features entry."sv }, { AppInstaller::Manifest::ManifestError::IncompleteMultiFileManifest, "The multi file manifest is incomplete.A multi file manifest must contain at least version, installer and defaultLocale manifest."sv }, { AppInstaller::Manifest::ManifestError::InconsistentMultiFileManifestFieldValue, "The multi file manifest has inconsistent field values."sv }, + { AppInstaller::Manifest::ManifestError::DuplicatePortableCommandAlias, "Duplicate portable command alias found."sv }, { AppInstaller::Manifest::ManifestError::DuplicateMultiFileManifestType, "The multi file manifest should contain only one file with the particular ManifestType."sv }, { AppInstaller::Manifest::ManifestError::DuplicateMultiFileManifestLocale, "The multi file manifest contains duplicate PackageLocale."sv }, { AppInstaller::Manifest::ManifestError::UnsupportedMultiFileManifestType, "The multi file manifest should not contain file with the particular ManifestType."sv }, @@ -260,6 +261,8 @@ namespace AppInstaller::Manifest resultErrors.emplace_back(ManifestError::ExceededNestedInstallerFilesLimit, "NestedInstallerFiles"); } + std::unordered_set commandAliasSet; + for (const auto& nestedInstallerFile : installer.NestedInstallerFiles) { if (nestedInstallerFile.RelativeFilePath.empty()) @@ -275,6 +278,19 @@ namespace AppInstaller::Manifest resultErrors.emplace_back(ManifestError::RelativeFilePathEscapesDirectory, "RelativeFilePath"); } } + + const std::string& alias = nestedInstallerFile.PortableCommandAlias; + if (!alias.empty()) + { + if (commandAliasSet.find(alias) == commandAliasSet.end()) + { + commandAliasSet.insert(alias); + } + else + { + resultErrors.emplace_back(ManifestError::DuplicatePortableCommandAlias, "PortableCommandAlias"); + } + } } } diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h b/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h index ddc01d5425..13062d8a5c 100644 --- a/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h +++ b/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h @@ -23,6 +23,7 @@ namespace AppInstaller::Manifest WINGET_DEFINE_RESOURCE_STRINGID(ArpVersionOverlapWithIndex); WINGET_DEFINE_RESOURCE_STRINGID(ArpVersionValidationInternalError); WINGET_DEFINE_RESOURCE_STRINGID(BothAllowedAndExcludedMarketsDefined); + WINGET_DEFINE_RESOURCE_STRINGID(DuplicatePortableCommandAlias); WINGET_DEFINE_RESOURCE_STRINGID(DuplicateMultiFileManifestLocale); WINGET_DEFINE_RESOURCE_STRINGID(DuplicateMultiFileManifestType); WINGET_DEFINE_RESOURCE_STRINGID(DuplicateInstallerEntry); From 13ef53610622c49a6923699d9b505c73ebf2be5a Mon Sep 17 00:00:00 2001 From: Ryan Fu Date: Mon, 26 Sep 2022 14:14:21 -0700 Subject: [PATCH 21/22] change validation check to toLower --- azure-pipelines.yml | 16 ++++++++-------- src/AppInstallerCLICore/PortableInstaller.cpp | 1 + ...d-InstallerTypeZip-DuplicateCommandAlias.yaml | 2 +- .../Manifest/ManifestValidation.cpp | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 4c5ad54a61..08c9f3e02e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -219,6 +219,14 @@ jobs: isPackaged: true filter: "TestCategory!=InProcess&TestCategory!=OutOfProcess" + # Resolves resource strings utilized by InProc E2E tests. + - task: CopyFiles@2 + displayName: 'Copy resources.pri to dotnet directory' + inputs: + SourceFolder: '$(buildOutDir)\AppInstallerCLI' + TargetFolder: '$(platformProgramFiles)\dotnet' + Contents: resources.pri + - template: templates/e2e-test.template.yml parameters: title: "COM API E2E Tests (In-process)" @@ -252,14 +260,6 @@ jobs: TargetFolder: 'src\AppInstallerCLIPackage\bin\$(buildPlatform)\$(buildConfiguration)' Contents: Microsoft.Management.Deployment.winmd - # Resolves resource strings utilized by InProc E2E tests. - - task: CopyFiles@2 - displayName: 'Copy resources.pri to dotnet directory' - inputs: - SourceFolder: '$(buildOutDir)\AppInstallerCLI' - TargetFolder: '$(platformProgramFiles)\dotnet' - Contents: resources.pri - - template: templates/e2e-test.template.yml parameters: title: "COM API E2E Tests (Out-of-process)" diff --git a/src/AppInstallerCLICore/PortableInstaller.cpp b/src/AppInstallerCLICore/PortableInstaller.cpp index 1e52bc5e52..4d50bf0045 100644 --- a/src/AppInstallerCLICore/PortableInstaller.cpp +++ b/src/AppInstallerCLICore/PortableInstaller.cpp @@ -160,6 +160,7 @@ namespace AppInstaller::CLI::Portable } } + // TODO: Optimize by applying the difference between expected and desired state. void PortableInstaller::ApplyDesiredState() { std::filesystem::path existingIndexPath = InstallLocation / GetPortableIndexFileName(); diff --git a/src/AppInstallerCLITests/TestData/Manifest-Bad-InstallerTypeZip-DuplicateCommandAlias.yaml b/src/AppInstallerCLITests/TestData/Manifest-Bad-InstallerTypeZip-DuplicateCommandAlias.yaml index 5c721c9d74..71f230b868 100644 --- a/src/AppInstallerCLITests/TestData/Manifest-Bad-InstallerTypeZip-DuplicateCommandAlias.yaml +++ b/src/AppInstallerCLITests/TestData/Manifest-Bad-InstallerTypeZip-DuplicateCommandAlias.yaml @@ -15,7 +15,7 @@ Installers: InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B NestedInstallerFiles: - RelativeFilePath: relativeFilePath1 - PortableCommandAlias: duplicateAlias + PortableCommandAlias: DUPLICATEALIAS - RelativeFilePath: relativeFilePath2 PortableCommandAlias: duplicateAlias ManifestType: singleton diff --git a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp index f459d6b649..ce0ed1be54 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp @@ -279,7 +279,7 @@ namespace AppInstaller::Manifest } } - const std::string& alias = nestedInstallerFile.PortableCommandAlias; + const auto& alias = Utility::ToLower(nestedInstallerFile.PortableCommandAlias); if (!alias.empty()) { if (commandAliasSet.find(alias) == commandAliasSet.end()) From 7f6e69715c62545d84c8f26d08082b7caadc9544 Mon Sep 17 00:00:00 2001 From: Ryan Fu Date: Mon, 26 Sep 2022 17:23:07 -0700 Subject: [PATCH 22/22] fix pipeline, e2e inproc, and yaml validation --- azure-pipelines.yml | 22 +++++------ .../Interop/InstallInterop.cs | 4 +- .../Interop/UninstallInterop.cs | 2 +- .../Interop/UpgradeInterop.cs | 2 +- .../AppInstallerCLITests.vcxproj | 3 ++ .../AppInstallerCLITests.vcxproj.filters | 3 ++ ...llerTypeZip-DuplicateRelativeFilePath.yaml | 22 +++++++++++ src/AppInstallerCLITests/YamlManifest.cpp | 1 + .../Manifest/ManifestValidation.cpp | 37 ++++++++++--------- .../Public/winget/ManifestValidation.h | 1 + 10 files changed, 65 insertions(+), 32 deletions(-) create mode 100644 src/AppInstallerCLITests/TestData/Manifest-Bad-InstallerTypeZip-DuplicateRelativeFilePath.yaml diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 08c9f3e02e..d5a657f19b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -219,6 +219,17 @@ jobs: isPackaged: true filter: "TestCategory!=InProcess&TestCategory!=OutOfProcess" + - task: PowerShell@2 + displayName: 'Set program files directory' + inputs: + targetType: 'inline' + script: | + if ("$(buildPlatform)" -eq "x86") { + Write-Host "##vso[task.setvariable variable=platformProgramFiles;]${env:ProgramFiles(x86)}" + } else { + Write-Host "##vso[task.setvariable variable=platformProgramFiles;]${env:ProgramFiles}" + } + # Resolves resource strings utilized by InProc E2E tests. - task: CopyFiles@2 displayName: 'Copy resources.pri to dotnet directory' @@ -233,17 +244,6 @@ jobs: isPackaged: false filter: "TestCategory=InProcess" - - task: PowerShell@2 - displayName: 'Set program files directory' - inputs: - targetType: 'inline' - script: | - if ("$(buildPlatform)" -eq "x86") { - Write-Host "##vso[task.setvariable variable=platformProgramFiles;]${env:ProgramFiles(x86)}" - } else { - Write-Host "##vso[task.setvariable variable=platformProgramFiles;]${env:ProgramFiles}" - } - # Winmd accessed by test runner process (dotnet.exe) - task: CopyFiles@2 displayName: 'Copy winmd to dotnet directory' diff --git a/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs b/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs index 4f315714a3..23aa75e842 100644 --- a/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs +++ b/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs @@ -283,7 +283,7 @@ public async Task InstallPortableExeWithCommand() // Assert Assert.AreEqual(InstallResultStatus.Ok, installResult.Status); - TestCommon.VerifyPortablePackage(Path.Combine(installDir, Constants.PortableExePackageDirName), commandAlias, fileName, productCode, true); + TestCommon.VerifyPortablePackage(installDir, commandAlias, fileName, productCode, true); } [Test] @@ -308,7 +308,7 @@ public async Task InstallPortableToExistingDirectory() // Assert Assert.AreEqual(InstallResultStatus.Ok, installResult.Status); - TestCommon.VerifyPortablePackage(Path.Combine(existingDir, Constants.PortableExePackageDirName), commandAlias, fileName, productCode, true); + TestCommon.VerifyPortablePackage(existingDir, commandAlias, fileName, productCode, true); } [Test] diff --git a/src/AppInstallerCLIE2ETests/Interop/UninstallInterop.cs b/src/AppInstallerCLIE2ETests/Interop/UninstallInterop.cs index 486118aa95..9ae0deba8d 100644 --- a/src/AppInstallerCLIE2ETests/Interop/UninstallInterop.cs +++ b/src/AppInstallerCLIE2ETests/Interop/UninstallInterop.cs @@ -198,7 +198,7 @@ public async Task UninstallPortableModifiedSymlink() // Uninstall var uninstallResult = await packageManager.UninstallPackageAsync(searchResult.CatalogPackage, TestFactory.CreateUninstallOptions()); - Assert.AreEqual(UninstallResultStatus.Ok, uninstallResult.Status); + Assert.AreEqual(UninstallResultStatus.UninstallError, uninstallResult.Status); Assert.True(modifiedSymlinkInfo.Exists, "Modified symlink should still exist"); // Remove modified symlink as to not interfere with other tests diff --git a/src/AppInstallerCLIE2ETests/Interop/UpgradeInterop.cs b/src/AppInstallerCLIE2ETests/Interop/UpgradeInterop.cs index ee22d312f0..8b7beab395 100644 --- a/src/AppInstallerCLIE2ETests/Interop/UpgradeInterop.cs +++ b/src/AppInstallerCLIE2ETests/Interop/UpgradeInterop.cs @@ -108,7 +108,7 @@ public async Task UpgradePortableARPMismatch() // Upgrade var upgradeResult = await packageManager.UpgradePackageAsync(searchResult.CatalogPackage, upgradeOptions); Assert.AreEqual(InstallResultStatus.InstallError, upgradeResult.Status); - Assert.AreEqual(Constants.ErrorCode.ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS, (int)upgradeResult.InstallerErrorCode); + Assert.AreEqual(Constants.ErrorCode.ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS, upgradeResult.ExtendedErrorCode.HResult); // Find package again, it should have not been upgraded searchResult = FindOnePackage(compositeSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, packageId); diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index c7be683a7b..fbf1d266d4 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -442,6 +442,9 @@ true + + true + true diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters index ea20407d23..ece41c6e45 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -321,6 +321,9 @@ TestData + + TestData + TestData diff --git a/src/AppInstallerCLITests/TestData/Manifest-Bad-InstallerTypeZip-DuplicateRelativeFilePath.yaml b/src/AppInstallerCLITests/TestData/Manifest-Bad-InstallerTypeZip-DuplicateRelativeFilePath.yaml new file mode 100644 index 0000000000..81286b78f8 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/Manifest-Bad-InstallerTypeZip-DuplicateRelativeFilePath.yaml @@ -0,0 +1,22 @@ +# Bad manifest. Installer type zip should not have any duplicate PortableCommandAlias values. +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: AppInstaller Test Installer +Publisher: Microsoft Corporation +Moniker: AICLITestExe +License: Test +ShortDescription: Test installer for zip without nestedInstallers specified +Scope: User +Installers: + - Architecture: x64 + InstallerUrl: https://ThisIsNotUsed + InstallerType: zip + InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B + NestedInstallerFiles: + - RelativeFilePath: RELATIVEFILEPATH + PortableCommandAlias: alias1 + - RelativeFilePath: relativefilepath + PortableCommandAlias: alias2 +ManifestType: singleton +ManifestVersion: 1.4.0 \ No newline at end of file diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp index d7bac33274..d4edac4537 100644 --- a/src/AppInstallerCLITests/YamlManifest.cpp +++ b/src/AppInstallerCLITests/YamlManifest.cpp @@ -276,6 +276,7 @@ TEST_CASE("ReadBadManifests", "[ManifestValidation]") { "Manifest-Bad-InstallerTypePortable-InvalidCommands.yaml", "Only zero or one value for Commands may be specified for InstallerType portable." }, { "Manifest-Bad-InstallerTypePortable-InvalidScope.yaml", "Scope is not supported for InstallerType portable." }, { "Manifest-Bad-InstallerTypeZip-DuplicateCommandAlias.yaml", "Duplicate portable command alias found." }, + { "Manifest-Bad-InstallerTypeZip-DuplicateRelativeFilePath.yaml", "Duplicate relative file path found." }, { "Manifest-Bad-InstallerTypeZip-InvalidRelativeFilePath.yaml", "Relative file path must not point to a location outside of archive directory" }, { "Manifest-Bad-InstallerTypeZip-MissingRelativeFilePath.yaml", "Required field missing. [RelativeFilePath]" }, { "Manifest-Bad-InstallerTypeZip-MultipleNestedInstallers.yaml", "Only one entry for NestedInstallerFiles can be specified for non-portable InstallerTypes." }, diff --git a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp index ce0ed1be54..f27088b566 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp @@ -33,6 +33,7 @@ namespace AppInstaller::Manifest { AppInstaller::Manifest::ManifestError::IncompleteMultiFileManifest, "The multi file manifest is incomplete.A multi file manifest must contain at least version, installer and defaultLocale manifest."sv }, { AppInstaller::Manifest::ManifestError::InconsistentMultiFileManifestFieldValue, "The multi file manifest has inconsistent field values."sv }, { AppInstaller::Manifest::ManifestError::DuplicatePortableCommandAlias, "Duplicate portable command alias found."sv }, + { AppInstaller::Manifest::ManifestError::DuplicateRelativeFilePath, "Duplicate relative file path found."sv }, { AppInstaller::Manifest::ManifestError::DuplicateMultiFileManifestType, "The multi file manifest should contain only one file with the particular ManifestType."sv }, { AppInstaller::Manifest::ManifestError::DuplicateMultiFileManifestLocale, "The multi file manifest contains duplicate PackageLocale."sv }, { AppInstaller::Manifest::ManifestError::UnsupportedMultiFileManifestType, "The multi file manifest should not contain file with the particular ManifestType."sv }, @@ -261,35 +262,37 @@ namespace AppInstaller::Manifest resultErrors.emplace_back(ManifestError::ExceededNestedInstallerFilesLimit, "NestedInstallerFiles"); } - std::unordered_set commandAliasSet; + std::set commandAliasSet; + std::set relativeFilePathSet; for (const auto& nestedInstallerFile : installer.NestedInstallerFiles) { if (nestedInstallerFile.RelativeFilePath.empty()) { resultErrors.emplace_back(ManifestError::RequiredFieldMissing, "RelativeFilePath"); + break; } - else + + // Check that the relative file path does not escape base directory. + const std::filesystem::path& basePath = std::filesystem::current_path(); + const std::filesystem::path& fullPath = basePath / ConvertToUTF16(nestedInstallerFile.RelativeFilePath); + if (AppInstaller::Filesystem::PathEscapesBaseDirectory(fullPath, basePath)) { - const std::filesystem::path& basePath = std::filesystem::current_path(); - const std::filesystem::path& fullPath = basePath / ConvertToUTF16(nestedInstallerFile.RelativeFilePath); - if (AppInstaller::Filesystem::PathEscapesBaseDirectory(fullPath, basePath)) - { - resultErrors.emplace_back(ManifestError::RelativeFilePathEscapesDirectory, "RelativeFilePath"); - } + resultErrors.emplace_back(ManifestError::RelativeFilePathEscapesDirectory, "RelativeFilePath"); + } + + // Check for duplicate relative filepath values. + if (!relativeFilePathSet.insert(Utility::ToLower(nestedInstallerFile.RelativeFilePath)).second) + { + resultErrors.emplace_back(ManifestError::DuplicateRelativeFilePath, "RelativeFilePath"); } + // Check for duplicate portable command alias values. const auto& alias = Utility::ToLower(nestedInstallerFile.PortableCommandAlias); - if (!alias.empty()) + if (!alias.empty() && !commandAliasSet.insert(alias).second) { - if (commandAliasSet.find(alias) == commandAliasSet.end()) - { - commandAliasSet.insert(alias); - } - else - { - resultErrors.emplace_back(ManifestError::DuplicatePortableCommandAlias, "PortableCommandAlias"); - } + resultErrors.emplace_back(ManifestError::DuplicatePortableCommandAlias, "PortableCommandAlias"); + break; } } } diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h b/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h index 13062d8a5c..52aa910680 100644 --- a/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h +++ b/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h @@ -24,6 +24,7 @@ namespace AppInstaller::Manifest WINGET_DEFINE_RESOURCE_STRINGID(ArpVersionValidationInternalError); WINGET_DEFINE_RESOURCE_STRINGID(BothAllowedAndExcludedMarketsDefined); WINGET_DEFINE_RESOURCE_STRINGID(DuplicatePortableCommandAlias); + WINGET_DEFINE_RESOURCE_STRINGID(DuplicateRelativeFilePath); WINGET_DEFINE_RESOURCE_STRINGID(DuplicateMultiFileManifestLocale); WINGET_DEFINE_RESOURCE_STRINGID(DuplicateMultiFileManifestType); WINGET_DEFINE_RESOURCE_STRINGID(DuplicateInstallerEntry);