commit e9651d53110d085ace38d028a4561248c52539b3 parent afc2228364cba053d4e99e8a16d926239be0d204 Author: yao-msft <50888816+yao-msft@users.noreply.github.com> Date: Wed, 5 Feb 2020 13:31:25 -0800 Refactor Install flow and add msix install support (#28) * HttpStream ready, signature ready, install success..., minor fixes * Refactor, manifest modifeied * Refactor and fix existing tests. * Add some msix install tests * Minor fixes * fix debug warnings * Fix break * PR comments * PR comments * Rebase and PR comments * One more PR comment Diffstat:
45 files changed, 1770 insertions(+), 254 deletions(-)
diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -169,14 +169,17 @@ <ClInclude Include="Commands\DescribeCommand.h" /> <ClInclude Include="Commands\InstallCommand.h" /> <ClInclude Include="Commands\RootCommand.h" /> - <ClInclude Include="Workflows\Common.h" /> - <ClInclude Include="Workflows\InstallFlow.h" /> <ClInclude Include="Invocation.h" /> <ClInclude Include="Localization.h" /> <ClInclude Include="pch.h" /> <ClInclude Include="Public\AppInstallerCLICore.h" /> <ClInclude Include="Search\Search.h" /> + <ClInclude Include="Workflows\Common.h" /> + <ClInclude Include="Workflows\ShellExecuteInstallerHandler.h" /> + <ClInclude Include="Workflows\InstallerHandlerBase.h" /> + <ClInclude Include="Workflows\InstallFlow.h" /> <ClInclude Include="Workflows\ManifestComparator.h" /> + <ClInclude Include="Workflows\MsixInstallerHandler.h" /> <ClInclude Include="Workflows\WorkflowReporter.h" /> </ItemGroup> <ItemGroup> @@ -185,11 +188,14 @@ <ClCompile Include="Commands\InstallCommand.cpp" /> <ClCompile Include="Commands\RootCommand.cpp" /> <ClCompile Include="Core.cpp" /> - <ClCompile Include="Workflows\ManifestComparator.cpp" /> - <ClCompile Include="Workflows\InstallFlow.cpp" /> <ClCompile Include="pch.cpp"> <PrecompiledHeader>Create</PrecompiledHeader> </ClCompile> + <ClCompile Include="Workflows\ShellExecuteInstallerHandler.cpp" /> + <ClCompile Include="Workflows\InstallerHandlerBase.cpp" /> + <ClCompile Include="Workflows\InstallFlow.cpp" /> + <ClCompile Include="Workflows\ManifestComparator.cpp" /> + <ClCompile Include="Workflows\MsixInstallerHandler.cpp" /> <ClCompile Include="Workflows\WorkflowReporter.cpp" /> </ItemGroup> <ItemGroup> diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -66,6 +66,15 @@ <ClInclude Include="Workflows\Common.h"> <Filter>Workflows</Filter> </ClInclude> + <ClInclude Include="Workflows\InstallerHandlerBase.h"> + <Filter>Workflows</Filter> + </ClInclude> + <ClInclude Include="Workflows\MsixInstallerHandler.h"> + <Filter>Workflows</Filter> + </ClInclude> + <ClInclude Include="Workflows\ShellExecuteInstallerHandler.h"> + <Filter>Workflows</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -95,6 +104,15 @@ <ClCompile Include="Workflows\WorkflowReporter.cpp"> <Filter>Workflows</Filter> </ClCompile> + <ClCompile Include="Workflows\InstallerHandlerBase.cpp"> + <Filter>Workflows</Filter> + </ClCompile> + <ClCompile Include="Workflows\MsixInstallerHandler.cpp"> + <Filter>Workflows</Filter> + </ClCompile> + <ClCompile Include="Workflows\ShellExecuteInstallerHandler.cpp"> + <Filter>Workflows</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLICore/Core.cpp b/src/AppInstallerCLICore/Core.cpp @@ -5,7 +5,7 @@ #include "Commands/RootCommand.h" using namespace winrt; -using namespace Windows::Foundation; +using namespace winrt::Windows::Foundation; using namespace AppInstaller::CLI; namespace AppInstaller::CLI diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -3,9 +3,12 @@ #include "pch.h" #include "InstallFlow.h" -#include "AppInstallerDownloader.h" #include "ManifestComparator.h" +#include "ShellExecuteInstallerHandler.h" +#include "MsixInstallerHandler.h" +using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::Management::Deployment; using namespace AppInstaller::Utility; using namespace AppInstaller::Manifest; @@ -13,6 +16,16 @@ namespace AppInstaller::Workflow { void InstallFlow::Install() { + ProcessManifest(); + + auto installerHandler = GetInstallerHandler(); + + installerHandler->Download(); + installerHandler->Install(); + } + + void InstallFlow::ProcessManifest() + { ManifestComparator manifestComparator(m_packageManifest, m_reporter); m_selectedLocalization = manifestComparator.GetPreferredLocalization(std::locale("")); @@ -27,144 +40,18 @@ namespace AppInstaller::Workflow { ); m_selectedInstaller = manifestComparator.GetPreferredInstaller(std::locale("")); - - DownloadInstaller(); - ExecuteInstaller(); } - void InstallFlow::DownloadInstaller() + std::unique_ptr<InstallerHandlerBase> InstallFlow::GetInstallerHandler() { - // Todo: Rework the path logic. The new path logic should work with MOTW. - std::filesystem::path tempInstallerPath = Runtime::GetPathToTemp(); - tempInstallerPath /= m_packageManifest.Id + '_' + m_packageManifest.Version + '.' + m_selectedInstaller.InstallerType; - - AICLI_LOG(CLI, Info, << "Generated temp download path: " << tempInstallerPath); - - auto downloader = Downloader::StartDownloadAsync( - m_selectedInstaller.Url, - tempInstallerPath, - true, - &m_reporter.GetDownloaderCallback()); - - auto downloadResult = downloader->Wait(); - - if (downloadResult == DownloaderResult::Failed) + switch (m_selectedInstaller.InstallerType) { - m_reporter.ShowMsg(WorkflowReporter::Level::Error, "Package download failed."); - THROW_EXCEPTION_MSG(WorkflowException(APPINSTALLER_CLI_ERROR_INSTALLFLOW_FAILED), "Package download failed"); + case ManifestInstaller::InstallerTypeEnum::Exe: + return std::make_unique<ShellExecuteInstallerHandler>(m_selectedInstaller, m_reporter); + case ManifestInstaller::InstallerTypeEnum::Msix: + return std::make_unique<MsixInstallerHandler>(m_selectedInstaller, m_reporter); + default: + THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); } - else if (downloadResult == DownloaderResult::Canceled) - { - m_reporter.ShowMsg(WorkflowReporter::Level::Info, "Package download canceled."); - THROW_EXCEPTION_MSG(WorkflowException(APPINSTALLER_CLI_ERROR_INSTALLFLOW_FAILED), "Package download canceled"); - } - - if (!std::equal( - m_selectedInstaller.Sha256.begin(), - m_selectedInstaller.Sha256.end(), - downloader->GetDownloadHash().begin())) - { - AICLI_LOG(CLI, Error, - << "Package hash verification failed. SHA256 in manifest: " - << SHA256::ConvertToString(m_selectedInstaller.Sha256) - << "SHA256 from download: " - << SHA256::ConvertToString(downloader->GetDownloadHash())); - - if (!m_reporter.PromptForBoolResponse(WorkflowReporter::Level::Warning, "Package hash verification failed. Continue?")) - { - m_reporter.ShowMsg(WorkflowReporter::Level::Error, "Canceled. Package hash mismatch."); - THROW_EXCEPTION_MSG(WorkflowException(APPINSTALLER_CLI_ERROR_INSTALLFLOW_FAILED), "Package installation canceled"); - } - } - else - { - AICLI_LOG(CLI, Info, << "Downloaded package hash verified"); - m_reporter.ShowMsg(WorkflowReporter::Level::Info, "Successfully verified SHA256."); - } - - m_downloadedInstaller = tempInstallerPath; - } - - void InstallFlow::ExecuteInstaller() - { - if (m_downloadedInstaller.empty()) - { - THROW_EXCEPTION_MSG(WorkflowException(APPINSTALLER_CLI_ERROR_INSTALLFLOW_FAILED), "Installer not downloaded yet"); - } - - m_reporter.ShowMsg(WorkflowReporter::Level::Info, "Installing package ..."); - - std::string installerArgs = GetInstallerArgs(); - AICLI_LOG(CLI, Info, << "Installer args: " << installerArgs); - - // Todo: add support for other installer types - std::future<DWORD> installTask; - if (Utility::ToLower(m_selectedInstaller.InstallerType) == "exe") - { - installTask = ExecuteExeInstallerAsync(m_downloadedInstaller, installerArgs); - } - else - { - m_reporter.ShowMsg(WorkflowReporter::Level::Error, "Installer type not supported."); - THROW_EXCEPTION_MSG(WorkflowException(APPINSTALLER_CLI_ERROR_INSTALLFLOW_FAILED), "Installer type not supported"); - } - - m_reporter.ShowIndefiniteSpinner(true); - - installTask.wait(); - - m_reporter.ShowIndefiniteSpinner(false); - - auto installResult = installTask.get(); - - if (installResult != 0) - { - m_reporter.ShowMsg(WorkflowReporter::Level::Error, "Install failed. Exit code: " + std::to_string(installResult)); - - THROW_EXCEPTION_MSG(WorkflowException(APPINSTALLER_CLI_ERROR_INSTALLFLOW_FAILED), - "Install failed. Installer task returned: %u", installResult); - } - - m_reporter.ShowMsg(WorkflowReporter::Level::Info, "Successfully installed!"); - } - - std::future<DWORD> InstallFlow::ExecuteExeInstallerAsync(const std::filesystem::path& filePath, const std::string& args) - { - AICLI_LOG(CLI, Info, << "Staring EXE installer. Path: " << filePath); - return std::async(std::launch::async, [&filePath, &args] { - - SHELLEXECUTEINFOA execInfo = { 0 }; - execInfo.cbSize = sizeof(SHELLEXECUTEINFO); - execInfo.fMask = SEE_MASK_NOCLOSEPROCESS; - std::string filePathUTF8Str = Utility::ConvertToUTF8(filePath.c_str()); - execInfo.lpFile = filePathUTF8Str.c_str(); - execInfo.lpParameters = args.c_str(); - execInfo.nShow = SW_SHOW; - if (!ShellExecuteExA(&execInfo) || !execInfo.hProcess) - { - return GetLastError(); - } - // Wait for installation to finish - WaitForSingleObject(execInfo.hProcess, INFINITE); - - // Get exe exit code - DWORD exitCode; - GetExitCodeProcess(execInfo.hProcess, &exitCode); - - CloseHandle(execInfo.hProcess); - - return exitCode; - }); - } - - std::string InstallFlow::GetInstallerArgs() - { - // Todo: Implement arg selection logic. - if (m_selectedInstaller.Switches.has_value()) - { - return m_selectedInstaller.Switches.value().Default; - } - - return ""; } } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.h b/src/AppInstallerCLICore/Workflows/InstallFlow.h @@ -3,10 +3,11 @@ #pragma once #include "Common.h" +#include "InstallerHandlerBase.h" #include "WorkflowReporter.h" -namespace AppInstaller::Workflow { - +namespace AppInstaller::Workflow +{ class InstallFlow { public: @@ -19,13 +20,11 @@ namespace AppInstaller::Workflow { AppInstaller::Manifest::Manifest m_packageManifest; AppInstaller::Manifest::ManifestInstaller m_selectedInstaller; AppInstaller::Manifest::ManifestLocalization m_selectedLocalization; - std::filesystem::path m_downloadedInstaller; WorkflowReporter m_reporter; - virtual void DownloadInstaller(); - virtual void ExecuteInstaller(); - std::string GetInstallerArgs(); + virtual void ProcessManifest(); - std::future<DWORD> ExecuteExeInstallerAsync(const std::filesystem::path& filePath, const std::string& args); + // Creates corresponding InstallerHandler according to InstallerType + virtual std::unique_ptr<InstallerHandlerBase> GetInstallerHandler(); }; } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/InstallerHandlerBase.cpp b/src/AppInstallerCLICore/Workflows/InstallerHandlerBase.cpp @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" +#include "Common.h" +#include "InstallerHandlerBase.h" + +using namespace AppInstaller::Utility; +using namespace AppInstaller::Manifest; + +namespace AppInstaller::Workflow +{ + void InstallerHandlerBase::Download() + { + // Todo: Rework the path logic. The new path logic should work with MOTW. + std::filesystem::path tempInstallerPath = Runtime::GetPathToTemp(); + tempInstallerPath /= SHA256::ConvertToString(m_manifestInstallerRef.Sha256); + + AICLI_LOG(CLI, Info, << "Generated temp download path: " << tempInstallerPath); + + auto downloader = Downloader::StartDownloadAsync( + m_manifestInstallerRef.Url, + tempInstallerPath, + true, + &m_downloaderCallback); + + auto downloadResult = downloader->Wait(); + + if (downloadResult == DownloaderResult::Failed) + { + m_reporterRef.ShowMsg(WorkflowReporter::Level::Error, "Package download failed."); + THROW_EXCEPTION_MSG(WorkflowException(APPINSTALLER_CLI_ERROR_INSTALLFLOW_FAILED), "Package download failed"); + } + else if (downloadResult == DownloaderResult::Canceled) + { + m_reporterRef.ShowMsg(WorkflowReporter::Level::Info, "Package download canceled."); + THROW_EXCEPTION_MSG(WorkflowException(APPINSTALLER_CLI_ERROR_INSTALLFLOW_FAILED), "Package download canceled"); + } + + if (!std::equal( + m_manifestInstallerRef.Sha256.begin(), + m_manifestInstallerRef.Sha256.end(), + downloader->GetDownloadHash().begin())) + { + AICLI_LOG(CLI, Error, + << "Package hash verification failed. SHA256 in manifest: " + << SHA256::ConvertToString(m_manifestInstallerRef.Sha256) + << "SHA256 from download: " + << SHA256::ConvertToString(downloader->GetDownloadHash())); + + if (!m_reporterRef.PromptForBoolResponse(WorkflowReporter::Level::Warning, "Package hash verification failed. Continue?")) + { + m_reporterRef.ShowMsg(WorkflowReporter::Level::Error, "Canceled. Package hash mismatch."); + THROW_EXCEPTION_MSG(WorkflowException(APPINSTALLER_CLI_ERROR_INSTALLFLOW_FAILED), "Package installation canceled"); + } + } + else + { + AICLI_LOG(CLI, Info, << "Downloaded installer hash verified"); + m_reporterRef.ShowMsg(WorkflowReporter::Level::Info, "Successfully verified SHA256."); + } + + m_downloadedInstaller = tempInstallerPath; + } + + void InstallerHandlerBase::DownloaderCallback::OnStarted() + { + m_reporterRef.ShowMsg(WorkflowReporter::Level::Info, "Starting package download ..."); + m_reporterRef.ShowProgress(true, 0); + } + + void InstallerHandlerBase::DownloaderCallback::OnProgress(LONGLONG progress, LONGLONG downloadSize) + { + int progressPercent = static_cast<int>(100 * progress / downloadSize); + m_reporterRef.ShowProgress(true, progressPercent); + } + + void InstallerHandlerBase::DownloaderCallback::OnCanceled() + { + m_reporterRef.ShowProgress(false, 0); + m_reporterRef.ShowMsg(WorkflowReporter::Level::Warning, "Package download canceled."); + } + + void InstallerHandlerBase::DownloaderCallback::OnCompleted() + { + m_reporterRef.ShowProgress(false, 0); + m_reporterRef.ShowMsg(WorkflowReporter::Level::Error, "Package download completed."); + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/InstallerHandlerBase.h b/src/AppInstallerCLICore/Workflows/InstallerHandlerBase.h @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once +#include "pch.h" +#include "WorkflowReporter.h" + +namespace AppInstaller::Workflow +{ + // This is the base class for installer handlers. Individual installer handler should override + // member methods to do appropriate work on different installers. + class InstallerHandlerBase + { + public: + + // The Download method downloads installer to local temp folder. + // The downloaded installer does not have any extension appended. + // SHA256 of the downloaded installer is verified during download. + virtual void Download(); + + virtual void Install() { THROW_HR(E_NOTIMPL); } + virtual void Cancel() { THROW_HR(E_NOTIMPL); } + + protected: + + // This will be triggered by file downloader to report download progress + class DownloaderCallback : public AppInstaller::Utility::IDownloaderCallback + { + public: + DownloaderCallback(WorkflowReporter& reporter) : m_reporterRef(reporter) {}; + + void OnStarted() override; + void OnProgress(LONGLONG progress, LONGLONG downloadSize) override; + void OnCanceled() override; + void OnCompleted() override; + + private: + WorkflowReporter& m_reporterRef; + }; + + InstallerHandlerBase(const Manifest::ManifestInstaller& manifestInstaller, WorkflowReporter& reporter) : + m_manifestInstallerRef(manifestInstaller), m_reporterRef(reporter), m_downloaderCallback(reporter) {}; + + const Manifest::ManifestInstaller& m_manifestInstallerRef; + WorkflowReporter& m_reporterRef; + std::filesystem::path m_downloadedInstaller; + DownloaderCallback m_downloaderCallback; + }; +} + diff --git a/src/AppInstallerCLICore/Workflows/MsixInstallerHandler.cpp b/src/AppInstallerCLICore/Workflows/MsixInstallerHandler.cpp @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" +#include "Common.h" +#include "MsixInstallerHandler.h" + +using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::Management::Deployment; +using namespace AppInstaller::Utility; +using namespace AppInstaller::Manifest; + +namespace AppInstaller::Workflow +{ + MsixInstallerHandler::MsixInstallerHandler( + const Manifest::ManifestInstaller& manifestInstaller, + WorkflowReporter& reporter) : + InstallerHandlerBase(manifestInstaller, reporter) + { + if (manifestInstaller.InstallerType != ManifestInstaller::InstallerTypeEnum::Msix) + { + THROW_HR_MSG(E_UNEXPECTED, "Installer type not supported."); + } + } + + void MsixInstallerHandler::Download() + { + if (m_manifestInstallerRef.SignatureSha256.empty()) + { + // Signature hash not provided. Go with download flow. + InstallerHandlerBase::Download(); + m_useStreaming = false; + } + else + { + // Signature hash provided. No download needed. Just verify signature hash. + Msix::MsixInfo msixInfo(m_manifestInstallerRef.Url); + auto signature = msixInfo.GetSignature(); + + SHA256::HashBuffer signatureHash; + SHA256::ComputeHash(signature.data(), static_cast<uint32_t>(signature.size()), signatureHash); + + if (!std::equal( + m_manifestInstallerRef.SignatureSha256.begin(), + m_manifestInstallerRef.SignatureSha256.end(), + signatureHash.begin())) + { + AICLI_LOG(CLI, Error, + << "Package hash verification failed. Signature SHA256 in manifest: " + << SHA256::ConvertToString(m_manifestInstallerRef.SignatureSha256) + << "Signature SHA256 from download: " + << SHA256::ConvertToString(signatureHash)); + + if (!m_reporterRef.PromptForBoolResponse(WorkflowReporter::Level::Warning, "Package hash verification failed. Continue?")) + { + m_reporterRef.ShowMsg(WorkflowReporter::Level::Error, "Canceled. Package hash mismatch."); + THROW_EXCEPTION_MSG(WorkflowException(APPINSTALLER_CLI_ERROR_INSTALLFLOW_FAILED), "Package installation canceled"); + } + } + else + { + AICLI_LOG(CLI, Info, << "Msix package signature hash verified"); + m_reporterRef.ShowMsg(WorkflowReporter::Level::Info, "Successfully verified SHA256."); + } + + m_useStreaming = true; + } + } + + void MsixInstallerHandler::Install() + { + if (!m_useStreaming && m_downloadedInstaller.empty()) + { + THROW_EXCEPTION_MSG(WorkflowException(APPINSTALLER_CLI_ERROR_INSTALLFLOW_FAILED), "Installer not downloaded yet"); + } + + auto installTask = ExecuteInstallerAsync( + m_useStreaming ? Uri(Utility::ConvertToUTF16(m_manifestInstallerRef.Url)) : Uri(m_downloadedInstaller.c_str())); + + installTask.get(); + } + + std::future<void> MsixInstallerHandler::ExecuteInstallerAsync(const Uri& uri) + { + PackageManager packageManager; + DeploymentOptions deploymentOptions = + DeploymentOptions::ForceApplicationShutdown | + DeploymentOptions::ForceTargetApplicationShutdown; + + m_reporterRef.ShowMsg(WorkflowReporter::Level::Info, "Starting package install..."); + m_reporterRef.ShowProgress(true, 0); + + // RequestAddPackageAsync will invoke smart screen. + auto deployOperation = packageManager.RequestAddPackageAsync( + uri, + nullptr, /*dependencyPackageUris*/ + deploymentOptions, + nullptr, /*targetVolume*/ + nullptr, /*optionalAndRelatedPackageFamilyNames*/ + nullptr /*relatedPackageUris*/); + + AsyncOperationProgressHandler<DeploymentResult, DeploymentProgress> progressCallback( + [this](const IAsyncOperationWithProgress<DeploymentResult, DeploymentProgress>&, DeploymentProgress progress) + { + // Todo: might need to tweak progress reporting logic to account + // for the time before DeploymentRequest is dequeued. + m_reporterRef.ShowProgress(true, progress.percentage); + } + ); + + // Set progress callback. + deployOperation.Progress(progressCallback); + + co_await deployOperation; + + auto deployResult = deployOperation.GetResults(); + + m_reporterRef.ShowProgress(false, 0); + + if (!SUCCEEDED(deployResult.ExtendedErrorCode())) + { + m_reporterRef.ShowMsg(WorkflowReporter::Level::Error, "Install failed. Reason: " + Utility::ConvertToUTF8(deployResult.ErrorText())); + + THROW_EXCEPTION_MSG(WorkflowException(APPINSTALLER_CLI_ERROR_INSTALLFLOW_FAILED), + "Install failed. Installer task returned: %u", deployResult.ExtendedErrorCode()); + } + + m_reporterRef.ShowMsg(WorkflowReporter::Level::Info, "Successfully installed."); + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/MsixInstallerHandler.h b/src/AppInstallerCLICore/Workflows/MsixInstallerHandler.h @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once +#include "InstallerHandlerBase.h" + +namespace AppInstaller::Workflow +{ + // MsixInstallerHandler handles appx/msix installers. + class MsixInstallerHandler : public InstallerHandlerBase + { + public: + MsixInstallerHandler( + const Manifest::ManifestInstaller& manifestInstaller, + WorkflowReporter& reporter); + + // Download method just checks installer signature hash if signature hash + // is provided in the manifest. Otherwise, Download will download the whole + // installer to local temp folder. + void Download() override; + + void Install() override; + + protected: + // If use streaming install vs download install. + bool m_useStreaming = true; + + virtual std::future<void> ExecuteInstallerAsync(const winrt::Windows::Foundation::Uri& uri); + }; +}+ \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" +#include "Common.h" +#include "ShellExecuteInstallerHandler.h" + +using namespace AppInstaller::Utility; +using namespace AppInstaller::Manifest; + +namespace AppInstaller::Workflow +{ + ShellExecuteInstallerHandler::ShellExecuteInstallerHandler( + const Manifest::ManifestInstaller& manifestInstaller, + WorkflowReporter& reporter) : + InstallerHandlerBase(manifestInstaller, reporter) + { + // Todo: add support for other installer types. + // This Installer Handler should support Inno, Wix, Nullsoft, Msi and Exe. + if (manifestInstaller.InstallerType != ManifestInstaller::InstallerTypeEnum::Exe) + { + THROW_HR_MSG(E_UNEXPECTED, "Installer type not supported."); + } + } + + void ShellExecuteInstallerHandler::Install() + { + if (m_downloadedInstaller.empty()) + { + THROW_EXCEPTION_MSG(WorkflowException(APPINSTALLER_CLI_ERROR_INSTALLFLOW_FAILED), "Installer not downloaded yet"); + } + + m_reporterRef.ShowMsg(WorkflowReporter::Level::Info, "Installing package ..."); + + std::string installerArgs = GetInstallerArgs(); + AICLI_LOG(CLI, Info, << "Installer args: " << installerArgs); + + RenameDownloadedInstaller(); + std::future<DWORD> installTask = ExecuteInstallerAsync(m_downloadedInstaller, installerArgs); + + m_reporterRef.ShowIndefiniteProgress(true); + + installTask.wait(); + + m_reporterRef.ShowIndefiniteProgress(false); + + auto installResult = installTask.get(); + + if (installResult != 0) + { + m_reporterRef.ShowMsg(WorkflowReporter::Level::Error, "Install failed. Exit code: " + std::to_string(installResult)); + + THROW_EXCEPTION_MSG(WorkflowException(APPINSTALLER_CLI_ERROR_INSTALLFLOW_FAILED), + "Install failed. Installer task returned: %u", installResult); + } + + m_reporterRef.ShowMsg(WorkflowReporter::Level::Info, "Successfully installed!"); + } + + std::future<DWORD> ShellExecuteInstallerHandler::ExecuteInstallerAsync(const std::filesystem::path& filePath, const std::string& args) + { + AICLI_LOG(CLI, Info, << "Staring installer. Path: " << filePath); + return std::async(std::launch::async, [&filePath, &args] + { + SHELLEXECUTEINFOA execInfo = { 0 }; + execInfo.cbSize = sizeof(SHELLEXECUTEINFO); + execInfo.fMask = SEE_MASK_NOCLOSEPROCESS; + std::string filePathUTF8Str = Utility::ConvertToUTF8(filePath.c_str()); + execInfo.lpFile = filePathUTF8Str.c_str(); + execInfo.lpParameters = args.c_str(); + execInfo.nShow = SW_SHOW; + if (!ShellExecuteExA(&execInfo) || !execInfo.hProcess) + { + return GetLastError(); + } + + // Wait for installation to finish + WaitForSingleObject(execInfo.hProcess, INFINITE); + + // Get exe exit code + DWORD exitCode; + GetExitCodeProcess(execInfo.hProcess, &exitCode); + + CloseHandle(execInfo.hProcess); + + return exitCode; + }); + } + + std::string ShellExecuteInstallerHandler::GetInstallerArgs() + { + // Todo: Implement arg selection logic. + if (m_manifestInstallerRef.Switches.has_value()) + { + return m_manifestInstallerRef.Switches.value().Default; + } + + return ""; + } + + void ShellExecuteInstallerHandler::RenameDownloadedInstaller() + { + std::filesystem::path renamedDownloadedInstaller(m_downloadedInstaller); + + if (m_manifestInstallerRef.InstallerType == ManifestInstaller::InstallerTypeEnum::Exe) + { + renamedDownloadedInstaller += L".exe"; + } + + std::filesystem::rename(m_downloadedInstaller, renamedDownloadedInstaller); + + m_downloadedInstaller.assign(renamedDownloadedInstaller); + AICLI_LOG(CLI, Info, << "Successfully renamed downloaded installer. Path: " << m_downloadedInstaller ); + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.h b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.h @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once +#include "InstallerHandlerBase.h" + +namespace AppInstaller::Workflow +{ + // ShellExecuteInstallerHandler handles installers run through ShellExecute. + // Exe, Wix, Nullsoft, Msi and Inno should be handled by this installer handler. + class ShellExecuteInstallerHandler : public InstallerHandlerBase + { + public: + ShellExecuteInstallerHandler( + const Manifest::ManifestInstaller& manifestInstaller, + WorkflowReporter& reporter); + + // Install is done though invoking SheelExecute on downloaded installer. + void Install() override; + + protected: + std::future<DWORD> ExecuteInstallerAsync(const std::filesystem::path& filePath, const std::string& args); + std::string GetInstallerArgs(); + + // This method appends appropriate extension to the downloaded installer. + // ShellExecute uses file extension to launch the installer appropriately. + virtual void RenameDownloadedInstaller(); + }; +}+ \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/WorkflowReporter.cpp b/src/AppInstallerCLICore/Workflows/WorkflowReporter.cpp @@ -6,38 +6,13 @@ namespace AppInstaller::Workflow { - void DownloaderCallback::OnStarted() - { - out << "Starting package download ..." << std::endl; - } - - void DownloaderCallback::OnProgress(LONGLONG progress, LONGLONG downloadSize) - { - out << "\rDownloading " << progress << '/' << downloadSize; - - if (progress == downloadSize) - { - out << std::endl; - } - } - - void DownloaderCallback::OnCanceled() - { - out << "Package download canceled ..." << std::endl; - } - - void DownloaderCallback::OnCompleted() - { - out << "Package download completed ..." << std::endl; - } - void WorkflowReporter::ShowPackageInfo( const std::string& name, const std::string& version, const std::string& author, const std::string& description, const std::string& homepage, - const std::string& licenceUrl + const std::string& licenseUrl ) { out << "Name: " << name << std::endl; @@ -45,7 +20,7 @@ namespace AppInstaller::Workflow out << "Author: " << author << std::endl; out << "Description: " << description << std::endl; out << "Homepage: " << homepage << std::endl; - out << "Licence: " << licenceUrl << std::endl; + out << "License: " << licenseUrl << std::endl; } bool WorkflowReporter::PromptForBoolResponse(Level level, const std::string& msg) @@ -68,7 +43,7 @@ namespace AppInstaller::Workflow out << msg << std::endl; } - void WorkflowReporter::ShowIndefiniteSpinner(bool running) + void WorkflowReporter::ShowIndefiniteProgress(bool running) { if (running) { @@ -80,6 +55,11 @@ namespace AppInstaller::Workflow } } + void WorkflowReporter::ShowProgress(bool running, int progress) + { + m_progressBar.ShowProgress(running, progress); + } + void IndefiniteSpinner::ShowSpinner() { if (!m_spinnerJob.valid() && !m_spinnerRunning && !m_canceled) @@ -117,4 +97,28 @@ namespace AppInstaller::Workflow m_spinnerJob.wait(); } } + + void ProgressBar::ShowProgress(bool running, int progress) + { + if (running) + { + if (m_isVisible) + { + out << "\rProgress: " << progress; + } + else + { + out << "Progress: " << progress; + m_isVisible = true; + } + } + else + { + if (m_isVisible) + { + out << std::endl; + m_isVisible = false; + } + } + } } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/WorkflowReporter.h b/src/AppInstallerCLICore/Workflows/WorkflowReporter.h @@ -7,22 +7,7 @@ namespace AppInstaller::Workflow { - // This will be triggered by file downloader to get download progress - class DownloaderCallback : public AppInstaller::Utility::IDownloaderCallback - { - public: - DownloaderCallback(std::ostream& stream) : out(stream) {}; - - void OnStarted() override; - void OnProgress(LONGLONG progress, LONGLONG downloadSize) override; - void OnCanceled() override; - void OnCompleted() override; - - private: - std::ostream& out; - }; - - // Class to print the in progress spinner + // Class to print a indefinite spinner. class IndefiniteSpinner { public: @@ -40,6 +25,19 @@ namespace AppInstaller::Workflow void ShowSpinnerInternal(); }; + // Todo: Need to implement real progress bar. Only prints progress number now. + class ProgressBar + { + public: + ProgressBar(std::ostream& stream) : out(stream) {}; + + void ShowProgress(bool running, int progress); + + private: + std::atomic<bool> m_isVisible = false; + std::ostream& out; + }; + // WorkflowReporter should be the central place to show workflow status to user. // Todo: need to implement actual console output to show color, progress bar, etc class WorkflowReporter @@ -55,7 +53,7 @@ namespace AppInstaller::Workflow }; WorkflowReporter(std::ostream& outStream, std::istream& inStream) : - out(outStream), in(inStream), m_downloaderCallback(outStream), m_spinner(outStream) {}; + out(outStream), in(inStream), m_progressBar(outStream), m_spinner(outStream) {}; void ShowPackageInfo( const std::string& name, @@ -63,21 +61,25 @@ namespace AppInstaller::Workflow const std::string& author, const std::string& description, const std::string& homepage, - const std::string& licenceUrl); + const std::string& licenseUrl); bool PromptForBoolResponse(Level level, const std::string& msg); void ShowMsg(Level level, const std::string& msg); - // running: shows the spinner if set to true, stops the spinner if set to false - void ShowIndefiniteSpinner(bool running); + // Used to show definite progress. + // running: shows progress bar if set to true, dismisses progress bar if set to false + void ShowProgress(bool running, int progress); - DownloaderCallback& GetDownloaderCallback() { return m_downloaderCallback; } + // Used to show indefinite progress. Currently an indefinite spinner is the form of + // showing indefinite progress. + // running: shows indefinite progress if set to true, stops indefinite progress if set to false + void ShowIndefiniteProgress(bool running); private: std::ostream& out; std::istream& in; - DownloaderCallback m_downloaderCallback; IndefiniteSpinner m_spinner; + ProgressBar m_progressBar; }; } \ No newline at end of file diff --git a/src/AppInstallerCLICore/pch.h b/src/AppInstallerCLICore/pch.h @@ -7,6 +7,7 @@ #include <winrt/Windows.Foundation.h> #include <winrt/Windows.Foundation.Collections.h> +#include <winrt/Windows.Management.Deployment.h> #include <wil/result_macros.h> @@ -20,6 +21,9 @@ #include <yaml-cpp\yaml.h> +#include <wrl/client.h> +#include <AppxPackaging.h> + #include "AppInstallerLogging.h" #include "AppInstallerTelemetry.h" #include "AppInstallerStrings.h" @@ -29,3 +33,4 @@ #include "AppInstallerErrors.h" #include "Manifest/ManifestInstaller.h" #include "Manifest/Manifest.h" +#include "AppInstallerMsixInfo.h" diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -172,7 +172,13 @@ <CopyFileToFolders Include="TestData\BadManifest-MissingName.yml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> - <CopyFileToFolders Include="TestData\InstallFlowTest.yml"> + <CopyFileToFolders Include="TestData\InstallFlowTest_Exe.yml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\InstallFlowTest_Msix_DownloadFlow.yml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\InstallFlowTest_Msix_StreamingFlow.yml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> <CopyFileToFolders Include="TestData\InstallFlowTest_NoApplicableArchitecture.yml"> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -65,10 +65,16 @@ <CopyFileToFolders Include="TestData\GoodManifest.yml"> <Filter>TestData</Filter> </CopyFileToFolders> - <CopyFileToFolders Include="TestData\InstallFlowTest.yml"> + <CopyFileToFolders Include="TestData\InstallFlowTest_NoApplicableArchitecture.yml"> <Filter>TestData</Filter> </CopyFileToFolders> - <CopyFileToFolders Include="TestData\InstallFlowTest_NoApplicableArchitecture.yml"> + <CopyFileToFolders Include="TestData\InstallFlowTest_Exe.yml"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\InstallFlowTest_Msix_DownloadFlow.yml"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\InstallFlowTest_Msix_StreamingFlow.yml"> <Filter>TestData</Filter> </CopyFileToFolders> </ItemGroup> diff --git a/src/AppInstallerCLITests/Downloader.cpp b/src/AppInstallerCLITests/Downloader.cpp @@ -6,10 +6,11 @@ #include "AppInstallerSHA256.h" using namespace AppInstaller::Utility; +using namespace std::string_literals; TEST_CASE("DownloadValidFileAndVerifyHash", "[Downloader]") { - TestCommon::TempFile tempFile("downloader_test", ".test"); + TestCommon::TempFile tempFile("downloader_test"s, ".test"s); INFO("Using temporary file named: " << tempFile.GetPath()); // Todo: point to files from our repo when the repo goes public @@ -30,7 +31,7 @@ TEST_CASE("DownloadValidFileAndVerifyHash", "[Downloader]") TEST_CASE("DownloadValidFileAndCancel", "[Downloader]") { - TestCommon::TempFile tempFile("downloader_test", ".test"); + TestCommon::TempFile tempFile("downloader_test"s, ".test"s); INFO("Using temporary file named: " << tempFile.GetPath()); auto downloader = Downloader::StartDownloadAsync("https://aka.ms/win32-x64-user-stable", tempFile.GetPath(), true); @@ -52,7 +53,7 @@ TEST_CASE("DownloadValidFileAndCancel", "[Downloader]") TEST_CASE("DownloadUnreachableUrl", "[Downloader]") { - TestCommon::TempFile tempFile("downloader_test", ".test"); + TestCommon::TempFile tempFile("downloader_test"s, ".test"s); INFO("Using temporary file named: " << tempFile.GetPath()); auto downloader = Downloader::StartDownloadAsync("https://does_not_exist.com/", tempFile.GetPath(), true); diff --git a/src/AppInstallerCLITests/InstallFlow.cpp b/src/AppInstallerCLITests/InstallFlow.cpp @@ -4,13 +4,54 @@ #include "TestCommon.h" #include "Manifest/Manifest.h" #include "AppInstallerDownloader.h" +#include "AppInstallerStrings.h" #include "Workflows/InstallFlow.h" +#include "Workflows/ShellExecuteInstallerHandler.h" +#include "Workflows/MsixInstallerHandler.h" +using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::Management::Deployment; using namespace TestCommon; using namespace AppInstaller::Workflow; using namespace AppInstaller::Utility; using namespace AppInstaller::Manifest; +class MsixInstallerHandlerTest : public MsixInstallerHandler +{ +public: + MsixInstallerHandlerTest( + const ManifestInstaller& manifestInstaller, + WorkflowReporter& reporter) : MsixInstallerHandler(manifestInstaller, reporter) {}; + +protected: + + std::future<void> ExecuteInstallerAsync(const Uri& uri) override + { + std::ofstream file("TestMsixInstalled.txt", std::ofstream::out); + + file << AppInstaller::Utility::ConvertToUTF8(uri.ToString()); + + file.close(); + + co_return; + } +}; + +class ShellExecuteInstallerHandlerTest : public ShellExecuteInstallerHandler +{ +public: + ShellExecuteInstallerHandlerTest( + const ManifestInstaller& manifestInstaller, + WorkflowReporter& reporter) : ShellExecuteInstallerHandler(manifestInstaller, reporter) {}; + + void Download() override + { + this->m_downloadedInstaller = TestDataFile("AppInstallerTestExeInstaller.exe"); + } + + void RenameDownloadedInstaller() override {}; +}; + class InstallFlowTest : public InstallFlow { public: @@ -18,19 +59,25 @@ public: InstallFlow(manifest, outStream, inStream) {} protected: - void DownloadInstaller() override + std::unique_ptr<InstallerHandlerBase> GetInstallerHandler() override { - this->m_downloadedInstaller = TestDataFile("AppInstallerTestExeInstaller.exe"); + switch (m_selectedInstaller.InstallerType) + { + case ManifestInstaller::InstallerTypeEnum::Exe: + return std::make_unique<ShellExecuteInstallerHandlerTest>(m_selectedInstaller, m_reporter); + case ManifestInstaller::InstallerTypeEnum::Msix: + return std::make_unique<MsixInstallerHandlerTest>(m_selectedInstaller, m_reporter); + default: + THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); + } } }; -TEST_CASE("InstallFlowWithTestManifest", "[InstallFlow]") +TEST_CASE("ExeInstallFlowWithTestManifest", "[InstallFlow]") { - auto installResultPath = std::filesystem::current_path().append("TestExeInstalled.txt"); + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); - std::filesystem::remove(installResultPath); - - auto manifest = Manifest::CreateFromPath(TestDataFile("InstallFlowTest.yml")); + auto manifest = Manifest::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yml")); std::ostringstream installOutput; InstallFlowTest testFlow(manifest, installOutput, std::cin); @@ -38,8 +85,8 @@ TEST_CASE("InstallFlowWithTestManifest", "[InstallFlow]") INFO(installOutput.str()); // Verify Installer is called and parameters are passed in. - REQUIRE(std::filesystem::exists(installResultPath)); - std::ifstream installResultFile(installResultPath); + REQUIRE(std::filesystem::exists(installResultPath.GetPath())); + std::ifstream installResultFile(installResultPath.GetPath()); REQUIRE(installResultFile.is_open()); std::string installResultStr; std::getline(installResultFile, installResultStr); @@ -48,9 +95,7 @@ TEST_CASE("InstallFlowWithTestManifest", "[InstallFlow]") TEST_CASE("InstallFlowWithNonApplicableArchitecture", "[InstallFlow]") { - auto installResultPath = std::filesystem::current_path().append("TestExeInstalled.txt"); - - std::filesystem::remove(installResultPath); + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); auto manifest = Manifest::CreateFromPath(TestDataFile("InstallFlowTest_NoApplicableArchitecture.yml")); @@ -60,5 +105,47 @@ TEST_CASE("InstallFlowWithNonApplicableArchitecture", "[InstallFlow]") INFO(installOutput.str()); // Verify Installer is called and parameters are passed in. - REQUIRE(!std::filesystem::exists(installResultPath)); + REQUIRE(!std::filesystem::exists(installResultPath.GetPath())); +} + +TEST_CASE("MsixInstallFlow_DownloadFlow", "[InstallFlow]") +{ + TestCommon::TempFile installResultPath("TestMsixInstalled.txt"); + + // Todo: point to files from our repo when the repo goes public + auto manifest = Manifest::CreateFromPath(TestDataFile("InstallFlowTest_Msix_DownloadFlow.yml")); + + std::ostringstream installOutput; + InstallFlowTest testFlow(manifest, installOutput, std::cin); + testFlow.Install(); + INFO(installOutput.str()); + + // Verify Installer is called and a local file is used as package Uri. + REQUIRE(std::filesystem::exists(installResultPath.GetPath())); + std::ifstream installResultFile(installResultPath.GetPath()); + REQUIRE(installResultFile.is_open()); + std::string installResultStr; + std::getline(installResultFile, installResultStr); + REQUIRE(installResultStr.find("file://") != std::string::npos); +} + +TEST_CASE("MsixInstallFlow_StreamingFlow", "[InstallFlow]") +{ + TestCommon::TempFile installResultPath("TestMsixInstalled.txt"); + + // Todo: point to files from our repo when the repo goes public + auto manifest = Manifest::CreateFromPath(TestDataFile("InstallFlowTest_Msix_StreamingFlow.yml")); + + std::ostringstream installOutput; + InstallFlowTest testFlow(manifest, installOutput, std::cin); + testFlow.Install(); + INFO(installOutput.str()); + + // Verify Installer is called and a local file is used as package Uri. + REQUIRE(std::filesystem::exists(installResultPath.GetPath())); + std::ifstream installResultFile(installResultPath.GetPath()); + REQUIRE(installResultFile.is_open()); + std::string installResultStr; + std::getline(installResultFile, installResultStr); + REQUIRE(installResultStr.find("https://") != std::string::npos); } \ No newline at end of file diff --git a/src/AppInstallerCLITests/SQLiteIndex.cpp b/src/AppInstallerCLITests/SQLiteIndex.cpp @@ -18,6 +18,7 @@ #include <Microsoft/Schema/1_0/ProtocolsTable.h> #include <Microsoft/Schema/1_0/ExtensionsTable.h> +using namespace std::string_literals; using namespace TestCommon; using namespace AppInstaller::Manifest; using namespace AppInstaller::Repository::Microsoft; @@ -25,7 +26,7 @@ using namespace AppInstaller::Repository::SQLite; TEST_CASE("SQLiteIndexCreateLatestAndReopen", "[sqliteindex]") { - TempFile tempFile{ "repolibtest_tempdb", ".db" }; + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; INFO("Using temporary file named: " << tempFile.GetPath()); Schema::Version versionCreated; @@ -63,7 +64,7 @@ TEST_CASE("SQLiteIndexCreateLatestAndReopen", "[sqliteindex]") TEST_CASE("SQLiteIndexCreateAndAddManifest", "[sqliteindex]") { - TempFile tempFile{ "repolibtest_tempdb", ".db" }; + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; INFO("Using temporary file named: " << tempFile.GetPath()); SQLiteIndex index = SQLiteIndex::CreateNew(tempFile, Schema::Version::Latest()); @@ -84,7 +85,7 @@ TEST_CASE("SQLiteIndexCreateAndAddManifest", "[sqliteindex]") TEST_CASE("SQLiteIndexCreateAndAddManifestFile", "[sqliteindex]") { - TempFile tempFile{ "repolibtest_tempdb", ".db" }; + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; INFO("Using temporary file named: " << tempFile.GetPath()); SQLiteIndex index = SQLiteIndex::CreateNew(tempFile, Schema::Version::Latest()); @@ -110,7 +111,7 @@ TEST_CASE("SQLiteIndex_RemoveManifestFile_NotPresent", "[sqliteindex]") TEST_CASE("SQLiteIndex_RemoveManifest", "[sqliteindex]") { - TempFile tempFile{ "repolibtest_tempdb", ".db" }; + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; INFO("Using temporary file named: " << tempFile.GetPath()); std::string manifest1Path = "test/id/test.id-1.0.0.yml"; @@ -190,7 +191,7 @@ TEST_CASE("SQLiteIndex_RemoveManifest", "[sqliteindex]") TEST_CASE("SQLiteIndex_RemoveManifestFile", "[sqliteindex]") { - TempFile tempFile{ "repolibtest_tempdb", ".db" }; + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; INFO("Using temporary file named: " << tempFile.GetPath()); { @@ -237,7 +238,7 @@ TEST_CASE("PathPartTable_EnsurePathExists_Negative_Paths", "[sqliteindex][V1_0]" TEST_CASE("PathPartTable_EnsurePathExists", "[sqliteindex][V1_0]") { - TempFile tempFile{ "repolibtest_tempdb", ".db" }; + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; INFO("Using temporary file named: " << tempFile.GetPath()); // Create the index diff --git a/src/AppInstallerCLITests/SQLiteWrapper.cpp b/src/AppInstallerCLITests/SQLiteWrapper.cpp @@ -6,6 +6,7 @@ #include <SQLiteStatementBuilder.h> using namespace AppInstaller::Repository::SQLite; +using namespace std::string_literals; static const char* s_firstColumn = "first"; static const char* s_secondColumn = "second"; @@ -104,7 +105,7 @@ TEST_CASE("SQLiteWrapperMemoryCreate", "[sqlitewrapper]") TEST_CASE("SQLiteWrapperFileCreateAndReopen", "[sqlitewrapper]") { - TestCommon::TempFile tempFile{ "repolibtest_tempdb", ".db" }; + TestCommon::TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; INFO("Using temporary file named: " << tempFile.GetPath()); int firstVal = 1; @@ -382,7 +383,7 @@ TEST_CASE("SQLBuilder_InsertValueBinding", "[sqlbuilder]") { char const* const columns[] = { "a", "b", "c", "d", "e", "f" }; - TestCommon::TempFile tempFile{ "repolibtest_tempdb", ".db" }; + TestCommon::TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; INFO("Using temporary file named: " << tempFile.GetPath()); Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::Create); diff --git a/src/AppInstallerCLITests/TestCommon.cpp b/src/AppInstallerCLITests/TestCommon.cpp @@ -19,16 +19,13 @@ namespace TestCommon return randStart++; } - inline std::string GetTempFilePath(const std::string& baseName, const std::string& baseExt) + inline std::filesystem::path GetTempFilePath(const std::string& baseName, const std::string& baseExt) { - char tempPath[MAX_PATH]{}; - REQUIRE(GetTempPathA(MAX_PATH, tempPath) != 0); + std::filesystem::path tempFilePath = std::filesystem::temp_directory_path(); - srand(static_cast<unsigned int>(time(NULL))); - std::stringstream tempFileName; - tempFileName << tempPath << '\\' << baseName << getRand() << baseExt; + tempFilePath /= baseName + std::to_string(getRand()) + baseExt; - return tempFileName.str(); + return tempFilePath; } static bool s_TempFileDestructorKeepsFile{}; @@ -41,7 +38,16 @@ namespace TestCommon _filepath = GetTempFilePath(baseName, baseExt); if (deleteFileOnConstruction) { - DeleteFileA(_filepath.c_str()); + std::filesystem::remove(_filepath); + } + } + + TempFile::TempFile(const std::filesystem::path& filePath, bool deleteFileOnConstruction) + { + _filepath = filePath; + if (deleteFileOnConstruction) + { + std::filesystem::remove(_filepath); } } @@ -49,7 +55,7 @@ namespace TestCommon { if (!s_TempFileDestructorKeepsFile) { - DeleteFileA(_filepath.c_str()); + std::filesystem::remove(_filepath); } } diff --git a/src/AppInstallerCLITests/TestCommon.h b/src/AppInstallerCLITests/TestCommon.h @@ -2,8 +2,6 @@ // Licensed under the MIT License. #pragma once #include "pch.h" -#include <filesystem> -#include <string> #define SQLITE_MEMORY_DB_CONNECTION_TARGET ":memory:" @@ -15,6 +13,7 @@ namespace TestCommon struct TempFile { TempFile(const std::string& baseName, const std::string& baseExt, bool deleteFileOnConstruction = true); + TempFile(const std::filesystem::path& filePath, bool deleteFileOnConstruction = true); TempFile(const TempFile&) = delete; TempFile& operator=(const TempFile&) = delete; @@ -24,13 +23,13 @@ namespace TestCommon ~TempFile(); - const std::string& GetPath() const { return _filepath; } - operator const std::string& () const { return _filepath; } + const std::filesystem::path& GetPath() const { return _filepath; } + operator const std::string () const { return _filepath.u8string(); } static void SetDestructorBehavior(bool keepFilesOnDestruction); private: - std::string _filepath; + std::filesystem::path _filepath; }; // Use this to find a test data file when testing. diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest.yml b/src/AppInstallerCLITests/TestData/InstallFlowTest_Exe.yml diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_Msix_DownloadFlow.yml b/src/AppInstallerCLITests/TestData/InstallFlowTest_Msix_DownloadFlow.yml @@ -0,0 +1,10 @@ +Id: AppInstallerCliTest.TestMsixInstaller +Version: 1.0.0.0 +Name: AppInstaller Test MSIX Installer +Publisher: Microsoft Corporation +AppMoniker: AICLITestMsix +Installers: + - Arch: x64 + Url: https://github.com/microsoft/msix-packaging/blob/master/src/test/testData/unpack/TestAppxPackage_x64.appx?raw=true + InstallerType: msix + Sha256: 6a2d3683fa19bf00e58e07d1313d20a5f5735ebbd6a999d33381d28740ee07ea+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_Msix_StreamingFlow.yml b/src/AppInstallerCLITests/TestData/InstallFlowTest_Msix_StreamingFlow.yml @@ -0,0 +1,11 @@ +Id: AppInstallerCliTest.TestMsixInstaller +Version: 1.0.0.0 +Name: AppInstaller Test MSIX Installer +Publisher: Microsoft Corporation +AppMoniker: AICLITestMsix +Installers: + - Arch: x64 + Url: https://github.com/microsoft/msix-packaging/blob/master/src/test/testData/unpack/TestAppxPackage_x64.appx?raw=true + InstallerType: msix + Sha256: 6a2d3683fa19bf00e58e07d1313d20a5f5735ebbd6a999d33381d28740ee07ea + SignatureSha256: 138781c3e6f635240353f3d14d1d57bdcb89413e49be63b375e6a5d7b93b0d07+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp @@ -48,7 +48,7 @@ TEST_CASE("ReadGoodManifestAndVerifyContents", "[PackageManifestHelper]") REQUIRE(manifest.Commands == MultiValue{ "makemsix", "makeappx" }); REQUIRE(manifest.Protocols == MultiValue{ "protocol1", "protocol2" }); REQUIRE(manifest.FileExtensions == MultiValue{ "appx", "appxbundle", "msix", "msixbundle" }); - REQUIRE(manifest.InstallerType == "Zip"); + REQUIRE(manifest.InstallerType == ManifestInstaller::InstallerTypeEnum::Zip); // default switches REQUIRE(manifest.Switches.has_value()); @@ -64,7 +64,7 @@ TEST_CASE("ReadGoodManifestAndVerifyContents", "[PackageManifestHelper]") REQUIRE(installer1.Url == "https://rubengustorage.blob.core.windows.net/publiccontainer/msixsdkx86.zip"); REQUIRE(installer1.Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); REQUIRE(installer1.Language == "en-US"); - REQUIRE(installer1.InstallerType == "Zip"); + REQUIRE(installer1.InstallerType == ManifestInstaller::InstallerTypeEnum::Zip); REQUIRE(installer1.Scope == "user"); REQUIRE(installer1.Switches.has_value()); @@ -78,7 +78,7 @@ TEST_CASE("ReadGoodManifestAndVerifyContents", "[PackageManifestHelper]") REQUIRE(installer2.Url == "https://rubengustorage.blob.core.windows.net/publiccontainer/msixsdkx64.zip"); REQUIRE(installer2.Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF0000")); REQUIRE(installer2.Language == "en-US"); - REQUIRE(installer2.InstallerType == "Zip"); + REQUIRE(installer2.InstallerType == ManifestInstaller::InstallerTypeEnum::Zip); REQUIRE(installer2.Scope == "user"); // Installer2 does not declare switches, it inherits switches from package default. diff --git a/src/AppInstallerCLITests/pch.h b/src/AppInstallerCLITests/pch.h @@ -9,6 +9,7 @@ #include <winrt/Windows.Foundation.h> #include <winrt/Windows.Foundation.Collections.h> +#include <winrt/Windows.Management.Deployment.h> #include <wil/result_macros.h> @@ -19,5 +20,6 @@ #include <sstream> #include <utility> #include <vector> +#include <string> #include <yaml-cpp/yaml.h> \ No newline at end of file diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -115,7 +115,7 @@ <PrecompiledHeaderOutputFile>$(IntDir)pch.pch</PrecompiledHeaderOutputFile> <PreprocessorDefinitions>_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions> <WarningLevel>Level4</WarningLevel> - <AdditionalOptions>%(AdditionalOptions) /permissive- /bigobj</AdditionalOptions> + <AdditionalOptions>%(AdditionalOptions) /permissive-</AdditionalOptions> </ClCompile> </ItemDefinitionGroup> <ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'"> @@ -166,10 +166,14 @@ <ItemGroup> <ClInclude Include="DateTime.h" /> <ClInclude Include="FileLogger.h" /> + <ClInclude Include="HttpStream\HttpClientWrapper.h" /> + <ClInclude Include="HttpStream\HttpLocalCache.h" /> + <ClInclude Include="HttpStream\HttpRandomAccessStream.h" /> <ClInclude Include="pch.h" /> <ClInclude Include="Public\AppInstallerDownloader.h" /> <ClInclude Include="Public\AppInstallerErrors.h" /> <ClInclude Include="Public\AppInstallerLanguageUtilities.h" /> + <ClInclude Include="Public\AppInstallerMsixInfo.h" /> <ClInclude Include="Public\AppInstallerRuntime.h" /> <ClInclude Include="Public\AppInstallerSHA256.h" /> <ClInclude Include="Public\AppInstallerStrings.h" /> @@ -186,6 +190,10 @@ <ClCompile Include="DateTime.cpp" /> <ClCompile Include="Downloader.cpp" /> <ClCompile Include="FileLogger.cpp" /> + <ClCompile Include="HttpStream\HttpClientWrapper.cpp" /> + <ClCompile Include="HttpStream\HttpLocalCache.cpp" /> + <ClCompile Include="HttpStream\HttpRandomAccessStream.cpp" /> + <ClCompile Include="MsixInfo.cpp" /> <ClCompile Include="Runtime.cpp" /> <ClCompile Include="pch.cpp"> <PrecompiledHeader>Create</PrecompiledHeader> diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -19,6 +19,9 @@ <Filter Include="Public"> <UniqueIdentifier>{5cdf3fa3-e657-4d84-81bb-f740aa476143}</UniqueIdentifier> </Filter> + <Filter Include="HttpStream"> + <UniqueIdentifier>{a9c14af9-ca74-4945-a19c-9e99df23a5ae}</UniqueIdentifier> + </Filter> </ItemGroup> <ItemGroup> <ClInclude Include="pch.h"> @@ -66,6 +69,18 @@ <ClInclude Include="Public\AppInstallerLanguageUtilities.h"> <Filter>Public</Filter> </ClInclude> + <ClInclude Include="HttpStream\HttpClientWrapper.h"> + <Filter>HttpStream</Filter> + </ClInclude> + <ClInclude Include="HttpStream\HttpLocalCache.h"> + <Filter>HttpStream</Filter> + </ClInclude> + <ClInclude Include="HttpStream\HttpRandomAccessStream.h"> + <Filter>HttpStream</Filter> + </ClInclude> + <ClInclude Include="Public\AppInstallerMsixInfo.h"> + <Filter>Public</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -101,6 +116,18 @@ <ClCompile Include="Architecture.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="HttpStream\HttpClientWrapper.cpp"> + <Filter>HttpStream</Filter> + </ClCompile> + <ClCompile Include="HttpStream\HttpLocalCache.cpp"> + <Filter>HttpStream</Filter> + </ClCompile> + <ClCompile Include="HttpStream\HttpRandomAccessStream.cpp"> + <Filter>HttpStream</Filter> + </ClCompile> + <ClCompile Include="MsixInfo.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCommonCore/AppInstallerStrings.cpp b/src/AppInstallerCommonCore/AppInstallerStrings.cpp @@ -42,4 +42,25 @@ namespace AppInstaller::Utility [](unsigned char c) { return static_cast<char>(std::tolower(c)); }); return result; } + + std::wstring ToLower(const std::wstring& in) + { + std::wstring result(in); + std::transform(result.begin(), result.end(), result.begin(), + [](unsigned short c) { return std::towlower(c); }); + return result; + } + + bool IsEmptyOrWhitespace(std::wstring_view str) + { + if (str.empty()) + { + return true; + } + + std::wstring inputAsWStr(str.data()); + bool nonWhitespaceNotFound = inputAsWStr.find_last_not_of(L" \t\v\f") == std::wstring::npos; + + return nonWhitespaceNotFound; + } } diff --git a/src/AppInstallerCommonCore/HttpStream/HttpClientWrapper.cpp b/src/AppInstallerCommonCore/HttpStream/HttpClientWrapper.cpp @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" +#include "Public/AppInstallerStrings.h" +#include "HttpClientWrapper.h" + +using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::Security::Cryptography; +using namespace winrt::Windows::Storage; +using namespace winrt::Windows::Storage::Streams; +using namespace winrt::Windows::Web::Http; +using namespace winrt::Windows::Web::Http::Headers; +using namespace winrt::Windows::Web::Http::Filters; + +// Note: this class is used by the HttpRandomAccessStream which is passed to the AppxPackaging COM API +// All exceptions thrown accross dll boundaries should be WinRT exception not custom exceptions. +// The HRESULTs will be mapped to UI error code by the appropriate component +namespace AppInstaller::Utility::HttpStream +{ + std::future<std::shared_ptr<HttpClientWrapper>> HttpClientWrapper::CreateAsync(const Uri& uri) + { + std::shared_ptr<HttpClientWrapper> instance = std::make_shared<HttpClientWrapper>(); + + // Use an HTTP filter to disable the default caching behavior and use the Most Recent caching behavior instead + // so we don't use a stale cached resource. Note: this wrapper object is used in the custom HTTP stream implementation + // so this affects the parsing of HTTP-based packages/bundles. + HttpBaseProtocolFilter filter; + filter.CacheControl().ReadBehavior(HttpCacheReadBehavior::MostRecent); + instance->m_httpClient = HttpClient(filter); + instance->m_requestUri = uri; + + instance->m_httpClient.DefaultRequestHeaders().Connection().Clear(); + instance->m_httpClient.DefaultRequestHeaders().Append(L"Connection", L"Keep-Alive"); + + co_await instance->PopulateInfoAsync(); + + co_return instance; + } + + // this function will issue a HEAD request to determine the size of the file and the redirect URI + std::future<void> HttpClientWrapper::PopulateInfoAsync() + { + HttpRequestMessage request(HttpMethod::Head(), m_requestUri); + + HttpResponseMessage response = co_await m_httpClient.SendRequestAsync(request, HttpCompletionOption::ResponseHeadersRead); + + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_NO_RANGES_PROCESSED), response.StatusCode() != HttpStatusCode::Ok); + + // Get the length from the response + if (response.Content().Headers().HasKey(L"Content-Length")) + { + std::wstring contentLength(response.Content().Headers().Lookup(L"Content-Length")); + m_sizeInBytes = std::stoll(contentLength); + } + else + { + m_sizeInBytes = 0; + } + + // Get the extension from the redirect URI + m_redirectUri = response.RequestMessage().RequestUri(); + + m_contentType = response.Content().Headers().HasKey(L"Content-Type") ? + response.Content().Headers().Lookup(L"Content-Type") + : L""; + + // If the size wasn't resolved try with a GET 0-0 request + if (m_sizeInBytes == 0) + { + co_await SendHttpRequestAsync(0, 1); + } + } + + std::future<IBuffer> HttpClientWrapper::SendHttpRequestAsync( + _In_ ULONG64 startPosition, + _In_ UINT32 requestedSizeInBytes) + { + unsigned long long endPosition = 0; + + winrt::check_hresult(ULong64Add(startPosition, requestedSizeInBytes, &endPosition)); + + // Subtracting one should be safe, as the consumer of the stream should not request + // an empty range, so this number can't go negative. + endPosition -= 1; + + std::wstring rangeHeaderValue = L"bytes=" + std::to_wstring(startPosition) + L"-" + std::to_wstring(endPosition); + + HttpRequestMessage request(HttpMethod::Get(), m_requestUri); + request.Headers().Append(L"Range", rangeHeaderValue); + + if (!Utility::IsEmptyOrWhitespace(m_etagHeader)) + { + request.Headers().Append(L"If-Match", m_etagHeader); + } + + if (!Utility::IsEmptyOrWhitespace(m_lastModifiedHeader)) + { + request.Headers().Append(L"If-Unmodified-Since", m_lastModifiedHeader); + } + + HttpResponseMessage response = co_await m_httpClient.SendRequestAsync(request, HttpCompletionOption::ResponseHeadersRead); + HttpContentHeaderCollection contentHeaders = response.Content().Headers(); + + if (response.StatusCode() != HttpStatusCode::PartialContent && startPosition != 0) + { + // throw HRESULT used for range-request error + THROW_HR(HRESULT_FROM_WIN32(ERROR_NO_RANGES_PROCESSED)); + } + + if (response.Headers().HasKey(L"Accept-Ranges") && + Utility::ToLower(std::wstring(response.Headers().Lookup(L"Accept-Ranges"))) == L"none") + { + // throw HRESULT used for range-request error + THROW_HR(HRESULT_FROM_WIN32(ERROR_NO_RANGES_PROCESSED)); + } + + if (Utility::IsEmptyOrWhitespace(m_etagHeader) && response.Headers().HasKey(L"ETag")) + { + m_etagHeader = response.Headers().Lookup(L"ETag"); + } + + if (Utility::IsEmptyOrWhitespace(m_lastModifiedHeader) && contentHeaders.HasKey(L"Last-Modified")) + { + m_lastModifiedHeader = contentHeaders.Lookup(L"Last-Modified"); + } + + // If we don't know the size, parse it from the Content-Range field. + if (m_sizeInBytes == 0 && contentHeaders.HasKey(L"Content-Range")) + { + // format: a-b/x where x is either a number or * + std::wstring contentRange(contentHeaders.Lookup(L"Content-Range")); + std::wstring length = contentRange.substr(contentRange.find(L"/") + 1); + m_sizeInBytes = (length == L"*") ? 0 : std::stoll(length); + } + + co_return co_await response.Content().ReadAsBufferAsync(); + } + + std::future<IBuffer> HttpClientWrapper::DownloadRangeAsync( + const ULONG64 startPosition, + const UINT32 requestedSizeInBytes, + const InputStreamOptions& options) + { + std::vector<byte> byteArray(requestedSizeInBytes); + IBuffer buffer = CryptographicBuffer::CreateFromByteArray(byteArray); + + co_return co_await SendHttpRequestAsync(startPosition, requestedSizeInBytes); + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/HttpStream/HttpClientWrapper.h b/src/AppInstallerCommonCore/HttpStream/HttpClientWrapper.h @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once +#include "pch.h" + +namespace AppInstaller::Utility::HttpStream +{ + // Wrapper around HTTP client. When created, an object of this class will send a HTTP + // head request to determine the size of the data source. + class HttpClientWrapper + { + public: + static std::future<std::shared_ptr<HttpClientWrapper>> CreateAsync(const winrt::Windows::Foundation::Uri& uri); + + std::future<winrt::Windows::Storage::Streams::IBuffer> DownloadRangeAsync( + const ULONG64 startPosition, + const UINT32 requestedSizeInBytes, + const winrt::Windows::Storage::Streams::InputStreamOptions& options); + + unsigned long long GetFullFileSize() + { + return m_sizeInBytes; + } + + winrt::Windows::Foundation::Uri GetRedirectUri() + { + return m_redirectUri; + } + + std::wstring GetContentType() + { + return m_contentType; + } + + private: + winrt::Windows::Web::Http::HttpClient m_httpClient; + winrt::Windows::Foundation::Uri m_requestUri = nullptr; + winrt::Windows::Foundation::Uri m_redirectUri = nullptr; + std::wstring m_contentType; + unsigned long long m_sizeInBytes; + std::wstring m_etagHeader; + std::wstring m_lastModifiedHeader; + + std::future<void> PopulateInfoAsync(); + + std::future<winrt::Windows::Storage::Streams::IBuffer> SendHttpRequestAsync( + _In_ ULONG64 startPosition, + _In_ UINT32 requestedSizeInBytes); + }; +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/HttpStream/HttpLocalCache.cpp b/src/AppInstallerCommonCore/HttpStream/HttpLocalCache.cpp @@ -0,0 +1,243 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" +#include "HttpLocalCache.h" + +using namespace Windows::Storage::Streams; +using namespace winrt::Windows::Storage::Streams; +using namespace winrt::Windows::Security::Cryptography; + +// Note: this class is used by the HttpRandomAccessStream which is passed to the AppxPackaging COM API +// All exceptions thrown accross dll boundaries should be WinRT exception not custom exceptions. +// The HRESULTs will be mapped to UI error code by the appropriate component +namespace AppInstaller::Utility::HttpStream +{ + std::future<IBuffer> HttpLocalCache::ReadFromCacheAndDownloadIfNecessaryAsync( + const ULONG64 requestedPosition, + const UINT32 requestedSize, + HttpClientWrapper* httpClientWrapper, + InputStreamOptions httpInputStreamOptions) + { + // Increment cache access counter user for implementing LRU replacement + m_accessCounter++; + + // Find all the pages for the given request, and the pages that are missing + std::vector<ULONG64> allPages; + std::vector<ULONG64> unsatisfiablePages; + FindCachePages(requestedPosition, requestedSize, allPages, unsatisfiablePages); + + // download the missing pages + co_await DownloadAndSaveToCacheAysnc( + unsatisfiablePages, + httpClientWrapper, + httpInputStreamOptions); + + // At this point, everything should be in the cache + IBuffer constructedBuffer = {}; + + for (UINT32 i = 0; i < allPages.size(); i++) + { + UINT64 pageOffset = allPages[i]; + IBuffer cachedPageBuffer = ReadPageFromCache(pageOffset); + constructedBuffer = ConcatenateBuffers(constructedBuffer, cachedPageBuffer); + } + + // trim buffer to match requested range + IBuffer requestedBuffer = TrimBufferToSatisfyRequest( + constructedBuffer, + requestedPosition, + requestedSize, + allPages); + + VacateStaleEntriesFromCache(); + + co_return requestedBuffer; + } + + void HttpLocalCache::FindCachePages( + ULONG64 requestedPosition, + UINT32 requestedSize, + std::vector<ULONG64>& allPages, + std::vector<ULONG64>& unsatisfiablePages) + { + ULONG64 requestedEndPosition; + ULONG64 currentPageOffset; + winrt::check_hresult(ULong64Add(requestedPosition, requestedSize, &requestedEndPosition)); + winrt::check_hresult(ULong64Mult((requestedPosition / PAGE_SIZE), PAGE_SIZE, ¤tPageOffset)); + + // There's always at least one page for the range + do + { + allPages.push_back(currentPageOffset); + + if (m_localCache.find(currentPageOffset) == m_localCache.end()) + { + unsatisfiablePages.push_back(currentPageOffset); + } + + winrt::check_hresult(ULong64Add(currentPageOffset, PAGE_SIZE, ¤tPageOffset)); + + } while (currentPageOffset < requestedEndPosition); + } + + // Breaks the provided buffer into smaller buffers and saves them to the cache at the corresponding + // page offset position, starting at firstPageOffset. The smaller buffers are all PAGE_SIZE bytes, + // except for the one corresponding to the last page in the file + void HttpLocalCache::SaveBufferToCache(const IBuffer& buffer, const ULONG64 firstPageOffset) + { + UINT32 remainingBufferSize = buffer.Length(); + UINT32 currentBufferIndex = 0; + ULONG64 currentPageOffset = firstPageOffset; + + while (remainingBufferSize > 0) + { + // Extract the sub-buffer + UINT32 currentPageSize = std::min(remainingBufferSize, PAGE_SIZE); + IBuffer currentPageBuffer = CreateTrimmedBuffer(buffer, currentBufferIndex, currentPageSize); + + // Add it to the cache + CachedPage currentPage; + currentPage.lastAccessCounter = m_accessCounter; + currentPage.buffer = currentPageBuffer; + m_localCache[currentPageOffset] = currentPage; + + // update loop vars + winrt::check_hresult(UInt32Sub(remainingBufferSize, currentPageSize, &remainingBufferSize)); + winrt::check_hresult(UInt32Add(currentBufferIndex, currentPageSize, ¤tBufferIndex)); + winrt::check_hresult(ULong64Add(currentPageOffset, PAGE_SIZE, ¤tPageOffset)); + } + } + + IBuffer HttpLocalCache::ReadPageFromCache(const ULONG64 pageOffset) + { + if (!(m_localCache.find(pageOffset) != m_localCache.end())) + { + THROW_HR(E_INVALIDARG); + } + + CachedPage& page = m_localCache[pageOffset]; + page.lastAccessCounter = m_accessCounter; + + return page.buffer; + } + + // Trims a buffer that was constructed (by fetching pages from cache and downloading missing pages) + // in order to satisfy a request and return the exact buffer the consumer asked for. + IBuffer HttpLocalCache::TrimBufferToSatisfyRequest( + const IBuffer& constructedBuffer, + const ULONG64 requestedPosition, + const UINT32 requestedSize, + const std::vector<ULONG64> allPages) + { + ULONG64 fullBufferStartOffset = allPages[0]; + + ULONG64 trimmedBufferStartRelativeIndex; + winrt::check_hresult(ULong64Sub(requestedPosition, fullBufferStartOffset, &trimmedBufferStartRelativeIndex)); + + IBuffer requestedBuffer = CreateTrimmedBuffer( + constructedBuffer, + (UINT32)trimmedBufferStartRelativeIndex, // Conversion is safe as buffer size is a UINT32. + requestedSize); + + return requestedBuffer; + } + + // Downloads a chunk of the file, saves it to the cache, and returns the corresponding buffer + // If the requested size is 0, this method returns an empty buffer without making HTTP calls + std::future<void> HttpLocalCache::DownloadAndSaveToCacheAysnc( + const std::vector<ULONG64> unsatisfiablePages, + HttpClientWrapper* httpClientWrapper, + InputStreamOptions httpInputStreamOptions) + { + // Determine the download job + // To make things easy, we will download the contiguous range that includes all the unsatisfiable ranges. + // Note that in theory, this may include cached pages. However, this situation is rarely expected to happen, + // if at all. The package reader usually reads things in chunks of 64 KB or less, so, we should expect to + // always have up to two statisfiable and unsatisfiable pages in total. + UINT64 fileSize = httpClientWrapper->GetFullFileSize(); + ULONG64 downloadJobStartPosition = 0U; + ULONG64 downloadJobEndPosition = 0U; + ULONG64 downloadJobSize = 0U; + if (unsatisfiablePages.size() > 0U) + { + downloadJobStartPosition = unsatisfiablePages[0]; + ULONG64 lastUnsatisfiableJob = unsatisfiablePages[unsatisfiablePages.size() - 1]; + winrt::check_hresult(ULong64Add(lastUnsatisfiableJob, PAGE_SIZE, &downloadJobEndPosition)); + + // make sure to not overflow file size + downloadJobEndPosition = std::min(downloadJobEndPosition, fileSize); + winrt::check_hresult(ULong64Sub(downloadJobEndPosition, downloadJobStartPosition, &downloadJobSize)); + } + + if (downloadJobSize != 0U) + { + // start download job + IBuffer downloadedBuffer = co_await httpClientWrapper->DownloadRangeAsync( + downloadJobStartPosition, + (UINT32)downloadJobSize, + httpInputStreamOptions); + + SaveBufferToCache(downloadedBuffer, downloadJobStartPosition); + } + } + + void HttpLocalCache::VacateStaleEntriesFromCache() + { + // Copy page offsets into vector and sort by the access counter + std::vector<std::pair<UINT64, int>> orderedPageOffsets; + for (auto pageIter = m_localCache.begin(); pageIter != m_localCache.end(); pageIter++) + { + orderedPageOffsets.push_back(std::pair<UINT64, int>(pageIter->first, pageIter->second.lastAccessCounter)); + } + + // Compare function to sort by access counter + auto cmp = [](std::pair<UINT64, int> const & a, std::pair<UINT64, int> const & b) + { + return a.second != b.second ? a.second < b.second : a.first < b.first; + }; + + std::sort(orderedPageOffsets.begin(), orderedPageOffsets.end(), cmp); + + for (auto pageIter = orderedPageOffsets.begin(); pageIter != orderedPageOffsets.end(); pageIter++) + { + if (m_localCache.size() > MAX_PAGES) + { + m_localCache.erase(pageIter->first); + } + else + { + break; + } + } + } + + IBuffer HttpLocalCache::CreateTrimmedBuffer( + const IBuffer& originalBuffer, + UINT32 trimStartIndex, + UINT32 size) + { + originalBuffer.as<::IInspectable>(); + + // Get the byte array from the IBuffer object + Microsoft::WRL::ComPtr<IBufferByteAccess> bufferByteAccess; + ::IInspectable* bufferAbi = (::IInspectable*)winrt::get_abi(originalBuffer); + bufferAbi->QueryInterface(IID_PPV_ARGS(&bufferByteAccess)); + byte* byteBuffer = nullptr; + bufferByteAccess->Buffer(&byteBuffer); + + // Create the array of bytes holding the trimmed bytes + IBuffer trimmedBuffer = CryptographicBuffer::CreateFromByteArray( + { byteBuffer + trimStartIndex, byteBuffer + trimStartIndex + size }); + + return trimmedBuffer; + } + + IBuffer HttpLocalCache::ConcatenateBuffers(const IBuffer& buffer1, const IBuffer& buffer2) + { + DataWriter writer; + writer.WriteBuffer(buffer1); + writer.WriteBuffer(buffer2); + return writer.DetachBuffer(); + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/HttpStream/HttpLocalCache.h b/src/AppInstallerCommonCore/HttpStream/HttpLocalCache.h @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once +#include "pch.h" +#include "HttpClientWrapper.h" + +namespace AppInstaller::Utility::HttpStream +{ + // Represents an entry in the cache. + struct CachedPage + { + int lastAccessCounter; + winrt::Windows::Storage::Streams::IBuffer buffer; + }; + + // A cache used internally by the custom HttpRandomAccessStream to reduce round-trips + class HttpLocalCache + { + public: + const UINT32 PAGE_SIZE = 2 << 16; // each entry in the cache is 64 KB + const UINT32 MAX_PAGES = 200; // cache size capped at 12.5 MB (200 * 64KB) + + // Returns a buffer matching the requested range by reading the parts of the range that are cached + // and downloading the rest using the provided httpClientWrapper object + std::future<winrt::Windows::Storage::Streams::IBuffer> ReadFromCacheAndDownloadIfNecessaryAsync( + const ULONG64 requestedPosition, + const UINT32 requestedSize, + HttpClientWrapper* httpClientWrapper, + winrt::Windows::Storage::Streams::InputStreamOptions httpInputStreamOptions); + + private: + std::map<ULONG64, CachedPage> m_localCache; + UINT32 m_accessCounter = 0U; + + // Returns a vector of all pages corresponding to a range, and another (subset) + // vector of the pages missing from the cache. + void FindCachePages( + const ULONG64 requestedPosition, + const UINT32 requestedSize, + std::vector<ULONG64>& allPages, + std::vector<ULONG64>& unsatisfiablePages); + + void SaveBufferToCache(const winrt::Windows::Storage::Streams::IBuffer& buffer, const ULONG64 firstPageOffset); + + winrt::Windows::Storage::Streams::IBuffer ReadPageFromCache(const ULONG64 pageOffset); + + void VacateStaleEntriesFromCache(); + + std::future<void> DownloadAndSaveToCacheAysnc( + const std::vector<ULONG64> unsatisfiablePages, + HttpClientWrapper* httpClientWrapper, + const winrt::Windows::Storage::Streams::InputStreamOptions httpInputStreamOptions); + + winrt::Windows::Storage::Streams::IBuffer TrimBufferToSatisfyRequest( + const winrt::Windows::Storage::Streams::IBuffer& constructedBuffer, + const ULONG64 requestedPosition, + const UINT32 requestedSize, + const std::vector<ULONG64> allPages); + + winrt::Windows::Storage::Streams::IBuffer CreateTrimmedBuffer( + const winrt::Windows::Storage::Streams::IBuffer& originalBuffer, + UINT32 trimStartIndex, + UINT32 size); + + winrt::Windows::Storage::Streams::IBuffer ConcatenateBuffers( + const winrt::Windows::Storage::Streams::IBuffer& buffer1, + const winrt::Windows::Storage::Streams::IBuffer& buffer2); + }; +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/HttpStream/HttpRandomAccessStream.cpp b/src/AppInstallerCommonCore/HttpStream/HttpRandomAccessStream.cpp @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" +#include "HttpRandomAccessStream.h" + +using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::Storage::Streams; + +// Note: the HttpRandomAccessStream is passed to the AppxPackaging COM API +// All exceptions thrown accross dll boundaries should be WinRT exception not custom exceptions. +// The HRESULTs will be mapped to UI error code by the appropriate component +namespace AppInstaller::Utility::HttpStream +{ + IAsyncOperation<IRandomAccessStream> HttpRandomAccessStream::CreateAsync(const Uri& uri) + { + winrt::com_ptr<HttpRandomAccessStream> stream = winrt::make_self<HttpRandomAccessStream>(); + + stream->m_httpHelper = co_await HttpClientWrapper::CreateAsync(uri); + stream->m_size = stream->m_httpHelper->GetFullFileSize(); + stream->m_httpLocalCache = std::make_unique<HttpLocalCache>(); + + co_return stream.as<IRandomAccessStream>(); + + } + + uint64_t HttpRandomAccessStream::Size() const + { + return m_size; + } + + void HttpRandomAccessStream::Size(uint64_t value) + { + UNREFERENCED_PARAMETER(value); + THROW_HR(E_NOTIMPL); + } + + uint64_t HttpRandomAccessStream::Position() const + { + return m_requestedPosition; + } + + bool HttpRandomAccessStream::CanRead() const + { + return true; + } + + bool HttpRandomAccessStream::CanWrite() const + { + return false; + } + + IInputStream HttpRandomAccessStream::GetInputStreamAt(uint64_t position) const + { + UNREFERENCED_PARAMETER(position); + THROW_HR(E_NOTIMPL); + } + + IOutputStream HttpRandomAccessStream::GetOutputStreamAt(uint64_t position) const + { + UNREFERENCED_PARAMETER(position); + THROW_HR(E_NOTIMPL); + } + + IRandomAccessStream HttpRandomAccessStream::CloneStream() const + { + THROW_HR(E_NOTIMPL); + } + + void HttpRandomAccessStream::Seek(uint64_t position) + { + m_requestedPosition = position; + } + + IAsyncOperationWithProgress<IBuffer, uint32_t> HttpRandomAccessStream::ReadAsync( + IBuffer buffer, + uint32_t count, + InputStreamOptions options) + { + IBuffer result = co_await m_httpLocalCache->ReadFromCacheAndDownloadIfNecessaryAsync( + m_requestedPosition, + count, + m_httpHelper.get(), + options); + winrt::check_hresult(ULong64Add(m_requestedPosition, result.Length(), &m_requestedPosition)); + + co_return result; + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/HttpStream/HttpRandomAccessStream.h b/src/AppInstallerCommonCore/HttpStream/HttpRandomAccessStream.h @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once +#include "pch.h" +#include "HttpClientWrapper.h" +#include "HttpLocalCache.h" + +namespace AppInstaller::Utility::HttpStream +{ + // Provides an implementation of a random access stream over HTTP that supports + // range-based fetching. This is intended to be used by AppxPackageReader. + // + // Note: If the server doesn't support HTTP ranges, this implementation will throw an exception. + class HttpRandomAccessStream : public winrt::implements< + HttpRandomAccessStream, + winrt::Windows::Storage::Streams::IRandomAccessStream, + winrt::Windows::Storage::Streams::IInputStream> + { + public: + static winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::Storage::Streams::IRandomAccessStream> CreateAsync( + const winrt::Windows::Foundation::Uri& uri); + uint64_t Size() const; + void Size(uint64_t value); + uint64_t Position() const; + bool CanRead() const; + bool CanWrite() const; + winrt::Windows::Storage::Streams::IInputStream GetInputStreamAt(uint64_t position) const; + winrt::Windows::Storage::Streams::IOutputStream GetOutputStreamAt(uint64_t position) const; + winrt::Windows::Storage::Streams::IRandomAccessStream CloneStream() const; + void Seek(uint64_t position); + winrt::Windows::Foundation::IAsyncOperationWithProgress<winrt::Windows::Storage::Streams::IBuffer, uint32_t> ReadAsync( + winrt::Windows::Storage::Streams::IBuffer buffer, + uint32_t count, + winrt::Windows::Storage::Streams::InputStreamOptions options); + + private: + std::shared_ptr<HttpClientWrapper> m_httpHelper; + std::unique_ptr<HttpLocalCache> m_httpLocalCache; + unsigned long long m_size; + unsigned long long m_requestedPosition; + }; +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/MsixInfo.cpp b/src/AppInstallerCommonCore/MsixInfo.cpp @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" +#include "HttpStream/HttpRandomAccessStream.h" +#include "Public/AppInstallerStrings.h" +#include "Public/AppInstallerMsixInfo.h" + + +using namespace winrt::Windows::Storage::Streams; +using namespace Microsoft::WRL; +using namespace AppInstaller::Utility::HttpStream; + +namespace AppInstaller::Msix +{ + bool GetBundleReader( + _In_ IStream* inputStream, + _Outptr_ IAppxBundleReader** reader) + { + ComPtr<IAppxBundleFactory> bundleFactory; + + // Create a new Appxbundle factory + THROW_IF_FAILED(CoCreateInstance( + __uuidof(AppxBundleFactory), + nullptr, + CLSCTX_INPROC_SERVER, + __uuidof(IAppxBundleFactory), + (LPVOID*)(&bundleFactory))); + + HRESULT hr = bundleFactory->CreateBundleReader(inputStream, reader); + + if (SUCCEEDED(hr)) + { + return true; + } + else if (hr == APPX_E_MISSING_REQUIRED_FILE) + { + // APPX_E_MISSING_REQUIRED_FILE returned when trying to open + // an *.msix as an *.msixbundle or vice-versa. + return false; + } + else + { + THROW_HR(hr); + } + } + + bool GetPackageReader( + _In_ IStream* inputStream, + _Outptr_ IAppxPackageReader** reader) + { + + ComPtr<IAppxFactory> appxFactory; + + // Create a new Appx factory + THROW_IF_FAILED(CoCreateInstance( + __uuidof(AppxFactory), + nullptr, + CLSCTX_INPROC_SERVER, + __uuidof(IAppxFactory), + (LPVOID*)(&appxFactory))); + + // Create a new package reader using the factory. + HRESULT hr = appxFactory->CreatePackageReader(inputStream, reader); + + if (SUCCEEDED(hr)) + { + return true; + } + else if (hr == APPX_E_MISSING_REQUIRED_FILE) + { + // APPX_E_MISSING_REQUIRED_FILE returned when trying to open + // an *.msix as an *.msixbundle or vice-versa. + return false; + } + else + { + THROW_HR(hr); + } + } + + MsixInfo::MsixInfo(const std::string& uriStr) + { + // Get an IStream from the input uri and try to create package or bundler reader. + winrt::Windows::Foundation::Uri uri(Utility::ConvertToUTF16(uriStr)); + IRandomAccessStream randomAccessStream = HttpRandomAccessStream::CreateAsync(uri).get(); + + ::IUnknown* rasAsIUnknown = (::IUnknown*)winrt::get_abi(randomAccessStream); + THROW_IF_FAILED(CreateStreamOverRandomAccessStream( + rasAsIUnknown, + IID_PPV_ARGS(m_stream.ReleaseAndGetAddressOf()))); + + if (GetBundleReader(m_stream.Get(), &m_bundleReader)) + { + m_isBundle = true; + } + else if (GetPackageReader(m_stream.Get(), &m_packageReader)) + { + m_isBundle = false; + } + else + { + THROW_HR_MSG(HRESULT_FROM_WIN32(ERROR_INSTALL_OPEN_PACKAGE_FAILED), + "Failed to open uri as msix package or bundle. Uri: %s", uriStr.c_str()); + } + } + + std::vector<byte> MsixInfo::GetSignature() + { + ComPtr<IAppxFile> signatureFile; + if (m_isBundle) + { + THROW_IF_FAILED(m_bundleReader->GetFootprintFile(APPX_BUNDLE_FOOTPRINT_FILE_TYPE_SIGNATURE, &signatureFile)); + } + else + { + THROW_IF_FAILED(m_packageReader->GetFootprintFile(APPX_FOOTPRINT_FILE_TYPE_SIGNATURE, &signatureFile)); + } + + std::vector<byte> signatureContent; + DWORD signatureSize; + + ComPtr<IStream> signatureStream; + THROW_IF_FAILED(signatureFile->GetStream(&signatureStream)); + + STATSTG stat = { 0 }; + THROW_IF_FAILED(signatureStream->Stat(&stat, STATFLAG_NONAME)); + THROW_HR_IF(E_UNEXPECTED, stat.cbSize.HighPart != 0); // Signature size should be small + signatureSize = stat.cbSize.LowPart; + + signatureContent.resize(signatureSize); + + DWORD signatureRead; + THROW_IF_FAILED(signatureStream->Read(signatureContent.data(), signatureSize, &signatureRead)); + THROW_HR_IF_MSG(E_UNEXPECTED, signatureRead != signatureSize, "Failed to read the whole signature stream"); + + return signatureContent; + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/AppInstallerMsixInfo.h b/src/AppInstallerCommonCore/Public/AppInstallerMsixInfo.h @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once +#include "pch.h" + +namespace AppInstaller::Msix +{ + // Function to create an AppxBundle package reader given the input file name. + // Returns true if success, false if the input stream is of wrong type. + bool GetBundleReader( + IStream* inputStream, + IAppxBundleReader** reader); + + // Function to create an Appx package reader given the input file name. + // Returns true if success, false if the input stream is of wrong type. + bool GetPackageReader( + IStream* inputStream, + IAppxPackageReader** reader); + + // MsixInfo class handles all appx/msix related query. + struct MsixInfo + { + MsixInfo(const std::string& uriStr); + + MsixInfo(const MsixInfo&) = default; + MsixInfo& operator=(const MsixInfo&) = default; + + MsixInfo(MsixInfo&&) = default; + MsixInfo& operator=(MsixInfo&&) = default; + + inline bool GetIsBundle() + { + return m_isBundle; + } + + // Full content of AppxSignature.p7x + std::vector<byte> GetSignature(); + + private: + bool m_isBundle; + Microsoft::WRL::ComPtr<IStream> m_stream; + Microsoft::WRL::ComPtr<IAppxBundleReader> m_bundleReader; + Microsoft::WRL::ComPtr<IAppxPackageReader> m_packageReader; + }; +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/AppInstallerStrings.h b/src/AppInstallerCommonCore/Public/AppInstallerStrings.h @@ -13,6 +13,12 @@ namespace AppInstaller::Utility // Converts the given UTF8 string to UTF16 std::wstring ConvertToUTF16(std::string_view input); - // Get the lower case version of the given string + // Get the lower case version of the given std::string std::string ToLower(const std::string& in); + + // Get the lower case version of the given std::wstring + std::wstring ToLower(const std::wstring& in); + + // Checks if the input string is empty or whitespace + bool IsEmptyOrWhitespace(std::wstring_view str); } diff --git a/src/AppInstallerCommonCore/pch.h b/src/AppInstallerCommonCore/pch.h @@ -9,10 +9,29 @@ #include "TraceLogging.h" +// wil/cppwinrt.h should always be included before any C++/WinRT or WIL header file when both are in use +#include <wil/cppwinrt.h> #include <wil/result_macros.h> #include <wil/safecast.h> #include <wil/resource.h> +#include <winrt/Windows.Foundation.h> +#include <winrt/Windows.Foundation.Collections.h> +#include <winrt/Windows.Security.Cryptography.h> +#include <winrt/Windows.Storage.h> +#include <winrt/Windows.Storage.Streams.h> +#include <winrt/Windows.Web.Http.h> +#include <winrt/Windows.Web.Http.Headers.h> +#include <winrt/Windows.Web.Http.Filters.h> + +#include <wrl/client.h> + +// Stream/buffer helper APIs +#include <robuffer.h> +#include <shcore.h> + +#include <AppxPackaging.h> + #include <chrono> #include <filesystem> #include <fstream> @@ -25,4 +44,5 @@ #include <string_view> #include <type_traits> #include <vector> -#include <future>- \ No newline at end of file +#include <future> +#include <cwctype>+ \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Manifest/Manifest.cpp b/src/AppInstallerRepositoryCore/Manifest/Manifest.cpp @@ -46,7 +46,6 @@ namespace AppInstaller::Manifest // Optional fields. this->AppMoniker = rootNode["AppMoniker"] ? rootNode["AppMoniker"].as<std::string>() : ""; - this->Authors = rootNode["Authors"] ? rootNode["Authors"].as<std::string>() : ""; this->Channel = rootNode["Channel"] ? rootNode["Channel"].as<std::string>() : ""; this->Author = rootNode["Author"] ? rootNode["Author"].as<std::string>() : ""; this->License = rootNode["License"] ? rootNode["License"].as<std::string>() : ""; @@ -55,7 +54,9 @@ namespace AppInstaller::Manifest this->Commands = SplitMultiValueField(rootNode["Commands"] ? rootNode["Commands"].as<std::string>() : ""); this->Protocols = SplitMultiValueField(rootNode["Protocols"] ? rootNode["Protocols"].as<std::string>() : ""); this->FileExtensions = SplitMultiValueField(rootNode["FileExtensions"] ? rootNode["FileExtensions"].as<std::string>() : ""); - this->InstallerType = rootNode["InstallerType"] ? rootNode["InstallerType"].as<std::string>() : ""; + this->InstallerType = rootNode["InstallerType"] ? + ManifestInstaller::ConvertToInstallerTypeEnum(rootNode["InstallerType"].as<std::string>()) : + ManifestInstaller::InstallerTypeEnum::Unknown; this->Description = rootNode["Description"] ? rootNode["Description"].as<std::string>() : ""; this->Homepage = rootNode["Homepage"] ? rootNode["Homepage"].as<std::string>() : ""; this->LicenseUrl = rootNode["LicenseUrl"] ? rootNode["LicenseUrl"].as<std::string>() : ""; diff --git a/src/AppInstallerRepositoryCore/Manifest/Manifest.h b/src/AppInstallerRepositoryCore/Manifest/Manifest.h @@ -34,9 +34,6 @@ namespace AppInstaller::Manifest std::string Publisher; - // Comma separated Values - std::string Authors; - std::string Channel; std::string Author; @@ -61,7 +58,7 @@ namespace AppInstaller::Manifest std::vector<ManifestLocalization> Localization; - std::string InstallerType; + ManifestInstaller::InstallerTypeEnum InstallerType; std::optional<InstallerSwitches> Switches; diff --git a/src/AppInstallerRepositoryCore/Manifest/ManifestInstaller.cpp b/src/AppInstallerRepositoryCore/Manifest/ManifestInstaller.cpp @@ -13,13 +13,18 @@ namespace AppInstaller::Manifest this->Url = installerNode["Url"].as<std::string>(); this->Sha256 = Utility::SHA256::ConvertToBytes(installerNode["Sha256"].as<std::string>()); + if (installerNode["SignatureSha256"]) + { + this->SignatureSha256 = Utility::SHA256::ConvertToBytes(installerNode["SignatureSha256"].as<std::string>()); + } + // Optional fields. this->Language = installerNode["Language"] ? installerNode["Language"].as<std::string>() : ""; this->Scope = installerNode["Scope"] ? installerNode["Scope"].as<std::string>() : ""; this->InstallerType = installerNode["InstallerType"] ? - installerNode["InstallerType"].as<std::string>() : - defaultInstaller.InstallerType; + ConvertToInstallerTypeEnum(installerNode["InstallerType"].as<std::string>()) : + InstallerTypeEnum::Unknown; if (installerNode["Switches"]) { @@ -34,4 +39,73 @@ namespace AppInstaller::Manifest this->Switches.emplace(defaultInstaller.Switches.value()); } } + + ManifestInstaller::InstallerTypeEnum ManifestInstaller::ConvertToInstallerTypeEnum(const std::string& in) + { + std::string inStrLower = Utility::ToLower(in); + InstallerTypeEnum result = InstallerTypeEnum::Unknown; + + if (inStrLower == "inno") + { + result = InstallerTypeEnum::Inno; + } + else if (inStrLower == "wix") + { + result = InstallerTypeEnum::Wix; + } + else if (inStrLower == "msi") + { + result = InstallerTypeEnum::Msi; + } + else if (inStrLower == "nullsoft") + { + result = InstallerTypeEnum::Nullsoft; + } + else if (inStrLower == "zip") + { + result = InstallerTypeEnum::Zip; + } + else if (inStrLower == "appx" || inStrLower == "msix") + { + result = InstallerTypeEnum::Msix; + } + else if (inStrLower == "exe") + { + result = InstallerTypeEnum::Exe; + } + + return result; + } + + std::ostream& operator<<(std::ostream& out, const ManifestInstaller::InstallerTypeEnum& installerType) + { + switch (installerType) + { + case ManifestInstaller::InstallerTypeEnum::Exe: + out << "Exe"; + break; + case ManifestInstaller::InstallerTypeEnum::Inno: + out << "Inno"; + break; + case ManifestInstaller::InstallerTypeEnum::Msi: + out << "Msi"; + break; + case ManifestInstaller::InstallerTypeEnum::Msix: + out << "Msix"; + break; + case ManifestInstaller::InstallerTypeEnum::Nullsoft: + out << "Nullsoft"; + break; + case ManifestInstaller::InstallerTypeEnum::Wix: + out << "Wix"; + break; + case ManifestInstaller::InstallerTypeEnum::Zip: + out << "Zip"; + break; + default: + out << "Unknown"; + } + + return out; + } } diff --git a/src/AppInstallerRepositoryCore/Manifest/ManifestInstaller.h b/src/AppInstallerRepositoryCore/Manifest/ManifestInstaller.h @@ -11,6 +11,19 @@ namespace AppInstaller::Manifest class ManifestInstaller { public: + + enum class InstallerTypeEnum + { + Inno, + Wix, + Msi, + Nullsoft, + Zip, + Msix, + Exe, + Unknown + }; + // Required. Values: x86, x64, arm, arm64, all. AppInstaller::Utility::Architecture Arch; @@ -20,6 +33,10 @@ namespace AppInstaller::Manifest // Required std::vector<BYTE> Sha256; + // Optional. Only used by appx/msix type. If provided, Appinstaller will + // validate appx/msix signature and perform streaming install. + std::vector<BYTE> SignatureSha256; + // Empty means default std::string Language; @@ -27,13 +44,17 @@ namespace AppInstaller::Manifest std::string Scope; // If present, has more presedence than root - std::string InstallerType; + InstallerTypeEnum InstallerType; // If present, has more presedence than root std::optional<InstallerSwitches> Switches; + static InstallerTypeEnum ConvertToInstallerTypeEnum(const std::string& in); + // Populates ManifestInstaller // defaultInstaller: if an optional field is not found in the YAML node, the field will be populated with value from defaultInstaller. void PopulateInstallerFields(const YAML::Node& installerNode, const ManifestInstaller& defaultInstaller); }; + + std::ostream& operator<<(std::ostream& out, const ManifestInstaller::InstallerTypeEnum& installerType); } \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/pch.h b/src/AppInstallerRepositoryCore/pch.h @@ -8,6 +8,7 @@ #include <AppInstallerErrors.h> #include <AppInstallerLogging.h> #include <AppInstallerSHA256.h> +#include <AppInstallerStrings.h> #include <yaml-cpp/yaml.h> #include <wil/result_macros.h>