diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b8aed9dfaa..d5a657f19b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -219,12 +219,6 @@ jobs: isPackaged: true filter: "TestCategory!=InProcess&TestCategory!=OutOfProcess" - - template: templates/e2e-test.template.yml - parameters: - title: "COM API E2E Tests (In-process)" - isPackaged: false - filter: "TestCategory=InProcess" - - task: PowerShell@2 displayName: 'Set program files directory' inputs: @@ -236,6 +230,20 @@ jobs: 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' + inputs: + SourceFolder: '$(buildOutDir)\AppInstallerCLI' + TargetFolder: '$(platformProgramFiles)\dotnet' + Contents: resources.pri + + - template: templates/e2e-test.template.yml + parameters: + title: "COM API E2E Tests (In-process)" + isPackaged: false + filter: "TestCategory=InProcess" + # Winmd accessed by test runner process (dotnet.exe) - task: CopyFiles@2 displayName: 'Copy winmd to dotnet directory' 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..5ddf0b00df 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -185,6 +185,9 @@ Workflows + + Header Files + @@ -337,6 +340,9 @@ Workflows + + Source Files + diff --git a/src/AppInstallerCLICore/ExecutionContextData.h b/src/AppInstallerCLICore/ExecutionContextData.h index becf945155..9ab78e1873 100644 --- a/src/AppInstallerCLICore/ExecutionContextData.h +++ b/src/AppInstallerCLICore/ExecutionContextData.h @@ -4,10 +4,9 @@ #include #include #include -#include -#include #include "CompletionData.h" #include "PackageCollection.h" +#include "PortableInstaller.h" #include "Workflows/WorkflowBase.h" #include @@ -54,8 +53,8 @@ namespace AppInstaller::CLI::Execution Dependencies, DependencySource, AllowedArchitectures, - PortableEntry, AllowUnknownScope, + PortableInstaller, Max }; @@ -220,15 +219,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; }; } } diff --git a/src/AppInstallerCLICore/PortableInstaller.cpp b/src/AppInstallerCLICore/PortableInstaller.cpp new file mode 100644 index 0000000000..4d50bf0045 --- /dev/null +++ b/src/AppInstallerCLICore/PortableInstaller.cpp @@ -0,0 +1,461 @@ +// 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" +#include "winget/Filesystem.h" +#include "winget/PathVariable.h" +#include "Microsoft/PortableIndex.h" +#include "Microsoft/Schema/IPortableIndex.h" +#include + +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::SQLite; +using namespace AppInstaller::Repository::Microsoft; +using namespace AppInstaller::Repository::Microsoft::Schema; + +namespace AppInstaller::CLI::Portable +{ + 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); + } + } + + 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); + } + } + + bool VerifyPortableFile(AppInstaller::Portable::PortableFileEntry& entry) + { + std::filesystem::path filePath = entry.GetFilePath(); + PortableFileType fileType = entry.FileType; + + if (fileType == PortableFileType::File) + { + if (std::filesystem::exists(filePath) && !SHA256::AreEqual(SHA256::ComputeHashFromFile(filePath), SHA256::ConvertToBytes(entry.SHA256))) + { + return false; + } + } + else if (fileType == PortableFileType::Symlink) + { + if (Filesystem::SymlinkExists(filePath) && !Filesystem::VerifySymlink(filePath, entry.SymlinkTarget)) + { + return false; + } + } + + return true; + } + + void PortableInstaller::InstallFile(AppInstaller::Portable::PortableFileEntry& entry) + { + 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 file at: " << filePath); + std::filesystem::remove(filePath); + } + + AICLI_LOG(Core, Info, << "Moving portable exe to: " << filePath); + + if (!RecordToIndex) + { + CommitToARPEntry(PortableValueName::PortableTargetFullPath, filePath); + CommitToARPEntry(PortableValueName::SHA256, entry.SHA256); + } + + Filesystem::RenameFile(entry.CurrentPath, filePath); + } + else if (fileType == PortableFileType::Directory) + { + AICLI_LOG(Core, Info, << "Moving directory to: " << filePath); + Filesystem::RenameFile(entry.CurrentPath, filePath); + } + else if (entry.FileType == PortableFileType::Symlink && !InstallDirectoryAddedToPath) + { + std::filesystem::file_status status = std::filesystem::status(filePath); + if (std::filesystem::is_directory(status)) + { + 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) + { + CommitToARPEntry(PortableValueName::PortableSymlinkFullPath, 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); + } + } + } + + void PortableInstaller::RemoveFile(AppInstaller::Portable::PortableFileEntry& entry) + { + const auto& filePath = entry.GetFilePath(); + PortableFileType fileType = entry.FileType; + + if (fileType == PortableFileType::File && std::filesystem::exists(filePath)) + { + 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)) + { + AICLI_LOG(CLI, Info, << "Removing directory at " << filePath); + std::filesystem::remove_all(filePath); + } + } + + // TODO: Optimize by applying the difference between expected and desired state. + void PortableInstaller::ApplyDesiredState() + { + std::filesystem::path existingIndexPath = InstallLocation / GetPortableIndexFileName(); + if (std::filesystem::exists(existingIndexPath)) + { + bool deleteIndex = false; + { + PortableIndex existingIndex = PortableIndex::Open(existingIndexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); + + for (auto expectedEntry : m_expectedEntries) + { + RemoveFile(expectedEntry); + existingIndex.RemovePortableFile(expectedEntry); + } + + deleteIndex = existingIndex.IsEmpty(); + } + + if (deleteIndex) + { + std::filesystem::remove(existingIndexPath); + AICLI_LOG(CLI, Info, << "Portable index deleted: " << existingIndexPath); + } + } + else + { + for (auto expectedEntry : m_expectedEntries) + { + RemoveFile(expectedEntry); + } + } + + // Check if existing install location differs from the target install location for proper cleanup. + if (!TargetInstallLocation.empty() && TargetInstallLocation != InstallLocation) + { + RemoveInstallDirectory(); + } + + if (RecordToIndex) + { + std::filesystem::path targetIndexPath = TargetInstallLocation / GetPortableIndexFileName(); + PortableIndex targetIndex = std::filesystem::exists(targetIndexPath) ? + PortableIndex::Open(targetIndexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite) : + PortableIndex::CreateNew(targetIndexPath.u8string()); + + for (auto desiredEntry : m_desiredEntries) + { + targetIndex.AddOrUpdatePortableFile(desiredEntry); + InstallFile(desiredEntry); + } + } + else + { + for (auto desiredEntry : m_desiredEntries) + { + InstallFile(desiredEntry); + } + } + } + + bool PortableInstaller::VerifyExpectedState() + { + for (auto entry : m_expectedEntries) + { + if (!VerifyPortableFile(entry)) + { + AICLI_LOG(CLI, Info, << "Portable file has been modified: " << entry.GetFilePath()); + return false; + } + } + + return true; + } + + void PortableInstaller::Install() + { + RegisterARPEntry(); + + CreateTargetInstallDirectory(); + + ApplyDesiredState(); + + AddToPathVariable(); + } + + void PortableInstaller::Uninstall() + { + ApplyDesiredState(); + + RemoveInstallDirectory(); + + RemoveFromPathVariable(); + + m_portableARPEntry.Delete(); + AICLI_LOG(CLI, Info, << "PortableARPEntry deleted."); + } + + void PortableInstaller::CreateTargetInstallDirectory() + { + if (std::filesystem::create_directories(TargetInstallLocation)) + { + AICLI_LOG(Core, Info, << "Created target install directory: " << TargetInstallLocation); + CommitToARPEntry(PortableValueName::InstallDirectoryCreated, true); + } + + CommitToARPEntry(PortableValueName::InstallLocation, TargetInstallLocation); + } + + void PortableInstaller::RemoveInstallDirectory() + { + if (std::filesystem::exists(InstallLocation) && InstallDirectoryCreated) + { + 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; + } + } + } + } + + void PortableInstaller::AddToPathVariable() + { + 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); + m_stream << Resource::String::ModifiedPathRequiresShellRestart << std::endl; + } + else + { + AICLI_LOG(CLI, Info, << "Target directory already exists in PATH registry: " << pathValue); + } + } + + void PortableInstaller::RemoveFromPathVariable() + { + 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); + } + else + { + if (PathVariable(GetScope()).Remove(pathValue)) + { + 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 PortableInstaller::SetAppsAndFeaturesMetadata(const Manifest::Manifest& manifest, const std::vector& entries) + { + AppInstaller::Manifest::AppsAndFeaturesEntry entry; + if (!entries.empty()) + { + entry = entries[0]; + } + + if (entry.DisplayName.empty()) + { + entry.DisplayName = manifest.CurrentLocalization.Get(); + } + if (entry.DisplayVersion.empty()) + { + entry.DisplayVersion = manifest.Version; + } + if (entry.Publisher.empty()) + { + 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(); + } + + void PortableInstaller::SetExpectedState() + { + const auto& indexPath = InstallLocation / GetPortableIndexFileName(); + + if (std::filesystem::exists(indexPath)) + { + PortableIndex portableIndex = PortableIndex::Open(indexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); + m_expectedEntries = portableIndex.GetAllPortableFiles(); + } + else + { + std::filesystem::path targetFullPath = PortableTargetFullPath; + std::filesystem::path symlinkFullPath = PortableSymlinkFullPath; + + if (!symlinkFullPath.empty()) + { + m_expectedEntries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkFullPath, targetFullPath))); + } + + if (!targetFullPath.empty()) + { + m_expectedEntries.emplace_back(std::move(PortableFileEntry::CreateFileEntry({}, targetFullPath, SHA256))); + } + } + } + + PortableInstaller::PortableInstaller(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode) : + m_portableARPEntry(PortableARPEntry(scope, arch, productCode)) + { + if (ARPEntryExists()) + { + 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); + InstallDirectoryAddedToPath = GetBoolValue(PortableValueName::InstallDirectoryAddedToPath); + InstallDirectoryCreated = GetBoolValue(PortableValueName::InstallDirectoryCreated); + } + + SetExpectedState(); + } + + void PortableInstaller::RegisterARPEntry() + { + 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::DisplayName, DisplayName); + CommitToARPEntry(PortableValueName::DisplayVersion, DisplayVersion); + CommitToARPEntry(PortableValueName::Publisher, Publisher); + CommitToARPEntry(PortableValueName::InstallDate, InstallDate); + CommitToARPEntry(PortableValueName::URLInfoAbout, URLInfoAbout); + CommitToARPEntry(PortableValueName::HelpLink, HelpLink); + } + + std::string PortableInstaller::GetStringValue(PortableValueName valueName) + { + if (m_portableARPEntry[valueName].has_value()) + { + return m_portableARPEntry[valueName]->GetValue(); + } + else + { + return {}; + } + } + + std::filesystem::path PortableInstaller::GetPathValue(PortableValueName valueName) + { + if (m_portableARPEntry[valueName].has_value()) + { + return m_portableARPEntry[valueName]->GetValue(); + } + { + return {}; + } + } + + bool PortableInstaller::GetBoolValue(PortableValueName valueName) + { + if (m_portableARPEntry[valueName].has_value()) + { + return m_portableARPEntry[valueName]->GetValue(); + } + else + { + return false; + } + } +} \ No newline at end of file diff --git a/src/AppInstallerCLICore/PortableInstaller.h b/src/AppInstallerCLICore/PortableInstaller.h new file mode 100644 index 0000000000..664720db38 --- /dev/null +++ b/src/AppInstallerCLICore/PortableInstaller.h @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. +// 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); + + // 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; + std::string InstallDate; + std::filesystem::path InstallLocation; + std::filesystem::path PortableSymlinkFullPath; + std::filesystem::path PortableTargetFullPath; + std::string Publisher; + std::string SHA256; + std::string URLInfoAbout; + std::string UninstallString; + std::string WinGetInstallerType; + std::string WinGetPackageIdentifier; + std::string WinGetSourceIdentifier; + bool InstallDirectoryCreated = false; + // If we fail to create a symlink, add install directory to PATH variable + bool InstallDirectoryAddedToPath = false; + + bool IsUpdate = false; + bool Purge = false; + bool RecordToIndex = false; + + // This is the incoming target install location determined from the context args. + std::filesystem::path TargetInstallLocation; + + PortableInstaller(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode); + + bool VerifyExpectedState(); + + void SetDesiredState(const std::vector& desiredEntries) + { + m_desiredEntries = desiredEntries; + }; + + void PrepareForCleanUp() + { + m_expectedEntries = m_desiredEntries; + m_desiredEntries = {}; + } + + void Install(); + + void Uninstall(); + + template + void CommitToARPEntry(PortableValueName valueName, T value) + { + m_portableARPEntry.SetValue(valueName, value); + } + + std::filesystem::path GetInstallDirectoryForPathVariable() + { + return InstallDirectoryAddedToPath ? InstallLocation : GetPortableLinksLocation(GetScope()); + } + + std::filesystem::path GetPortableIndexFileName() + { + return Utility::ConvertToUTF16(GetProductCode() + ".db"); + } + + Manifest::ScopeEnum GetScope() { return m_portableARPEntry.GetScope(); }; + + Utility::Architecture GetArch() { return m_portableARPEntry.GetArchitecture(); }; + + std::string GetProductCode() { return m_portableARPEntry.GetProductCode(); }; + + bool ARPEntryExists() { return m_portableARPEntry.Exists(); }; + + std::string GetOutputMessage() + { + return m_stream.str(); + } + + void SetAppsAndFeaturesMetadata( + const Manifest::Manifest& manifest, + const std::vector& entries); + + private: + PortableARPEntry m_portableARPEntry; + std::vector m_desiredEntries; + std::vector m_expectedEntries; + std::stringstream m_stream; + + std::string GetStringValue(PortableValueName valueName); + std::filesystem::path GetPathValue(PortableValueName valueName); + bool GetBoolValue(PortableValueName valueName); + + void SetExpectedState(); + void RegisterARPEntry(); + + void ApplyDesiredState(); + void InstallFile(AppInstaller::Portable::PortableFileEntry& desiredState); + void RemoveFile(AppInstaller::Portable::PortableFileEntry& desiredState); + + void CreateTargetInstallDirectory(); + void RemoveInstallDirectory(); + + void AddToPathVariable(); + void RemoveFromPathVariable(); + }; +} \ No newline at end of file diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index 5c8843712b..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); @@ -231,7 +233,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(PortableHashMismatchOverridden); WINGET_DEFINE_RESOURCE_STRINGID(PortableHashMismatchOverrideRequired); WINGET_DEFINE_RESOURCE_STRINGID(PortableInstallFailed); - WINGET_DEFINE_RESOURCE_STRINGID(PortableInstallFromArchiveNotSupported); + WINGET_DEFINE_RESOURCE_STRINGID(PortablePackageAlreadyExists); WINGET_DEFINE_RESOURCE_STRINGID(PortableRegistryCollisionOverridden); WINGET_DEFINE_RESOURCE_STRINGID(PositionArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(PreserveArgumentDescription); @@ -363,7 +365,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 3f11a0430d..6b54656e5b 100644 --- a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp @@ -4,27 +4,35 @@ #include "ArchiveFlow.h" #include "winget/Archive.h" #include "winget/Filesystem.h" +#include "PortableFlow.h" 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(); - const auto& installerParentPath = installerPath.parent_path(); + std::filesystem::path destinationFolder = installerPath.parent_path() / s_Extracted; + std::filesystem::create_directory(destinationFolder); - // 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); + context.Reporter.Info() << Resource::String::ExtractingArchive << std::endl; + HRESULT result = AppInstaller::Archive::TryExtractArchive(installerPath, destinationFolder); - if (SUCCEEDED(hr)) + if (SUCCEEDED(result)) { AICLI_LOG(CLI, Info, << "Successfully extracted archive"); + context.Reporter.Info() << Resource::String::ExtractArchiveSucceeded << std::endl; } else { - AICLI_LOG(CLI, Info, << "Failed to extract archive with code " << 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); } @@ -40,29 +48,33 @@ 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; + std::filesystem::path targetInstallerPath = context.Get().parent_path() / s_Extracted; - if (Filesystem::PathEscapesBaseDirectory(nestedInstallerPath, installerParentPath)) + for (const auto& nestedInstallerFile : installer.NestedInstallerFiles) { - 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 - { - AICLI_LOG(CLI, Info, << "Setting installerPath to: " << nestedInstallerPath); - context.Add(nestedInstallerPath); + const std::filesystem::path& nestedInstallerPath = targetInstallerPath / ConvertToUTF16(nestedInstallerFile.RelativeFilePath); + + 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; + 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 (!IsPortableType(installer.NestedInstallerType)) + { + // Update the installerPath to the extracted non-portable installer. + AICLI_LOG(CLI, Info, << "Setting installerPath to: " << nestedInstallerPath); + targetInstallerPath = nestedInstallerPath; + } } + + context.Add(targetInstallerPath); } void EnsureValidNestedInstallerMetadataForArchiveInstall(Execution::Context& context) diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp index 0422eb5e6c..632a3f2d43 100644 --- a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -336,6 +336,8 @@ namespace AppInstaller::CLI::Workflow void PortableInstall(Execution::Context& context) { context << + InitializePortableInstaller << + VerifyPackageAndSourceMatch << PortableInstallImpl << ReportInstallerResult("Portable"sv, APPINSTALLER_CLI_ERROR_PORTABLE_INSTALL_FAILED, true); } @@ -474,7 +476,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 85ae72b71a..21640d8042 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -2,21 +2,18 @@ // Licensed under the MIT License. #include "pch.h" #include "PortableFlow.h" +#include "PortableInstaller.h" #include "WorkflowBase.h" #include "winget/Filesystem.h" -#include "winget/PortableARPEntry.h" -#include "winget/PortableEntry.h" -#include "winget/PathVariable.h" -#include "AppInstallerStrings.h" +#include "winget/PortableFileEntry.h" +#include 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 std::filesystem; +using namespace AppInstaller::Utility; +using namespace AppInstaller::CLI::Portable; +using namespace AppInstaller::Portable; +using namespace AppInstaller::Repository::Microsoft; namespace AppInstaller::CLI::Workflow { @@ -24,14 +21,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; @@ -49,484 +38,295 @@ namespace AppInstaller::CLI::Workflow return MakeSuitablePathPart(packageId + "_" + source); } - 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); - } - } - - 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); - } - } - - std::filesystem::path GetPortableTargetDirectory(Execution::Context& context) + void EnsureValidArgsForPortableInstall(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; + std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); - if (!locationArg.empty()) + try { - targetInstallDirectory = std::filesystem::path{ ConvertToUTF16(locationArg) }; + if (MakeSuitablePathPart(renameArg) != renameArg) + { + context.Reporter.Error() << Resource::String::ReservedFilenameError << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS); + } } - else + catch (...) { - const std::string& productCode = GetPortableProductCode(context); - targetInstallDirectory = GetPortableInstallRoot(scope, arch); - targetInstallDirectory /= ConvertToUTF16(productCode); + context.Reporter.Error() << Resource::String::ReservedFilenameError << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS); } - - return targetInstallDirectory; } - std::filesystem::path GetPortableTargetFullPath(Execution::Context& context) + void EnsureVolumeSupportsReparsePoints(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); + Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); + const std::filesystem::path& symlinkDirectory = GetPortableLinksLocation(scope); + + if (!AppInstaller::Filesystem::SupportsReparsePoints(symlinkDirectory)) + { + context.Reporter.Error() << Resource::String::ReparsePointsNotSupportedError << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_REPARSE_POINT_NOT_SUPPORTED); } - else + } + + void EnsureRunningAsAdminForMachineScopeInstall(Execution::Context& context) + { + // Admin is required for machine scope install or else creating a symlink in the %PROGRAMFILES% link location will fail. + Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); + if (scope == Manifest::ScopeEnum::Machine) { - fileName = installerPath.filename(); + context << Workflow::EnsureRunningAsAdmin; } - - 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); + void VerifyPackageAndSourceMatch(Execution::Context& context) + { + const std::string& packageIdentifier = context.Get().Id; - std::filesystem::path commandAlias; - if (!renameArg.empty()) - { - commandAlias = ConvertToUTF16(renameArg); - } - else + std::string sourceIdentifier; + if (context.Contains(Execution::Data::PackageVersion)) + { + sourceIdentifier = context.Get()->GetSource().GetIdentifier(); + } + else + { + sourceIdentifier = s_DefaultSource; + } + + PortableInstaller& portableInstaller = context.Get(); + if (portableInstaller.ARPEntryExists()) + { + if (packageIdentifier != portableInstaller.WinGetPackageIdentifier || sourceIdentifier != portableInstaller.WinGetSourceIdentifier) { - if (!commands.empty()) + // TODO: Replace HashOverride with --Force when argument behavior gets updated. + if (!context.Args.Contains(Execution::Args::Type::HashOverride)) { - commandAlias = ConvertToUTF16(commands[0]); + 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 { - commandAlias = installerPath.filename(); + AICLI_LOG(CLI, Info, << "Overriding registry match check..."); + context.Reporter.Warn() << Resource::String::PortableRegistryCollisionOverridden << std::endl; } - } - - AppendExeExtension(commandAlias); - return GetPortableLinksLocation(scope) / commandAlias; + } } - Manifest::AppsAndFeaturesEntry GetAppsAndFeaturesEntryForPortableInstall( - const std::vector& appsAndFeaturesEntries, - const AppInstaller::Manifest::Manifest& manifest) + portableInstaller.WinGetPackageIdentifier = packageIdentifier; + portableInstaller.WinGetSourceIdentifier = sourceIdentifier; + } + + void InitializePortableInstaller(Execution::Context& context) + { + Manifest::ScopeEnum scope = Manifest::ScopeEnum::Unknown; + bool isUpdate = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate); + if (isUpdate) { - 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()) + IPackageVersion::Metadata installationMetadata = context.Get()->GetMetadata(); + auto installerScopeItr = installationMetadata.find(Repository::PackageVersionMetadata::InstalledScope); + if (installerScopeItr != installationMetadata.end()) { - appsAndFeaturesEntry.Publisher = manifest.CurrentLocalization.Get(); + scope = Manifest::ConvertToScopeEnum(installerScopeItr->second); } - - return appsAndFeaturesEntry; + } + else + { + scope = Manifest::ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); } - void InitializePortableARPEntry(Execution::Context& context) - { - const std::string& packageIdentifier = context.Get().Id; + Utility::Architecture arch = context.Get()->Arch; + const std::string& productCode = GetPortableProductCode(context); - 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; - } - } - } + PortableInstaller portableInstaller = PortableInstaller(scope, arch, productCode); + portableInstaller.IsUpdate = isUpdate; + + // Set target install directory + std::string_view locationArg = context.Args.GetArg(Execution::Args::Type::InstallLocation); + std::filesystem::path targetInstallDirectory; - 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)); + if (!locationArg.empty()) + { + targetInstallDirectory = std::filesystem::path{ ConvertToUTF16(locationArg) }; } - - 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); + else + { + targetInstallDirectory = GetPortableInstallRoot(scope, arch); + targetInstallDirectory /= ConvertToUTF16(productCode); } - void RemovePortableExe(Execution::Context& context) - { - Portable::PortableEntry& portableEntry = context.Get(); - const auto& targetPath = portableEntry.PortableTargetFullPath; + portableInstaller.TargetInstallLocation = targetInstallDirectory; + portableInstaller.SetAppsAndFeaturesMetadata(context.Get(), context.Get()->AppsAndFeaturesEntries); + context.Add(std::move(portableInstaller)); + } - 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::vector GetDesiredStateForPortableInstall(Execution::Context& context) + { + std::filesystem::path& installerPath = context.Get(); + PortableInstaller& portableInstaller = context.Get(); + std::vector entries; - std::filesystem::remove(targetPath); - AICLI_LOG(CLI, Info, << "Successfully deleted portable exe:" << targetPath); - } - else + 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. + if (std::filesystem::is_directory(installerPath)) + { + for (const auto& entry : std::filesystem::directory_iterator(installerPath)) { - 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)) + 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)) { - if (isCreated) - { - std::filesystem::remove(installDirectory); - AICLI_LOG(CLI, Info, << "Install directory deleted: " << installDirectory); - } + entries.emplace_back(std::move(PortableFileEntry::CreateDirectoryEntry(entryPath, targetPath))); } else { - context.Reporter.Warn() << Resource::String::FilesRemainInInstallDirectory << installDirectory << std::endl; - } - } - else - { - AICLI_LOG(CLI, Info, << "Install directory does not exist: " << installDirectory); - } - } - - 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; + entries.emplace_back(std::move(PortableFileEntry::CreateFileEntry(entryPath, targetPath, {}))); + } } - 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)) + if (entries.size() > 1) { - 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); + portableInstaller.RecordToIndex = true; } - if (std::filesystem::remove(symlinkFullPath)) - { - AICLI_LOG(CLI, Info, << "Removed existing file at " << symlinkFullPath); - context.Reporter.Warn() << Resource::String::OverwritingExistingFileAtMessage << ' ' << symlinkFullPath.u8string() << std::endl; - } + const std::vector& nestedInstallerFiles = context.Get()->NestedInstallerFiles; - portableEntry.CreatePortableSymlink(); - } - - void AddToPathVariable(Execution::Context& context) - { - Portable::PortableEntry& portableEntry = context.Get(); - const std::filesystem::path& pathValue = portableEntry.GetPathValue(); - if (portableEntry.AddToPathVariable()) + for (const auto& nestedInstallerFile : nestedInstallerFiles) { - AICLI_LOG(CLI, Info, << "Appended target directory to PATH registry: " << pathValue); - context.Reporter.Warn() << Resource::String::ModifiedPathRequiresShellRestart << std::endl; + const std::filesystem::path& targetPath = targetInstallDirectory / ConvertToUTF16(nestedInstallerFile.RelativeFilePath); + + std::filesystem::path commandAlias; + if (nestedInstallerFile.PortableCommandAlias.empty()) + { + commandAlias = targetPath.filename(); + } + else + { + commandAlias = ConvertToUTF16(nestedInstallerFile.PortableCommandAlias); + } + + Filesystem::AppendExtension(commandAlias, ".exe"); + entries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkDirectory / commandAlias, targetPath))); } - else - { - AICLI_LOG(CLI, Info, << "Target directory already exists in PATH registry: " << pathValue); - } - } - - void RemoveFromPathVariable(Execution::Context& context) - { - Portable::PortableEntry& portableEntry = context.Get(); - const std::filesystem::path& pathValue = portableEntry.GetPathValue(); - if (portableEntry.RemoveFromPathVariable()) + } + 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; + + if (!renameArg.empty()) { - AICLI_LOG(CLI, Info, << "Removed target directory from PATH registry: " << pathValue); + fileName = commandAlias = ConvertToUTF16(renameArg); } 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)) + if (!commands.empty()) { - AICLI_LOG(CLI, Info, << "Portable symlink not found; Unable to delete portable symlink: " << symlinkPath); + commandAlias = ConvertToUTF16(commands[0]); } + else + { + commandAlias = installerPath.filename(); + } + + fileName = installerPath.filename(); } - 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 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); + AppInstaller::Filesystem::AppendExtension(fileName, ".exe"); + AppInstaller::Filesystem::AppendExtension(commandAlias, ".exe"); - 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>()); + const std::filesystem::path& targetFullPath = targetInstallDirectory / fileName; + entries.emplace_back(std::move(PortableFileEntry::CreateFileEntry(installerPath, targetFullPath, {}))); + entries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkDirectory / commandAlias, targetFullPath))); } - void EnsureValidArgsForPortableInstall(Execution::Context& context) - { - std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); - - try - { - if (MakeSuitablePathPart(renameArg) != renameArg) - { - context.Reporter.Error() << Resource::String::ReservedFilenameError << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS); - } - } - catch (...) - { - context.Reporter.Error() << Resource::String::ReservedFilenameError << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS); - } - } - - void EnsureVolumeSupportsReparsePoints(Execution::Context& context) - { - Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); - const std::filesystem::path& symlinkDirectory = GetPortableLinksLocation(scope); - - if (!AppInstaller::Filesystem::SupportsReparsePoints(symlinkDirectory)) - { - context.Reporter.Error() << Resource::String::ReparsePointsNotSupportedError << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_REPARSE_POINT_NOT_SUPPORTED); - } - } - - void EnsureRunningAsAdminForMachineScopeInstall(Execution::Context& context) - { - // Admin is required for machine scope install or else creating a symlink in the %PROGRAMFILES% link location will fail. - Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); - if (scope == Manifest::ScopeEnum::Machine) - { - context << Workflow::EnsureRunningAsAdmin; - } - } + return entries; } - + void PortableInstallImpl(Execution::Context& context) { - Manifest::ScopeEnum scope = Manifest::ScopeEnum::Unknown; - bool isUpdate = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate); - if (isUpdate) - { - IPackageVersion::Metadata installationMetadata = context.Get()->GetMetadata(); - auto installerScopeItr = installationMetadata.find(Repository::PackageVersionMetadata::InstalledScope); - if (installerScopeItr != installationMetadata.end()) - { - scope = Manifest::ConvertToScopeEnum(installerScopeItr->second); - } - } - else - { - scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); - } - - PortableARPEntry uninstallEntry = PortableARPEntry( - scope, - context.Get()->Arch, - GetPortableProductCode(context)); - - PortableEntry portableEntry = PortableEntry(uninstallEntry); - context.Add(std::move(portableEntry)); + PortableInstaller& portableInstaller = context.Get(); try { context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl; - context << - InitializePortableARPEntry << - MovePortableExe << - CreatePortableSymlink << - AddToPathVariable << - CommitPortableMetadataToRegistry; + std::vector desiredState = GetDesiredStateForPortableInstall(context); + + portableInstaller.SetDesiredState(desiredState); + + if (!portableInstaller.VerifyExpectedState()) + { + 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); + } + } - context.Add(context.GetTerminationHR()); + portableInstaller.Install(); + context.Add(ERROR_SUCCESS); + context.Reporter.Warn() << portableInstaller.GetOutputMessage(); } catch (...) { 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(); - - if (installReturnCode != 0 && installReturnCode != APPINSTALLER_CLI_ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS && !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; + if (!portableInstaller.IsUpdate) + { + context.Reporter.Warn() << Resource::String::PortableInstallFailed << std::endl; + portableInstaller.PrepareForCleanUp(); +; portableInstaller.Uninstall(); + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED); + } } } void PortableUninstallImpl(Execution::Context& context) { + PortableInstaller& portableInstaller = context.Get(); + try { - context.Reporter.Info() << Resource::String::UninstallFlowStartingPackageUninstall << std::endl; + context.Reporter.Info() << Resource::String::UninstallFlowStartingPackageUninstall << std::endl; + + 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); + } + } - context << - RemovePortableExe << - RemoveInstallDirectory << - RemovePortableSymlink << - RemoveFromPathVariable << - RemovePortableARPEntry; + portableInstaller.Purge = context.Args.Contains(Execution::Args::Type::Purge) || + (!portableInstaller.IsUpdate && Settings::User().Get() && !context.Args.Contains(Execution::Args::Type::Preserve)); - context.Add(context.GetTerminationHR()); + portableInstaller.Uninstall(); + context.Add(ERROR_SUCCESS); + context.Reporter.Warn() << portableInstaller.GetOutputMessage(); } catch (...) { context.Add(Workflow::HandleException(context, std::current_exception())); } - - // Reset termination to allow for ReportUninstallResult to process return code. - context.ResetTermination(); } void EnsureSupportForPortableInstall(Execution::Context& context) @@ -555,16 +355,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 1c2b9d8105..4b1c1032ca 100644 --- a/src/AppInstallerCLICore/Workflows/PortableFlow.h +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.h @@ -17,9 +17,27 @@ 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 EnsureNonPortableTypeForArchiveInstall(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); } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp b/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp index 26aa633ed5..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,15 +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]; - Registry::Portable::PortableARPEntry uninstallEntry = Registry::Portable::PortableARPEntry( - ConvertToScopeEnum(installedScope), - Utility::ConvertToArchitectureEnum(installedArch), + + PortableInstaller portableInstaller = PortableInstaller( + Manifest::ConvertToScopeEnum(installedScope), + Utility::ConvertToArchitectureEnum(installedArch), productCodes[0]); - Portable::PortableEntry portableEntry = Portable::PortableEntry(uninstallEntry); - - context.Add(portableEntry); + context.Add(std::move(portableInstaller)); break; } default: diff --git a/src/AppInstallerCLIE2ETests/InstallCommand.cs b/src/AppInstallerCLIE2ETests/InstallCommand.cs index 503276b9eb..56655dfdba 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}" @@ -174,7 +174,7 @@ 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"; @@ -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); @@ -246,26 +246,23 @@ public void InstallPortableToExistingDirectory() [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); 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); 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] @@ -277,7 +274,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,10 +285,9 @@ 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}")); TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true); } @@ -307,7 +303,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 +321,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 +329,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 +338,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/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/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..61a15dacbb 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,30 +97,53 @@ 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 File.Delete(symlinkPath); FileSystemInfo modifiedSymlinkInfo = File.CreateSymbolicLink(symlinkPath, "fakeTargetExe"); + 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(modifiedSymlinkInfo.Exists, "Modified symlink should still exist"); - // Remove modified symlink as to not interfere with other tests - bool modifiedSymlinkExists = modifiedSymlinkInfo.Exists; - modifiedSymlinkInfo.Delete(); + // Try again with --force + var result2 = TestCommon.RunAICLICommand("uninstall", $"{packageId} --force"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result2.ExitCode); + 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); + } + + [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"; + 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")); - 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/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 18c07fd368..ff41cfe009 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -1344,11 +1344,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"} @@ -1372,15 +1372,9 @@ 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 - - Installing a portable package from an archive is not yet supported - Failed to extract the contents of the archive @@ -1435,4 +1429,13 @@ 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 + + + Successfully extracted archive + + + Extracting archive... + \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index 50ee1604ff..fbf1d266d4 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -213,7 +213,7 @@ - + @@ -298,7 +298,7 @@ true - + true @@ -439,6 +439,12 @@ true + + true + + + true + true @@ -634,7 +640,7 @@ true - + true diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters index 64d36f600b..ece41c6e45 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -227,7 +227,7 @@ Source Files\Common - + Source Files\Common @@ -318,6 +318,12 @@ TestData + + TestData + + + TestData + TestData @@ -537,7 +543,7 @@ TestData - + TestData @@ -588,7 +594,7 @@ TestData - + TestData diff --git a/src/AppInstallerCLITests/Archive.cpp b/src/AppInstallerCLITests/Archive.cpp index 304f27965b..7354c3ed9f 100644 --- a/src/AppInstallerCLITests/Archive.cpp +++ b/src/AppInstallerCLITests/Archive.cpp @@ -19,6 +19,7 @@ TEST_CASE("Extract_ZipArchive", "[archive]") HRESULT hr = TryExtractArchive(testZipPath, tempDirectoryPath); + std::filesystem::path expectedPath = tempDirectoryPath / "test.txt"; REQUIRE(SUCCEEDED(hr)); - REQUIRE(std::filesystem::exists(tempDirectoryPath / "test.txt")); + REQUIRE(std::filesystem::exists(expectedPath)); } \ No newline at end of file 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/PortableEntry.cpp b/src/AppInstallerCLITests/PortableEntry.cpp deleted file mode 100644 index df18a69bab..0000000000 --- a/src/AppInstallerCLITests/PortableEntry.cpp +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#include "pch.h" -#include "TestCommon.h" -#include -#include -#include -#include - -using namespace AppInstaller::Portable; -using namespace AppInstaller::Utility; -using namespace TestCommon; - -TEST_CASE("VerifyPortableMove", "[PortableEntry]") -{ - PortableARPEntry testARPEntry = PortableARPEntry( - 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"; - - 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)); - REQUIRE(testEntry.InstallDirectoryCreated); - - // Create a second PortableEntry instance to emulate installing for a second time. (ARP entry should already exist) - PortableARPEntry testARPEntry2 = PortableARPEntry( - 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(); - 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; - REQUIRE(testEntry2.InstallDirectoryCreated); - testEntry2.RemoveARPEntry(); -} - -TEST_CASE("VerifySymlinkCheck", "[PortableEntry]") -{ - PortableARPEntry testARPEntry = PortableARPEntry( - 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(); - - TestCommon::TempDirectory tempDirectory("TempDirectory", true); - testEntry.PortableTargetFullPath = testFile.GetPath(); - testEntry.PortableSymlinkFullPath = tempDirectory.GetPath() / "symlink.exe"; - - testEntry.CreatePortableSymlink(); - - 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]") -{ - 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 diff --git a/src/AppInstallerCLITests/PortableIndex.cpp b/src/AppInstallerCLITests/PortableIndex.cpp index aadc1bf5b9..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"; @@ -114,8 +116,8 @@ 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->FileType == IPortableIndex::PortableFileType::Symlink); + REQUIRE(fileFromIndex->GetFilePath() == portableFile.GetFilePath()); + 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"; @@ -157,8 +159,8 @@ 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->FileType == IPortableIndex::PortableFileType::Symlink); + REQUIRE(fileFromIndex->GetFilePath() == portableFile.GetFilePath()); + 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 new file mode 100644 index 0000000000..a9953daae9 --- /dev/null +++ b/src/AppInstallerCLITests/PortableInstaller.cpp @@ -0,0 +1,174 @@ +// 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 std::string_literals; +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; + +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.TargetInstallLocation = tempDirectory.GetPath(); + portableInstaller.SetDesiredState(desiredTestState); + REQUIRE(portableInstaller.VerifyExpectedState()); + + 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_CreateInstallRoot", "[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.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)); + 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 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)); + REQUIRE_FALSE(AppInstaller::Filesystem::SymlinkExists(symlinkPath2)); + REQUIRE_FALSE(std::filesystem::exists(directoryPath)); +} \ 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/Manifest-Bad-InstallerTypeZip-DuplicateCommandAlias.yaml b/src/AppInstallerCLITests/TestData/Manifest-Bad-InstallerTypeZip-DuplicateCommandAlias.yaml new file mode 100644 index 0000000000..71f230b868 --- /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/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/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..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_) \ @@ -225,8 +227,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 +1042,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 +1054,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); @@ -1079,7 +1081,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 +1165,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 +1718,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 +1932,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"); diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp index f80e60fd2e..d4edac4537 100644 --- a/src/AppInstallerCLITests/YamlManifest.cpp +++ b/src/AppInstallerCLITests/YamlManifest.cpp @@ -275,6 +275,8 @@ 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-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/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj index 4b95471821..953e49792f 100644 --- a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj +++ b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -330,7 +330,7 @@ - + @@ -407,7 +407,6 @@ - diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters index e8a005b75b..574833856a 100644 --- a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters +++ b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -192,7 +192,7 @@ Public\winget - + Public\winget @@ -365,9 +365,6 @@ Source Files - - Source Files - Source Files diff --git a/src/AppInstallerCommonCore/Archive.cpp b/src/AppInstallerCommonCore/Archive.cpp index 41cbde0a12..a699e12fea 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)); 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 652402aa5a..25d9bc2629 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) @@ -168,6 +168,25 @@ 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; + } + } + + bool SymlinkExists(const std::filesystem::path& symlinkPath) + { + return std::filesystem::is_symlink(std::filesystem::symlink_status(symlinkPath)); + } + std::filesystem::path GetExpandedPath(const std::string& path) { std::string trimPath = path; diff --git a/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp index a1e3f3943c..d761c3d7fc 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp @@ -497,6 +497,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/Manifest/ManifestValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp index c8b6b1c8aa..f27088b566 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp @@ -32,6 +32,8 @@ 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::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 }, @@ -260,20 +262,37 @@ namespace AppInstaller::Manifest resultErrors.emplace_back(ManifestError::ExceededNestedInstallerFilesLimit, "NestedInstallerFiles"); } + 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() && !commandAliasSet.insert(alias).second) + { + resultErrors.emplace_back(ManifestError::DuplicatePortableCommandAlias, "PortableCommandAlias"); + break; } } } diff --git a/src/AppInstallerCommonCore/PortableARPEntry.cpp b/src/AppInstallerCommonCore/PortableARPEntry.cpp index 73a39f214c..084341067a 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) { @@ -96,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/PortableEntry.cpp b/src/AppInstallerCommonCore/PortableEntry.cpp deleted file mode 100644 index 32d18d42aa..0000000000 --- a/src/AppInstallerCommonCore/PortableEntry.cpp +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#include "pch.h" -#include "winget/PortableEntry.h" -#include "winget/PortableARPEntry.h" -#include "winget/Manifest.h" -#include "winget/Filesystem.h" -#include "winget/PathVariable.h" -#include "Public/AppInstallerLogging.h" - -using namespace AppInstaller::Registry; -using namespace AppInstaller::Registry::Portable; -using namespace AppInstaller::Registry::Environment; - -namespace AppInstaller::Portable -{ - PortableEntry::PortableEntry(PortableARPEntry& portableARPEntry) : - m_portableARPEntry(portableARPEntry) - { - // 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); - } - } - - void PortableEntry::MovePortableExe(const std::filesystem::path& installerPath) - { - bool isDirectoryCreated = false; - if (std::filesystem::create_directories(InstallLocation)) - { - AICLI_LOG(Core, Info, << "Created target install directory: " << InstallLocation); - isDirectoryCreated = true; - } - - if (std::filesystem::exists(PortableTargetFullPath)) - { - std::filesystem::remove(PortableTargetFullPath); - AICLI_LOG(Core, Info, << "Removing existing portable exe at: " << 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 PortableEntry::VerifyPortableExeHash() - { - std::ifstream inStream{ PortableTargetFullPath, std::ifstream::binary }; - const Utility::SHA256::HashBuffer& targetFileHash = Utility::SHA256::ComputeHash(inStream); - inStream.close(); - - return Utility::SHA256::AreEqual(Utility::SHA256::ConvertToBytes(SHA256), targetFileHash); - } - - void PortableEntry::CreatePortableSymlink() - { - if (Filesystem::CreateSymlink(PortableTargetFullPath, PortableSymlinkFullPath)) - { - AICLI_LOG(Core, Info, << "Symlink created at: " << PortableSymlinkFullPath); - } - else - { - // Symlink creation should only fail if the user executes in user mode and non-admin. - // 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); - } - } - - bool PortableEntry::VerifySymlinkTarget() - { - AICLI_LOG(Core, Info, << "Expected portable target path: " << PortableTargetFullPath); - const std::filesystem::path& symlinkTargetPath = std::filesystem::read_symlink(PortableSymlinkFullPath); - - if (symlinkTargetPath == PortableTargetFullPath) - { - AICLI_LOG(Core, Info, << "Portable symlink target matches portable target path: " << symlinkTargetPath); - return true; - } - else - { - AICLI_LOG(Core, Info, << "Portable symlink does not match portable target path: " << symlinkTargetPath); - return false; - } - } - - bool PortableEntry::AddToPathVariable() - { - return PathVariable(GetScope()).Append(GetPathValue()); - } - - bool PortableEntry::RemoveFromPathVariable() - { - bool removeFromPath = true; - std::filesystem::path pathValue = GetPathValue(); - if (!InstallDirectoryAddedToPath) - { - // 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; - } - } - - if (removeFromPath) - { - return PathVariable(GetScope()).Remove(pathValue); - } - else - { - return false; - } - } - - void PortableEntry::RemoveARPEntry() - { - m_portableARPEntry.Delete(); - } - - std::string PortableEntry::GetStringValue(PortableValueName valueName) - { - if (m_portableARPEntry[valueName].has_value()) - { - return m_portableARPEntry[valueName]->GetValue(); - } - else - { - return {}; - } - } - - std::filesystem::path PortableEntry::GetPathValue(PortableValueName valueName) - { - if (m_portableARPEntry[valueName].has_value()) - { - return m_portableARPEntry[valueName]->GetValue(); - } - { - return {}; - } - } - - bool PortableEntry::GetBoolValue(PortableValueName valueName) - { - if (m_portableARPEntry[valueName].has_value()) - { - return m_portableARPEntry[valueName]->GetValue(); - } - else - { - return false; - } - } -} \ No newline at end of file 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/Public/winget/Filesystem.h b/src/AppInstallerCommonCore/Public/winget/Filesystem.h index dbde4c31db..e4e859891b 100644 --- a/src/AppInstallerCommonCore/Public/winget/Filesystem.h +++ b/src/AppInstallerCommonCore/Public/winget/Filesystem.h @@ -21,6 +21,16 @@ 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& 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); + + // 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. diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h index addb1d457b..80401672b5 100644 --- a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h +++ b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h @@ -325,6 +325,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/AppInstallerCommonCore/Public/winget/ManifestValidation.h b/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h index ddc01d5425..52aa910680 100644 --- a/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h +++ b/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h @@ -23,6 +23,8 @@ 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(DuplicateRelativeFilePath); WINGET_DEFINE_RESOURCE_STRINGID(DuplicateMultiFileManifestLocale); WINGET_DEFINE_RESOURCE_STRINGID(DuplicateMultiFileManifestType); WINGET_DEFINE_RESOURCE_STRINGID(DuplicateInstallerEntry); diff --git a/src/AppInstallerCommonCore/Public/winget/PortableARPEntry.h b/src/AppInstallerCommonCore/Public/winget/PortableARPEntry.h index caf9bce8ff..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); @@ -47,9 +45,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/AppInstallerCommonCore/Public/winget/PortableEntry.h b/src/AppInstallerCommonCore/Public/winget/PortableEntry.h deleted file mode 100644 index 67fc4c1b33..0000000000 --- a/src/AppInstallerCommonCore/Public/winget/PortableEntry.h +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#pragma once -#include -#include -#include "winget/PortableARPEntry.h" -#include -#include - -using namespace AppInstaller::Registry; -using namespace AppInstaller::Registry::Portable; - -namespace AppInstaller::Portable -{ - struct PortableEntry - { - 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; - std::string Publisher; - std::string SHA256; - std::string URLInfoAbout; - std::string UninstallString; - std::string WinGetInstallerType; - std::string WinGetPackageIdentifier; - std::string WinGetSourceIdentifier; - bool InstallDirectoryAddedToPath = false; - - template - void Commit(PortableValueName valueName, T value) - { - m_portableARPEntry.SetValue(valueName, value); - } - - Manifest::ScopeEnum GetScope() { return m_portableARPEntry.GetScope(); }; - - bool Exists() { return m_portableARPEntry.Exists(); }; - - PortableEntry(PortableARPEntry& portableARPEntry); - - std::filesystem::path GetPathValue() const - { - return InstallDirectoryAddedToPath ? InstallLocation : PortableSymlinkFullPath.parent_path(); - } - - bool VerifyPortableExeHash(); - - bool VerifySymlinkTarget(); - - void MovePortableExe(const std::filesystem::path& installerPath); - - void CreatePortableSymlink(); - - bool AddToPathVariable(); - - bool RemoveFromPathVariable(); - - void RemoveARPEntry(); - - private: - PortableARPEntry m_portableARPEntry; - 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/AppInstallerCommonCore/Public/winget/PortableFileEntry.h b/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h new file mode 100644 index 0000000000..6c7976d2db --- /dev/null +++ b/src/AppInstallerCommonCore/Public/winget/PortableFileEntry.h @@ -0,0 +1,81 @@ +// 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) + { + if (FileType != PortableFileType::Symlink) + { + m_filePath = std::filesystem::weakly_canonical(path); + } + else + { + m_filePath = path; + } + }; + + std::filesystem::path GetFilePath() const { return m_filePath; }; + + 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); + + 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(); + return symlinkEntry; + } + + static PortableFileEntry CreateDirectoryEntry(const std::filesystem::path& currentPath, const std::filesystem::path& directoryPath) + { + PortableFileEntry directoryEntry; + directoryEntry.FileType = PortableFileType::Directory; + directoryEntry.CurrentPath = currentPath; + directoryEntry.SetFilePath(directoryPath); + return directoryEntry; + } + + private: + std::filesystem::path m_filePath; + }; +} \ No newline at end of file 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 3b09ee00a0..f3bb074715 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 { @@ -19,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(); @@ -26,7 +30,7 @@ namespace AppInstaller::Repository::Microsoft return result; } - 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() << "]"); @@ -42,7 +46,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 }; @@ -56,7 +60,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 }; @@ -74,6 +78,34 @@ namespace AppInstaller::Repository::Microsoft return result; } + 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); + } + + bool PortableIndex::IsEmpty() + { + return m_interface->IsEmpty(m_dbconn); + } + + void PortableIndex::AddOrUpdatePortableFile(const Portable::PortableFileEntry& 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 2becefd82c..8319451d81 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,11 +32,19 @@ namespace AppInstaller::Repository::Microsoft return { filePath, disposition, std::move(indexFile) }; } - IdType AddPortableFile(const Schema::IPortableIndex::PortableFile& file); + IdType AddPortableFile(const Portable::PortableFileEntry& file); + + void RemovePortableFile(const Portable::PortableFileEntry& file); + + bool UpdatePortableFile(const Portable::PortableFileEntry& file); + + void AddOrUpdatePortableFile(const Portable::PortableFileEntry& file); + + std::vector GetAllPortableFiles(); - void RemovePortableFile(const Schema::IPortableIndex::PortableFile& file); + bool Exists(const Portable::PortableFileEntry& file); - bool UpdatePortableFile(const Schema::IPortableIndex::PortableFile& file); + bool IsEmpty(); private: // Constructor used to open an existing index. diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/IPortableIndex.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/IPortableIndex.h index 2ac19f9543..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::weakly_canonical(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,13 +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 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; }; } \ 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..ce97b2538f 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface.h @@ -13,8 +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; + 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; }; } \ 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..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); @@ -73,4 +73,19 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 savepoint.Commit(); return { status, portableEntryResult.value() }; } + + bool PortableIndexInterface::Exists(SQLite::Connection& connection, const Portable::PortableFileEntry& file) + { + return GetExistingPortableFileId(connection, file).has_value(); + } + + bool PortableIndexInterface::IsEmpty(SQLite::Connection& connection) + { + 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..2efca323fd 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,12 +103,12 @@ 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(); - 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); return portableFile; @@ -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()) + { + Portable::PortableFileEntry portableFile; + 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); + result.emplace_back(std::move(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..3bd5c6a23c 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.h @@ -30,15 +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); }; } \ No newline at end of file