diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index e1646184d6..382162dc12 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -180,6 +180,7 @@ FOF foldc foldcase FOLDERID +FONTHASH FORPARSING foundfr fsanitize @@ -427,6 +428,7 @@ pid pidl pidlist PKCS +PKEY pkgmgr pkindex pkix @@ -440,7 +442,10 @@ PRIMARYKEY processthreads productcode PRODUCTICON +propkey +PROPVARIANT proxystub +pwsz pscustomobject pseudocode PSHOST diff --git a/doc/windows/package-manager/winget/returnCodes.md b/doc/windows/package-manager/winget/returnCodes.md index caad7a5ef4..2acacdd1b4 100644 --- a/doc/windows/package-manager/winget/returnCodes.md +++ b/doc/windows/package-manager/winget/returnCodes.md @@ -146,6 +146,8 @@ ms.localizationpriority: medium | 0x8A150084 | -1978335100 | APPINSTALLER_CLI_ERROR_SFSCLIENT_PACKAGE_NOT_SUPPORTED | The Microsoft Store package does not support download command. | | 0x8A150085 | -1978335099 | APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED_FORBIDDEN | Failed to retrieve Microsoft Store package license. The Microsoft Entra Id account does not have required privilege. | | 0x8A150086 | -1978335098 | APPINSTALLER_CLI_ERROR_INSTALLER_ZERO_BYTE_FILE | Downloaded zero byte installer; ensure that your network connection is working properly. | +| 0x8A150087 | -1978335097 | APPINSTALLER_CLI_ERROR_FONT_INSTALL_FAILED | Failed to install font package. | +| 0x8A150088 | -1978335096 | APPINSTALLER_CLI_ERROR_FONT_FILE_NOT_SUPPORTED | Font file is not supported and cannot be installed. | ## Install errors. @@ -170,7 +172,7 @@ ms.localizationpriority: medium | 0x8A150111 | -1978334959 | APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE_BY_APPLICATION | Application is currently in use by another application. | | 0x8A150112 | -1978334958 | APPINSTALLER_CLI_ERROR_INSTALL_INVALID_PARAMETER | Invalid parameter. | | 0x8A150113 | -1978334957 | APPINSTALLER_CLI_ERROR_INSTALL_SYSTEM_NOT_SUPPORTED | Package not supported by the system. | -| 0x8A150114 | -1978334956 | APPINSTALLER_CLI_ERROR_INSTALL_UPGRADE_NOT_SUPPORTED | The installer does not support upgrading an existing package. | +| 0x8A150114 | -1978334956 | APPINSTALLER_CLI_ERROR_INSTALL_UPGRADE_NOT_SUPPORTED | The installer does not support upgrading an existing package. | | 0x8A150115 | -1978334955 | APPINSTALLER_CLI_ERROR_INSTALL_CUSTOM_ERROR | Installation failed with installer custom error. | ## Check for package installed status diff --git a/schemas/JSON/manifests/v1.10.0/manifest.installer.1.10.0.json b/schemas/JSON/manifests/v1.10.0/manifest.installer.1.10.0.json index 98da3b2c34..e7523e92c7 100644 --- a/schemas/JSON/manifests/v1.10.0/manifest.installer.1.10.0.json +++ b/schemas/JSON/manifests/v1.10.0/manifest.installer.1.10.0.json @@ -65,7 +65,8 @@ "wix", "burn", "pwa", - "portable" + "portable", + "font" ], "description": "Enumeration of supported installer types. InstallerType is required in either root level or individual Installer level" }, @@ -80,7 +81,8 @@ "nullsoft", "wix", "burn", - "portable" + "portable", + "font" ], "description": "Enumeration of supported nested installer types contained inside an archive file" }, diff --git a/schemas/JSON/manifests/v1.10.0/manifest.singleton.1.10.0.json b/schemas/JSON/manifests/v1.10.0/manifest.singleton.1.10.0.json index dd48208c79..380c62b227 100644 --- a/schemas/JSON/manifests/v1.10.0/manifest.singleton.1.10.0.json +++ b/schemas/JSON/manifests/v1.10.0/manifest.singleton.1.10.0.json @@ -167,7 +167,8 @@ "wix", "burn", "pwa", - "portable" + "portable", + "font" ], "description": "Enumeration of supported installer types. InstallerType is required in either root level or individual Installer level" }, @@ -182,7 +183,8 @@ "nullsoft", "wix", "burn", - "portable" + "portable", + "font" ], "description": "Enumeration of supported nested installer types contained inside an archive file" }, diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj index 45c9c0fa04..cfff631fb3 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -398,6 +398,7 @@ + @@ -483,6 +484,7 @@ + Create diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters index a8f48edd34..e5942dd17c 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -266,6 +266,9 @@ Workflows + + Header Files + @@ -502,6 +505,9 @@ Workflows + + Source Files + diff --git a/src/AppInstallerCLICore/Commands/FontCommand.cpp b/src/AppInstallerCLICore/Commands/FontCommand.cpp index c6faae5b44..7eb2f8e108 100644 --- a/src/AppInstallerCLICore/Commands/FontCommand.cpp +++ b/src/AppInstallerCLICore/Commands/FontCommand.cpp @@ -5,6 +5,7 @@ #include "Workflows/CompletionFlow.h" #include "Workflows/WorkflowBase.h" #include "Workflows/FontFlow.h" +#include "Workflows/InstallFlow.h" #include "Resources.h" namespace AppInstaller::CLI @@ -20,6 +21,7 @@ namespace AppInstaller::CLI { return InitializeFromMoveOnly>>({ std::make_unique(FullName()), + std::make_unique(FullName()), }); } @@ -43,6 +45,52 @@ namespace AppInstaller::CLI OutputHelp(context.Reporter); } + std::vector FontInstallCommand::GetArguments() const + { + return { + Argument::ForType(Args::Type::Manifest), + Argument{ Args::Type::InstallScope, Resource::String::InstallScopeDescription, ArgumentType::Standard, Argument::Visibility::Help }, + }; + } + + Resource::LocString FontInstallCommand::ShortDescription() const + { + return { Resource::String::FontInstallCommandShortDescription }; + } + + Resource::LocString FontInstallCommand::LongDescription() const + { + return { Resource::String::FontInstallCommandLongDescription }; + } + + void FontInstallCommand::Complete(Execution::Context& context, Args::Type valueType) const + { + UNREFERENCED_PARAMETER(valueType); + context.Reporter.Error() << Resource::String::PendingWorkError << std::endl; + } + + Utility::LocIndView FontInstallCommand::HelpLink() const + { + return s_FontCommand_HelpLink; + } + + void FontInstallCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const + { + Argument::ValidateCommonArguments(execArgs); + } + + void FontInstallCommand::ExecuteInternal(Execution::Context& context) const + { + if (context.Args.Contains(Execution::Args::Type::Manifest)) + { + context << + Workflow::ReportExecutionStage(ExecutionStage::Discovery) << + Workflow::GetManifestFromArg << + Workflow::SelectInstaller << + Workflow::InstallSinglePackage; + } + } + std::vector FontListCommand::GetArguments() const { return { diff --git a/src/AppInstallerCLICore/Commands/FontCommand.h b/src/AppInstallerCLICore/Commands/FontCommand.h index c29f703f6c..741821cd9c 100644 --- a/src/AppInstallerCLICore/Commands/FontCommand.h +++ b/src/AppInstallerCLICore/Commands/FontCommand.h @@ -21,6 +21,24 @@ namespace AppInstaller::CLI void ExecuteInternal(Execution::Context& context) const override; }; + struct FontInstallCommand final : public Command + { + FontInstallCommand(std::string_view parent) : Command("install", parent) {} + + std::vector GetArguments() const override; + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + void Complete(Execution::Context& context, Execution::Args::Type valueType) const override; + + Utility::LocIndView HelpLink() const override; + + protected: + void ValidateArgumentsInternal(Execution::Args& execArgs) const override; + void ExecuteInternal(Execution::Context& context) const override; + }; + struct FontListCommand final : public Command { FontListCommand(std::string_view parent) : Command("list", parent) {} diff --git a/src/AppInstallerCLICore/FontInstaller.cpp b/src/AppInstallerCLICore/FontInstaller.cpp new file mode 100644 index 0000000000..7cd4e38602 --- /dev/null +++ b/src/AppInstallerCLICore/FontInstaller.cpp @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "ExecutionContext.h" +#include "FontInstaller.h" +#include +#include +#include +#include +#include +#include + +namespace AppInstaller::CLI::Font +{ + namespace + { + constexpr std::wstring_view s_FontsPathSubkey = L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts"; + constexpr std::wstring_view s_TrueType = L" (TrueType)"; + + bool IsTrueTypeFont(DWRITE_FONT_FILE_TYPE fileType) + { + return ( + fileType == DWRITE_FONT_FILE_TYPE_TRUETYPE || + fileType == DWRITE_FONT_FILE_TYPE_TRUETYPE_COLLECTION + ); + } + } + + FontFile::FontFile(std::filesystem::path filePath, DWRITE_FONT_FILE_TYPE fileType) + : FilePath(std::move(filePath)), FileType(fileType) + { + Title = AppInstaller::Fonts::GetFontFileTitle(FilePath); + + if (IsTrueTypeFont(FileType)) + { + Title += s_TrueType; + } + } + + FontInstaller::FontInstaller(Manifest::ScopeEnum scope) : m_scope(scope) + { + if (scope == Manifest::ScopeEnum::Machine) + { + m_installLocation = Runtime::GetPathTo(Runtime::PathName::FontsMachineInstallLocation); + m_key = Registry::Key::Create(HKEY_LOCAL_MACHINE, std::wstring{ s_FontsPathSubkey }); + } + else + { + m_installLocation = Runtime::GetPathTo(Runtime::PathName::FontsUserInstallLocation); + m_key = Registry::Key::Create(HKEY_CURRENT_USER, std::wstring{ s_FontsPathSubkey }); + } + } + + bool FontInstaller::EnsureInstall() + { + for (auto& fontFile : m_fontFiles) + { + if (m_key[fontFile.Title].has_value()) + { + if (!std::filesystem::exists(m_key[fontFile.Title]->GetValue())) + { + AICLI_LOG(CLI, Info, << "Removing existing font value as font file does not exist."); + m_key.DeleteValue(fontFile.Title); + } + else + { + AICLI_LOG(CLI, Info, << "Existing font value found: " << AppInstaller::Utility::ConvertToUTF8(fontFile.Title)); + return false; + } + } + + std::filesystem::path destinationPath = m_installLocation / fontFile.FilePath.filename(); + auto initialStem = fontFile.FilePath.stem(); + auto extension = fontFile.FilePath.extension(); + + // If a file exists at the destination path, make the filename unique. + int index = 0; + while (std::filesystem::exists(destinationPath)) + { + std::filesystem::path unique = { "_" + std::to_string(index) }; + auto duplicateStem = initialStem; + duplicateStem += unique; + duplicateStem += extension; + destinationPath = m_installLocation / duplicateStem; + index++; + } + + fontFile.DestinationPath = std::move(destinationPath); + } + + return true; + } + + void FontInstaller::Install() + { + bool isMachineScope = m_scope == Manifest::ScopeEnum::Machine; + + for (const auto& fontFile : m_fontFiles) + { + AICLI_LOG(CLI, Info, << "Creating font value with name : " << AppInstaller::Utility::ConvertToUTF8(fontFile.Title)); + if (isMachineScope) + { + m_key.SetValue(fontFile.Title, fontFile.DestinationPath.filename(), REG_SZ); + } + else + { + m_key.SetValue(fontFile.Title, fontFile.DestinationPath, REG_SZ); + } + } + + for (const auto& fontFile : m_fontFiles) + { + AICLI_LOG(CLI, Info, << "Moving font file to: " << fontFile.DestinationPath); + AppInstaller::Filesystem::RenameFile(fontFile.FilePath, fontFile.DestinationPath); + } + } + + void FontInstaller::Uninstall() + { + for (const auto& fontFile : m_fontFiles) + { + if (m_key[fontFile.Title].has_value()) + { + AICLI_LOG(CLI, Info, << "Existing font value found:" << AppInstaller::Utility::ConvertToUTF8(fontFile.Title)); + std::filesystem::path existingFontFilePath = { m_key[fontFile.Title]->GetValue() }; + + if (m_scope == Manifest::ScopeEnum::Machine) + { + // Font entries in the HKEY_LOCAL_MACHINE hive only have the filename specified as the value. Prepend install location. + existingFontFilePath = m_installLocation / existingFontFilePath; + } + + if (std::filesystem::exists(existingFontFilePath)) + { + AICLI_LOG(CLI, Info, << "Removing existing font file at:" << existingFontFilePath); + std::filesystem::remove(existingFontFilePath); + } + + AICLI_LOG(CLI, Info, << "Deleting registry value:" << existingFontFilePath); + m_key.DeleteValue(fontFile.Title); + } + } + } +} diff --git a/src/AppInstallerCLICore/FontInstaller.h b/src/AppInstallerCLICore/FontInstaller.h new file mode 100644 index 0000000000..0e4395ddad --- /dev/null +++ b/src/AppInstallerCLICore/FontInstaller.h @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include +#include + +namespace AppInstaller::CLI::Font +{ + struct FontFile + { + FontFile(std::filesystem::path filePath, DWRITE_FONT_FILE_TYPE fileType); + + std::filesystem::path FilePath; + DWRITE_FONT_FILE_TYPE FileType; + std::wstring Title; + std::filesystem::path DestinationPath; + }; + + struct FontInstaller + { + FontInstaller(Manifest::ScopeEnum scope); + + std::filesystem::path FontFileLocation; + + void SetFontFiles(const std::vector& fontFiles) + { + m_fontFiles = fontFiles; + } + + // Checks if all expected registry values and font files can be installed prior to installation. + bool EnsureInstall(); + + void Install(); + + void Uninstall(); + + private: + Manifest::ScopeEnum m_scope; + std::filesystem::path m_installLocation; + Registry::Key m_key; + + std::vector m_fontFiles; + }; +} diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index cd56f587c2..c87a166f79 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -250,13 +250,18 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(FileNotFound); WINGET_DEFINE_RESOURCE_STRINGID(FilesRemainInInstallDirectory); WINGET_DEFINE_RESOURCE_STRINGID(FlagContainAdjoinedError); + WINGET_DEFINE_RESOURCE_STRINGID(FontAlreadyInstalled); WINGET_DEFINE_RESOURCE_STRINGID(FontCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(FontCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(FontFace); WINGET_DEFINE_RESOURCE_STRINGID(FontFaces); WINGET_DEFINE_RESOURCE_STRINGID(FontFamily); WINGET_DEFINE_RESOURCE_STRINGID(FontFamilyNameArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(FontFileNotSupported); WINGET_DEFINE_RESOURCE_STRINGID(FontFilePaths); + WINGET_DEFINE_RESOURCE_STRINGID(FontInstallCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(FontInstallCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(FontInstallFailed); WINGET_DEFINE_RESOURCE_STRINGID(FontListCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(FontListCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(FontVersion); diff --git a/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp b/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp index 084cbb4d3f..d97d046a46 100644 --- a/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp @@ -71,23 +71,30 @@ namespace AppInstaller::CLI::Workflow // Gets the file name that can be used to ShellExecute the file. std::filesystem::path GetInstallerPostHashValidationFileName(Execution::Context& context) { + const auto& installer = context.Get(); + // Get file name from download URI - std::filesystem::path filename = GetFileNameFromURI(context.Get()->Url); - std::wstring_view installerExtension = GetInstallerFileExtension(context); + std::filesystem::path filename = GetFileNameFromURI(installer->Url); - // Assuming that we find a safe stem value in the URI, use it. - // This should be extremely common, but just in case fall back to the older name style. - if (filename.has_stem() && ((filename.wstring().size() + installerExtension.size()) < MAX_PATH)) + // Default to URI for fonts since fonts can have multiple file extensions. + if (!DoesInstallerTypeSupportMultipleFileExtensions(installer->BaseInstallerType)) { - filename = filename.stem(); - } - else - { - const auto& manifest = context.Get(); - filename = Utility::ConvertToUTF16(manifest.Id + '.' + manifest.Version); - } + std::wstring_view installerExtension = GetInstallerFileExtension(context); - filename += installerExtension; + // Assuming that we find a safe stem value in the URI, use it. + // This should be extremely common, but just in case fall back to the older name style. + if (filename.has_stem() && ((filename.wstring().size() + installerExtension.size()) < MAX_PATH)) + { + filename = filename.stem(); + } + else + { + const auto& manifest = context.Get(); + filename = Utility::ConvertToUTF16(manifest.Id + '.' + manifest.Version); + } + + filename += installerExtension; + } // Make file name suitable for file system path filename = Utility::ConvertToUTF16(Utility::MakeSuitablePathPart(filename.u8string())); @@ -223,6 +230,7 @@ namespace AppInstaller::CLI::Workflow case InstallerTypeEnum::Portable: case InstallerTypeEnum::Wix: case InstallerTypeEnum::Zip: + case InstallerTypeEnum::Font: context << DownloadInstallerFile; break; case InstallerTypeEnum::Msix: diff --git a/src/AppInstallerCLICore/Workflows/FontFlow.cpp b/src/AppInstallerCLICore/Workflows/FontFlow.cpp index ce9bb7a233..dc9bb827ec 100644 --- a/src/AppInstallerCLICore/Workflows/FontFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/FontFlow.cpp @@ -5,10 +5,12 @@ #include "TableOutput.h" #include #include +#include namespace AppInstaller::CLI::Workflow { using namespace AppInstaller::CLI::Execution; + using namespace AppInstaller::CLI::Font; namespace { @@ -121,4 +123,74 @@ namespace AppInstaller::CLI::Workflow OutputInstalledFontFamiliesTable(context, lines); } } + + void FontInstallImpl(Execution::Context& context) + { + Manifest::ScopeEnum scope = Manifest::ScopeEnum::Unknown; + if (context.Args.Contains(Execution::Args::Type::InstallScope)) + { + scope = Manifest::ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); + } + + FontInstaller fontInstaller = FontInstaller(scope); + + context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl; + + try + { + const auto& installerPath = context.Get(); + std::vector filePaths; + + // InstallerPath will point to a directory if extracted from an archive. + if (std::filesystem::is_directory(installerPath)) + { + const std::vector& nestedInstallerFiles = context.Get()->NestedInstallerFiles; + for (const auto& nestedInstallerFile : nestedInstallerFiles) + { + filePaths.emplace_back(installerPath / ConvertToUTF16(nestedInstallerFile.RelativeFilePath)); + } + } + else + { + filePaths.emplace_back(installerPath); + } + + Fonts::FontCatalog fontCatalog; + std::vector fontFiles; + + for (std::filesystem::path filePath : filePaths) + { + DWRITE_FONT_FILE_TYPE fileType; + if (!fontCatalog.IsFontFileSupported(filePath, fileType)) + { + AICLI_LOG(CLI, Warning, << "Font file is not supported: " << filePath); + context.Reporter.Error() << Resource::String::FontFileNotSupported << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_FONT_FILE_NOT_SUPPORTED); + } + else + { + AICLI_LOG(CLI, Verbose, << "Font file is supported: " << filePath); + fontFiles.emplace_back(FontFile(filePath, fileType)); + } + } + + fontInstaller.SetFontFiles(fontFiles); + + if (!fontInstaller.EnsureInstall()) + { + context.Reporter.Warn() << Resource::String::FontAlreadyInstalled << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_FONT_ALREADY_INSTALLED); + } + + fontInstaller.Install(); + context.Add(S_OK); + } + catch (...) + { + context.Add(Workflow::HandleException(context, std::current_exception())); + context.Reporter.Warn() << Resource::String::FontInstallFailed << std::endl; + fontInstaller.Uninstall(); + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_INSTALL_FAILED); + } + } } diff --git a/src/AppInstallerCLICore/Workflows/FontFlow.h b/src/AppInstallerCLICore/Workflows/FontFlow.h index c3346e1a60..5ad5e0a2e8 100644 --- a/src/AppInstallerCLICore/Workflows/FontFlow.h +++ b/src/AppInstallerCLICore/Workflows/FontFlow.h @@ -10,4 +10,10 @@ namespace AppInstaller::CLI::Workflow // Inputs: None // Outputs: None void ReportInstalledFonts(Execution::Context& context); + + // Installs the font package. + // Required Args: None + // Inputs: Manifest, Scope, Rename, Location + // Outputs: None + void FontInstallImpl(Execution::Context& context); } diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp index e85540c379..dcd299340e 100644 --- a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "InstallFlow.h" #include "DownloadFlow.h" +#include "FontFlow.h" #include "UninstallFlow.h" #include "UpdateFlow.h" #include "ResumeFlow.h" @@ -276,6 +277,17 @@ namespace AppInstaller::CLI::Workflow VerifyAndSetNestedInstaller << ExecuteInstallerForType(context.Get().value().NestedInstallerType); } + + // Runs the flow for installing a font package. + // Required Args: None + // Inputs: Installer, InstallerPath + // Outputs: None + void FontInstall(Execution::Context& context) + { + context << + FontInstallImpl << + ReportInstallerResult("Font"sv, APPINSTALLER_CLI_ERROR_FONT_INSTALL_FAILED, true); + } } bool ExemptFromSingleInstallLocking(InstallerTypeEnum type) @@ -443,6 +455,9 @@ namespace AppInstaller::CLI::Workflow case InstallerTypeEnum::Zip: context << details::ArchiveInstall; break; + case InstallerTypeEnum::Font: + context << details::FontInstall; + break; default: THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); } diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.h b/src/AppInstallerCLICore/Workflows/InstallFlow.h index cbc0ce70bc..cc3df3810f 100644 --- a/src/AppInstallerCLICore/Workflows/InstallFlow.h +++ b/src/AppInstallerCLICore/Workflows/InstallFlow.h @@ -49,6 +49,12 @@ namespace AppInstaller::CLI::Workflow // Inputs: Installer, InstallerPath, Manifest // Outputs: None void ArchiveInstall(Execution::Context& context); + + // Runs the flow for installing a font package. + // Required Args: None + // Inputs: Installer, InstallerPath, Manifest + // Outputs: None + void FontInstall(Execution::Context& context); } // Ensures that there is an applicable installer. diff --git a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj index 179ffb1583..5806dd9080 100644 --- a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj +++ b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj @@ -61,6 +61,8 @@ + + diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs index 70d3023a18..9586c7950b 100644 --- a/src/AppInstallerCLIE2ETests/Constants.cs +++ b/src/AppInstallerCLIE2ETests/Constants.cs @@ -27,6 +27,7 @@ public class Constants public const string MsiInstallerPathParameter = "MsiTestInstallerPath"; public const string MsiInstallerV2PathParameter = "MsiTestInstallerV2Path"; public const string MsixInstallerPathParameter = "MsixTestInstallerPath"; + public const string FontPathParameter = "FontTestPath"; public const string PackageCertificatePathParameter = "PackageCertificatePath"; public const string PowerShellModulePathParameter = "PowerShellModulePath"; public const string SkipTestSourceParameter = "SkipTestSource"; @@ -55,11 +56,13 @@ public class Constants public const string MsiInstaller = "AppInstallerTestMsiInstaller"; public const string MsixInstaller = "AppInstallerTestMsixInstaller"; public const string ZipInstaller = "AppInstallerTestZipInstaller"; + public const string Font = "AppInstallerTestFont"; public const string ExeInstallerFileName = "AppInstallerTestExeInstaller.exe"; public const string MsiInstallerFileName = "AppInstallerTestMsiInstaller.msi"; public const string MsiInstallerV2FileName = "AppInstallerTestMsiInstallerV2.msi"; public const string MsixInstallerFileName = "AppInstallerTestMsixInstaller.msix"; public const string ZipInstallerFileName = "AppInstallerTestZipInstaller.zip"; + public const string FontFileName = "AppInstallerTestFont.ttf"; public const string ModifyRepairInstaller = "AppInstallerTest.TestModifyRepair"; public const string IndexPackage = "source.msix"; public const string MakeAppx = "makeappx.exe"; @@ -118,6 +121,8 @@ public class Constants public const string UninstallSubKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall"; public const string PathSubKey_User = @"Environment"; public const string PathSubKey_Machine = @"SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; + public const string FontsSubKey = @"Software\Microsoft\Windows NT\CurrentVersion\Fonts"; + public const string TestFontSubKeyName = "Cascadia Code PL Regular (TrueType)"; // User settings public const string ArchiveExtractionMethod = "archiveExtractionMethod"; @@ -272,6 +277,8 @@ public class ErrorCode public const int ERROR_ADMIN_CONTEXT_REPAIR_PROHIBITED = unchecked((int)0x8A15007D); public const int ERROR_INSTALLER_ZERO_BYTE_FILE = unchecked((int)0x8A150086); + public const int ERROR_FONT_INSTALL_FAILED = unchecked((int)0x8A150087); + public const int ERROR_FONT_FILE_NOT_SUPPORTED = unchecked((int)0x8A150088); public const int ERROR_INSTALL_PACKAGE_IN_USE = unchecked((int)0x8A150101); public const int ERROR_INSTALL_INSTALL_IN_PROGRESS = unchecked((int)0x8A150102); diff --git a/src/AppInstallerCLIE2ETests/FontCommand.cs b/src/AppInstallerCLIE2ETests/FontCommand.cs new file mode 100644 index 0000000000..1292caad24 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/FontCommand.cs @@ -0,0 +1,61 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests +{ + using AppInstallerCLIE2ETests.Helpers; + using NUnit.Framework; + + /// + /// Test font command. + /// + public class FontCommand : BaseCommand + { + /// + /// One time set up. + /// + [OneTimeSetUp] + public void OneTimeSetup() + { + WinGetSettingsHelper.ConfigureFeature("fonts", true); + } + + /// + /// Test install a font with user scope. + /// + [Test] + public void InstallFont_UserScope() + { + var result = TestCommon.RunAICLICommand("install", "AppInstallerTest.TestFont"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + TestCommon.VerifyFontPackage(Constants.TestFontSubKeyName, Constants.FontFileName, TestCommon.Scope.User); + } + + /// + /// Test install a font with machine scope. + /// + [Test] + public void InstallFont_MachineScope() + { + var result = TestCommon.RunAICLICommand("install", "AppInstallerTest.TestFont --scope Machine"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + TestCommon.VerifyFontPackage(Constants.TestFontSubKeyName, Constants.FontFileName, TestCommon.Scope.Machine); + } + + /// + /// Test install an invalid font file. + /// + [Test] + public void InstallInvalidFont() + { + var result = TestCommon.RunAICLICommand("install", "AppInstallerTest.TestInvalidFont"); + Assert.AreEqual(Constants.ErrorCode.ERROR_FONT_FILE_NOT_SUPPORTED, result.ExitCode); + Assert.True(result.StdOut.Contains("The font file is not supported and cannot be installed.")); + } + } +} diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs index 88fb61d5c0..f0b640abff 100644 --- a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs @@ -344,6 +344,81 @@ public static string GetCheckpointsDirectory() } } + /// + /// Gets the fonts directory based on scope. + /// + /// Scope. + /// The path of the fonts directory. + public static string GetFontsDirectory(Scope scope) + { + if (scope == Scope.Machine) + { + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts"); + } + else + { + return Path.Combine(Environment.GetEnvironmentVariable("LocalAppData"), "Microsoft", "Windows", "Fonts"); + } + } + + /// + /// Verify font package. + /// + /// Name of the font registry subkey entry. + /// Filename of the installed font file. + /// Scope. + /// Should exist. + public static void VerifyFontPackage( + string fontSubKeyName, + string fontFileName, + Scope scope = Scope.User, + bool shouldExist = true) + { + // Expected font file path. + string expectedFontInstallPath = Path.Combine(GetFontsDirectory(scope), fontFileName); + bool fontFileExists = File.Exists(expectedFontInstallPath); + + // Expected font registry entry. + bool fontEntryExists = false; + RegistryKey baseKey = (scope == Scope.Machine) ? Registry.LocalMachine : Registry.CurrentUser; + string expectedSubKeyValue = (scope == Scope.Machine) ? Path.GetFileName(expectedFontInstallPath) : expectedFontInstallPath; + using (RegistryKey fontsRegistryKey = baseKey.OpenSubKey(Constants.FontsSubKey, true)) + { + var fontSubKeyValue = fontsRegistryKey.GetValue(fontSubKeyName); + if (fontSubKeyValue != null) + { + fontEntryExists = fontSubKeyValue.Equals(expectedSubKeyValue); + } + } + + if (shouldExist) + { + // TODO: Replace with font uninstall when implemented. + if (fontEntryExists) + { + using (RegistryKey fontsRegistryKey = baseKey.OpenSubKey(Constants.FontsSubKey, true)) + { + fontsRegistryKey.DeleteValue(fontSubKeyName); + } + } + + if (fontFileExists) + { + try + { + File.Delete(expectedFontInstallPath); + } + catch (UnauthorizedAccessException) + { + // TODO: This error occurs for user mode if the font is in use. Skip cleanup. + } + } + } + + Assert.AreEqual(shouldExist, fontFileExists, $"Expected font path: {expectedFontInstallPath}"); + Assert.AreEqual(shouldExist, fontEntryExists, $"Expected {fontSubKeyName} subkey with value {expectedSubKeyValue} in registry path: {Constants.FontsSubKey}"); + } + /// /// Verify portable package. /// @@ -351,7 +426,7 @@ public static string GetCheckpointsDirectory() /// Command alias. /// File name. /// Product code. - /// Should exists. + /// Should exist. /// Scope. /// Install directory added to path instead of the symlink directory. public static void VerifyPortablePackage( diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestIndex.cs b/src/AppInstallerCLIE2ETests/Helpers/TestIndex.cs index 2728661e86..d2eb625e7a 100644 --- a/src/AppInstallerCLIE2ETests/Helpers/TestIndex.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/TestIndex.cs @@ -8,7 +8,6 @@ namespace AppInstallerCLIE2ETests.Helpers { using System; using System.IO; - using System.Text.Json; using Microsoft.WinGetSourceCreator; using WinGetSourceCreator.Model; @@ -25,6 +24,7 @@ static TestIndex() TestIndex.MsiInstallerV2 = Path.Combine(TestSetup.Parameters.StaticFileRootPath, Constants.MsiInstaller, Constants.MsiInstallerV2FileName); TestIndex.MsixInstaller = Path.Combine(TestSetup.Parameters.StaticFileRootPath, Constants.MsixInstaller, Constants.MsixInstallerFileName); TestIndex.ZipInstaller = Path.Combine(TestSetup.Parameters.StaticFileRootPath, Constants.ZipInstaller, Constants.ZipInstallerFileName); + TestIndex.Font = Path.Combine(TestSetup.Parameters.StaticFileRootPath, Constants.Font, Constants.FontFileName); } /// @@ -52,6 +52,11 @@ static TestIndex() /// public static string ZipInstaller { get; private set; } + /// + /// Gets the font file path used by the manifests in the E2E test. + /// + public static string Font { get; private set; } + /// /// Generate test source. /// @@ -99,6 +104,16 @@ public static void GenerateE2ESource() throw new FileNotFoundException(testParams.MsixInstallerPath); } + if (string.IsNullOrEmpty(testParams.FontPath)) + { + throw new ArgumentNullException($"{Constants.FontPathParameter} is required"); + } + + if (!File.Exists(testParams.FontPath)) + { + throw new FileNotFoundException(testParams.FontPath); + } + if (string.IsNullOrEmpty(testParams.PackageCertificatePath)) { throw new ArgumentNullException($"{Constants.PackageCertificatePathParameter} is required"); @@ -148,6 +163,13 @@ public static void GenerateE2ESource() HashToken = "", SignatureToken = "", }, + new LocalInstaller + { + Type = InstallerType.Font, + Name = Path.Combine(Constants.Font, Constants.FontFileName), + Input = testParams.FontPath, + HashToken = "", + }, }, DynamicInstallers = new () { diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs b/src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs index b681fc710f..73faa5afcd 100644 --- a/src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs @@ -44,6 +44,7 @@ private TestSetup() this.MsiInstallerPath = this.InitializeFileParam(Constants.MsiInstallerPathParameter); this.MsixInstallerPath = this.InitializeFileParam(Constants.MsixInstallerPathParameter); this.MsiInstallerV2Path = this.InitializeFileParam(Constants.MsiInstallerV2PathParameter); + this.MsiInstallerV2Path = this.InitializeFileParam(Constants.FontPathParameter); this.ForcedExperimentalFeatures = this.InitializeStringArrayParam(Constants.ForcedExperimentalFeaturesParameter); } @@ -119,6 +120,11 @@ public static TestSetup Parameters /// public string ZipInstallerPath { get; } + /// + /// Gets the font path. + /// + public string FontPath { get; } + /// /// Gets the package cert path. /// diff --git a/src/AppInstallerCLIE2ETests/Test.runsettings b/src/AppInstallerCLIE2ETests/Test.runsettings index 6f6079e24e..6c4742a3f4 100644 --- a/src/AppInstallerCLIE2ETests/Test.runsettings +++ b/src/AppInstallerCLIE2ETests/Test.runsettings @@ -1,4 +1,4 @@ - +