commit b79bf102e3d16840eb1107105aac7f9dbcb93573 parent b7dc50ed8b8e8f59fcdde9a4fe99b5929b9835f8 Author: yao-msft <50888816+yao-msft@users.noreply.github.com> Date: Thu, 16 Jul 2020 12:20:19 -0700 Implement store app installation from manifest (#493) Diffstat:
39 files changed, 1051 insertions(+), 699 deletions(-)
diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.cpp b/src/AppInstallerCLICore/Commands/InstallCommand.cpp @@ -62,7 +62,6 @@ namespace AppInstaller::CLI Workflow::EnsureApplicableInstaller << Workflow::ShowInstallationDisclaimer << Workflow::DownloadInstaller << - Workflow::VerifyInstallerHash << Workflow::ExecuteInstaller << Workflow::RemoveInstaller; } diff --git a/src/AppInstallerCLICore/Commands/ValidateCommand.cpp b/src/AppInstallerCLICore/Commands/ValidateCommand.cpp @@ -41,7 +41,7 @@ namespace AppInstaller::CLI try { - (void)Manifest::Manifest::CreateFromPath(inputFile, true, true); + (void)Manifest::YamlParser::CreateFromPath(inputFile, true, true); context.Reporter.Info() << Resource::String::ManifestValidationSuccess << std::endl; } catch (const Manifest::ManifestException& e) diff --git a/src/AppInstallerCLICore/ExecutionReporter.h b/src/AppInstallerCLICore/ExecutionReporter.h @@ -10,6 +10,7 @@ #include <wil/resource.h> #include <atomic> +#include <iomanip> #include <istream> #include <ostream> #include <string> @@ -17,6 +18,8 @@ namespace AppInstaller::CLI::Execution { +#define WINGET_OSTREAM_FORMAT_HRESULT(hr) "0x" << std::hex << std::setw(8) << std::setfill('0') << hr + namespace details { // List of approved types for output, others are potentially not localized. diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -64,6 +64,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(IdArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(InstallationDisclaimer1); WINGET_DEFINE_RESOURCE_STRINGID(InstallationDisclaimer2); + WINGET_DEFINE_RESOURCE_STRINGID(InstallationDisclaimerMSStore); WINGET_DEFINE_RESOURCE_STRINGID(InstallationRequiresHigherWindows); WINGET_DEFINE_RESOURCE_STRINGID(InstallCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(InstallCommandShortDescription); @@ -71,6 +72,8 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(InstallerHashMismatchOverridden); WINGET_DEFINE_RESOURCE_STRINGID(InstallerHashMismatchOverrideRequired); WINGET_DEFINE_RESOURCE_STRINGID(InstallerHashVerified); + WINGET_DEFINE_RESOURCE_STRINGID(InstallFlowInstallSuccess); + WINGET_DEFINE_RESOURCE_STRINGID(InstallFlowStartingPackageInstall); WINGET_DEFINE_RESOURCE_STRINGID(InstallForceArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(InteractiveArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(InvalidAliasError); @@ -91,6 +94,14 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(MonikerArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(MsixArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(MsixSignatureHashFailed); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallAppBlocked); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallFailed); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallGetEntitlementNetworkError); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallGetEntitlementNoStoreAccount); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallGetEntitlementServerError); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallGetEntitlementSuccess); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallStoreClientBlocked); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallTryGetEntitlement); WINGET_DEFINE_RESOURCE_STRINGID(MultiplePackagesFound); WINGET_DEFINE_RESOURCE_STRINGID(NameArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(NoApplicableInstallers); diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -6,14 +6,15 @@ #include "ShellExecuteInstallerHandler.h" #include "WorkflowBase.h" - -using namespace winrt::Windows::Foundation; -using namespace winrt::Windows::Management::Deployment; -using namespace AppInstaller::Utility; -using namespace AppInstaller::Manifest; - namespace AppInstaller::CLI::Workflow { + using namespace winrt::Windows::ApplicationModel::Store::Preview::InstallControl; + using namespace winrt::Windows::Foundation; + using namespace winrt::Windows::Foundation::Collections; + using namespace winrt::Windows::Management::Deployment; + using namespace AppInstaller::Utility; + using namespace AppInstaller::Manifest; + void EnsureMinOSVersion(Execution::Context& context) { const auto& manifest = context.Get<Execution::Data::Manifest>(); @@ -39,9 +40,18 @@ namespace AppInstaller::CLI::Workflow void ShowInstallationDisclaimer(Execution::Context& context) { - context.Reporter.Info() << - Resource::String::InstallationDisclaimer1 << std::endl << - Resource::String::InstallationDisclaimer2 << std::endl; + auto installerType = context.Get<Execution::Data::Installer>().value().InstallerType; + + if (installerType == ManifestInstaller::InstallerTypeEnum::MSStore) + { + context.Reporter.Info() << Resource::String::InstallationDisclaimerMSStore << std::endl; + } + else + { + context.Reporter.Info() << + Resource::String::InstallationDisclaimer1 << std::endl << + Resource::String::InstallationDisclaimer2 << std::endl; + } } void DownloadInstaller(Execution::Context& context) @@ -56,19 +66,22 @@ namespace AppInstaller::CLI::Workflow case ManifestInstaller::InstallerTypeEnum::Msi: case ManifestInstaller::InstallerTypeEnum::Nullsoft: case ManifestInstaller::InstallerTypeEnum::Wix: - context << DownloadInstallerFile; + context << DownloadInstallerFile << VerifyInstallerHash; break; case ManifestInstaller::InstallerTypeEnum::Msix: if (installer.SignatureSha256.empty()) { - context << DownloadInstallerFile; + context << DownloadInstallerFile << VerifyInstallerHash; } else { // Signature hash provided. No download needed. Just verify signature hash. - context << GetMsixSignatureHash; + context << GetMsixSignatureHash << VerifyInstallerHash; } break; + case ManifestInstaller::InstallerTypeEnum::MSStore: + // Nothing to do here + break; default: THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); } @@ -192,6 +205,11 @@ namespace AppInstaller::CLI::Workflow case ManifestInstaller::InstallerTypeEnum::Msix: context << MsixInstall; break; + case ManifestInstaller::InstallerTypeEnum::MSStore: + context << + EnsureFeatureEnabled(Settings::ExperimentalFeature::Feature::ExperimentalMSStore) << + MSStoreInstall; + break; default: THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); } @@ -217,7 +235,7 @@ namespace AppInstaller::CLI::Workflow uri = Uri(Utility::ConvertToUTF16(context.Get<Execution::Data::Installer>()->Url)); } - context.Reporter.Info() << "Starting package install..." << std::endl; + context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl; try { @@ -235,7 +253,7 @@ namespace AppInstaller::CLI::Workflow AICLI_TERMINATE_CONTEXT(re.GetErrorCode()); } - context.Reporter.Info() << "Successfully installed." << std::endl; + context.Reporter.Info() << Resource::String::InstallFlowInstallSuccess << std::endl; } void RemoveInstaller(Execution::Context& context) @@ -248,4 +266,124 @@ namespace AppInstaller::CLI::Workflow std::filesystem::remove(path); } } + + void MSStoreInstall(Execution::Context& context) + { + auto productId = Utility::ConvertToUTF16(context.Get<Execution::Data::Installer>()->ProductId); + + constexpr std::wstring_view s_StoreClientName = L"Microsoft.WindowsStore"sv; + constexpr std::wstring_view s_StoreClientPublisher = L"CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"sv; + + // Policy check + AppInstallManager installManager; + if (installManager.IsStoreBlockedByPolicyAsync(s_StoreClientName, s_StoreClientPublisher).get()) + { + context.Reporter.Error() << Resource::String::MSStoreInstallStoreClientBlocked << std::endl; + AICLI_LOG(CLI, Error, << "Store client is blocked by policy. MSStore install failed."); + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_MSSTORE_BLOCKED_BY_POLICY); + } + + if (!installManager.GetIsAppAllowedToInstallAsync(productId).get()) + { + context.Reporter.Error() << Resource::String::MSStoreInstallAppBlocked << std::endl; + AICLI_LOG(CLI, Error, << "App is blocked by policy. MSStore install failed. ProductId: " << Utility::ConvertToUTF8(productId)); + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_MSSTORE_APP_BLOCKED_BY_POLICY); + } + + // Verifying/Acquiring product ownership + context.Reporter.Info() << Resource::String::MSStoreInstallTryGetEntitlement << std::endl; + GetEntitlementResult enr = installManager.GetFreeUserEntitlementAsync(productId, winrt::hstring(), winrt::hstring()).get(); + + if (enr.Status() == GetEntitlementStatus::Succeeded) + { + context.Reporter.Info() << Resource::String::MSStoreInstallGetEntitlementSuccess << std::endl; + AICLI_LOG(CLI, Error, << "Get entitlement succeeded."); + } + else + { + if (enr.Status() == GetEntitlementStatus::NoStoreAccount) + { + context.Reporter.Info() << Resource::String::MSStoreInstallGetEntitlementNoStoreAccount << std::endl; + AICLI_LOG(CLI, Error, << "Get entitlement failed. No Store account."); + } + else if (enr.Status() == GetEntitlementStatus::NetworkError) + { + context.Reporter.Info() << Resource::String::MSStoreInstallGetEntitlementNetworkError << std::endl; + AICLI_LOG(CLI, Error, << "Get entitlement failed. Network error."); + } + else if (enr.Status() == GetEntitlementStatus::ServerError) + { + context.Reporter.Info() << Resource::String::MSStoreInstallGetEntitlementServerError << std::endl; + AICLI_LOG(CLI, Error, << "Get entitlement succeeded. Server error. ProductId: " << Utility::ConvertToUTF8(productId)); + } + + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_MSSTORE_INSTALL_FAILED); + } + + context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl; + + IVectorView<AppInstallItem> installItems = installManager.StartProductInstallAsync( + productId, // ProductId + winrt::hstring(), // CatalogId + winrt::hstring(), // FlightId + L"WinGetCli", // ClientId + false, // repair + false, + winrt::hstring(), + nullptr).get(); + + for (auto const& installItem : installItems) + { + AICLI_LOG(CLI, Info, << + "Started MSStore package installation. ProductId: " << Utility::ConvertToUTF8(installItem.ProductId()) << + " PackageFamilyName: " << Utility::ConvertToUTF8(installItem.PackageFamilyName())); + } + + context.Reporter.ExecuteWithProgress( + [&](IProgressCallback& progress) + { + // We are aggregating all AppInstallItem progresses into one. + // Averaging every progress for now until we have a better way to find overall progress. + uint64_t overallProgressMax = 100 * installItems.Size(); + uint64_t currentProgress = 0; + + while (currentProgress < overallProgressMax) + { + currentProgress = 0; + + for (auto const& installItem : installItems) + { + const auto& status = installItem.GetCurrentStatus(); + currentProgress += static_cast<uint64_t>(status.PercentComplete()); + + HRESULT errorCode = status.ErrorCode(); + if (!SUCCEEDED(errorCode)) + { + context.Reporter.Info() << Resource::String::MSStoreInstallFailed << ' ' << WINGET_OSTREAM_FORMAT_HRESULT(errorCode) << std::endl; + AICLI_LOG(CLI, Error, << "MSStore install failed. ProductId: " << Utility::ConvertToUTF8(productId) << " HResult: " << WINGET_OSTREAM_FORMAT_HRESULT(errorCode)); + AICLI_TERMINATE_CONTEXT(errorCode); + } + } + + // It may take a while for Store client to pick up the install request. + // So we show indefinite progress here to avoid a progress bar stuck at 0. + if (currentProgress > 0) + { + progress.OnProgress(currentProgress, overallProgressMax, ProgressType::Percent); + } + + if (progress.IsCancelled()) + { + for (auto const& installItem : installItems) + { + installItem.Cancel(); + } + } + + Sleep(100); + } + }); + + context.Reporter.Info() << Resource::String::InstallFlowInstallSuccess << std::endl; + } } diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.h b/src/AppInstallerCLICore/Workflows/InstallFlow.h @@ -71,6 +71,12 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void MsixInstall(Execution::Context& context); + // Deploys the Store app. + // Required Args: None + // Inputs: Manifest?, Installer + // Outputs: None + void MSStoreInstall(Execution::Context& context); + // Deletes the installer file. // Required Args: None // Inputs: InstallerPath diff --git a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp @@ -149,7 +149,7 @@ namespace AppInstaller::CLI::Workflow void ShellExecuteInstallImpl(Execution::Context& context) { - context.Reporter.Info() << "Installing ..." << std::endl; + context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl; const std::string& installerArgs = context.Get<Execution::Data::InstallerArgs>(); @@ -180,7 +180,7 @@ namespace AppInstaller::CLI::Workflow } else { - context.Reporter.Info() << "Successfully installed!" << std::endl; + context.Reporter.Info() << Resource::String::InstallFlowInstallSuccess << std::endl; } } diff --git a/src/AppInstallerCLICore/Workflows/ShowFlow.cpp b/src/AppInstallerCLICore/Workflows/ShowFlow.cpp @@ -49,13 +49,23 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Info() << "Installer:" << std::endl; if (installer) { + context.Reporter.Info() << " Type: " << Manifest::ManifestInstaller::InstallerTypeToString(installer->InstallerType) << std::endl; if (!installer->Language.empty()) { context.Reporter.Info() << " Language: " << installer->Language << std::endl; } - context.Reporter.Info() << " SHA256: " << Utility::SHA256::ConvertToString(installer->Sha256) << std::endl; - context.Reporter.Info() << " Download Url: " << installer->Url << std::endl; - context.Reporter.Info() << " Type: " << Manifest::ManifestInstaller::InstallerTypeToString(installer->InstallerType) << std::endl; + if (!installer->Url.empty()) + { + context.Reporter.Info() << " Download Url: " << installer->Url << std::endl; + } + if (!installer->Sha256.empty()) + { + context.Reporter.Info() << " SHA256: " << Utility::SHA256::ConvertToString(installer->Sha256) << std::endl; + } + if (!installer->ProductId.empty()) + { + context.Reporter.Info() << " Store Product Id: " << installer->ProductId << std::endl; + } } else { diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -5,6 +5,7 @@ #include "ExecutionContext.h" #include "ManifestComparator.h" #include "TableOutput.h" +#include "Manifest/YamlParser.h" namespace AppInstaller::CLI::Workflow @@ -312,7 +313,7 @@ namespace AppInstaller::CLI::Workflow VerifyFile(Execution::Args::Type::Manifest) << [](Execution::Context& context) { - Manifest::Manifest manifest = Manifest::Manifest::CreateFromPath(Utility::ConvertToUTF16(context.Args.GetArg(Execution::Args::Type::Manifest))); + Manifest::Manifest manifest = Manifest::YamlParser::CreateFromPath(Utility::ConvertToUTF16(context.Args.GetArg(Execution::Args::Type::Manifest))); Logging::Telemetry().LogManifestFields(manifest.Id, manifest.Name, manifest.Version, true); context.Add<Execution::Data::Manifest>(std::move(manifest)); }; @@ -363,6 +364,17 @@ namespace AppInstaller::CLI::Workflow AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_COMMAND_REQUIRES_ADMIN); } } + + void EnsureFeatureEnabled::operator()(Execution::Context& context) const + { + if (!Settings::ExperimentalFeature::IsEnabled(m_feature)) + { + context.Reporter.Error() << Resource::String::FeatureDisabledMessage << " : '" << + Settings::ExperimentalFeature::GetFeature(m_feature).JsonName() << '\'' << std::endl; + AICLI_LOG(CLI, Error, << Settings::ExperimentalFeature::GetFeature(m_feature).Name() << " feature is disabled. Execution cancelled."); + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_EXPERIMENTAL_FEATURE_DISABLED); + } + } } AppInstaller::CLI::Execution::Context& operator<<(AppInstaller::CLI::Execution::Context& context, AppInstaller::CLI::Workflow::WorkflowTask::Func f) diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -3,6 +3,7 @@ #pragma once #include "ExecutionArgs.h" +#include <winget/ExperimentalFeature.h> #include <string> #include <string_view> @@ -133,6 +134,20 @@ namespace AppInstaller::CLI::Workflow // Inputs: None // Outputs: None void EnsureRunningAsAdmin(Execution::Context& context); + + // Ensures that the feature is enabled. + // Required Args: the desired feature + // Inputs: None + // Outputs: None + struct EnsureFeatureEnabled : public WorkflowTask + { + EnsureFeatureEnabled(Settings::ExperimentalFeature::Feature feature) : WorkflowTask("EnsureFeatureEnabled"), m_feature(feature) {} + + void operator()(Execution::Context& context) const override; + + private: + Settings::ExperimentalFeature::Feature m_feature; + }; } // Passes the context to the function if it has not been terminated; returns the context. diff --git a/src/AppInstallerCLICore/pch.h b/src/AppInstallerCLICore/pch.h @@ -1,49 +1,50 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#pragma once - -#define NOMINMAX -#include <windows.h> -#include <WinInet.h> - -#include <winrt/Windows.Foundation.h> -#include <winrt/Windows.Foundation.Collections.h> -#include <winrt/Windows.Management.Deployment.h> -#include <winrt/Windows.UI.ViewManagement.h> -#include <winrt/Windows.ApplicationModel.Resources.h> -#include <winrt/Windows.ApplicationModel.Resources.Core.h> - -#include <wil/result_macros.h> - -#include <array> -#include <iostream> -#include <fstream> -#include <future> -#include <functional> -#include <memory> -#include <numeric> -#include <optional> -#include <sstream> -#include <string_view> -#include <vector> - -#include <yaml-cpp\yaml.h> - -#include <wrl/client.h> -#include <AppxPackaging.h> - -#include <AppInstallerDateTime.h> -#include <AppInstallerDeployment.h> -#include <AppInstallerDownloader.h> -#include <AppInstallerErrors.h> -#include <AppInstallerLogging.h> -#include <AppInstallerMsixInfo.h> -#include <AppInstallerRepositorySearch.h> -#include <AppInstallerRepositorySource.h> -#include <AppInstallerRuntime.h> -#include <AppInstallerSHA256.h> -#include <AppInstallerStrings.h> -#include <AppInstallerTelemetry.h> -#include <Manifest/ManifestInstaller.h> -#include <Manifest/Manifest.h> -#include <winget/LocIndependent.h> +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once + +#define NOMINMAX +#include <windows.h> +#include <WinInet.h> + +#include <winrt/Windows.Foundation.h> +#include <winrt/Windows.Foundation.Collections.h> +#include <winrt/Windows.Management.Deployment.h> +#include <winrt/Windows.UI.ViewManagement.h> +#include <winrt/Windows.ApplicationModel.Resources.h> +#include <winrt/Windows.ApplicationModel.Resources.Core.h> +#include <winrt/Windows.ApplicationModel.Store.Preview.InstallControl.h> + +#include <wil/result_macros.h> + +#include <array> +#include <iostream> +#include <fstream> +#include <future> +#include <functional> +#include <memory> +#include <numeric> +#include <optional> +#include <sstream> +#include <string_view> +#include <vector> + +#include <yaml-cpp\yaml.h> + +#include <wrl/client.h> +#include <AppxPackaging.h> + +#include <AppInstallerDateTime.h> +#include <AppInstallerDeployment.h> +#include <AppInstallerDownloader.h> +#include <AppInstallerErrors.h> +#include <AppInstallerLogging.h> +#include <AppInstallerMsixInfo.h> +#include <AppInstallerRepositorySearch.h> +#include <AppInstallerRepositorySource.h> +#include <AppInstallerRuntime.h> +#include <AppInstallerSHA256.h> +#include <AppInstallerStrings.h> +#include <AppInstallerTelemetry.h> +#include <Manifest/YamlParser.h> +#include <winget/LocIndependent.h> +#include <winget/ExperimentalFeature.h> diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -173,7 +173,7 @@ <value>Found a positional argument when none was expected</value> </data> <data name="FeatureDisabledMessage" xml:space="preserve"> - <value>This command is a work in progress, and may be changed dramatically or removed altogether in the future. To enable it, edit your settings ('winget settings') to include the experimental feature</value> + <value>This feature is a work in progress, and may be changed dramatically or removed altogether in the future. To enable it, edit your settings ('winget settings') to include the experimental feature</value> <comment>{Locked="winget settings"}</comment> </data> <data name="FeaturesCommandLongDescription" xml:space="preserve"> @@ -236,6 +236,10 @@ They can be configured through the settings file 'winget settings'.</value> <data name="InstallationDisclaimer2" xml:space="preserve"> <value>Microsoft is not responsible for, nor does it grant any licenses to, third-party packages.</value> </data> + <data name="InstallationDisclaimerMSStore" xml:space="preserve"> + <value>This package is provided through Microsoft Store. winget may need to acquire the package from Microsoft Store on behalf of the current user.</value> + <comment>{Locked="winget"}</comment> + </data> <data name="InstallationRequiresHigherWindows" xml:space="preserve"> <value>Cannot install package, as it requires a higher version of Windows:</value> </data> @@ -260,6 +264,12 @@ They can be configured through the settings file 'winget settings'.</value> <data name="InstallerHashVerified" xml:space="preserve"> <value>Successfully verified installer hash</value> </data> + <data name="InstallFlowInstallSuccess" xml:space="preserve"> + <value>Successfully installed</value> + </data> + <data name="InstallFlowStartingPackageInstall" xml:space="preserve"> + <value>Starting package install...</value> + </data> <data name="InstallForceArgumentDescription" xml:space="preserve"> <value>Override the installer hash check</value> </data> @@ -322,6 +332,30 @@ They can be configured through the settings file 'winget settings'.</value> <data name="MsixSignatureHashFailed" xml:space="preserve"> <value>Failed to calculate MSIX signature hash.</value> </data> + <data name="MSStoreInstallAppBlocked" xml:space="preserve"> + <value>Failed to install Microsoft Store package because the specific app is blocked by policy</value> + </data> + <data name="MSStoreInstallFailed" xml:space="preserve"> + <value>Failed to install Microsoft Store package. Error code:</value> + </data> + <data name="MSStoreInstallGetEntitlementNetworkError" xml:space="preserve"> + <value>Verifying/Requesting package acquisition failed: network error</value> + </data> + <data name="MSStoreInstallGetEntitlementNoStoreAccount" xml:space="preserve"> + <value>Verifying/Requesting package acquisition failed: no store account found</value> + </data> + <data name="MSStoreInstallGetEntitlementServerError" xml:space="preserve"> + <value>Verifying/Requesting package acquisition failed: server error</value> + </data> + <data name="MSStoreInstallGetEntitlementSuccess" xml:space="preserve"> + <value>Verifying/Requesting package acquisition success</value> + </data> + <data name="MSStoreInstallStoreClientBlocked" xml:space="preserve"> + <value>Failed to install Microsoft Store package because Microsoft Store client is blocked by policy</value> + </data> + <data name="MSStoreInstallTryGetEntitlement" xml:space="preserve"> + <value>Verifying/Requesting package acquisition...</value> + </data> <data name="MultiplePackagesFound" xml:space="preserve"> <value>Multiple packages found matching input criteria. Please refine the input.</value> </data> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -219,6 +219,9 @@ <CopyFileToFolders Include="TestData\InstallFlowTest_Msix_StreamingFlow.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\InstallFlowTest_MSStore.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\InstallFlowTest_NoApplicableArchitecture.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -258,6 +258,9 @@ <CopyFileToFolders Include="TestData\InstallFlowTest_Exe.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\InstallFlowTest_MSStore.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\InstallerArgTest_Msi_WithSwitches.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> diff --git a/src/AppInstallerCLITests/SQLiteIndexSource.cpp b/src/AppInstallerCLITests/SQLiteIndexSource.cpp @@ -2,7 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include "TestCommon.h" -#include <Manifest/Manifest.h> +#include <Manifest/YamlParser.h> #include <Microsoft/SQLiteIndexSource.h> using namespace std::string_literals; @@ -17,7 +17,7 @@ std::shared_ptr<SQLiteIndexSource> SimpleTestSetup(const std::string& filePath, SQLiteIndex index = SQLiteIndex::CreateNew(filePath, Schema::Version::Latest()); TestDataFile testManifest("Manifest-Good.yaml"); - manifest = Manifest::CreateFromPath(testManifest); + manifest = YamlParser::CreateFromPath(testManifest); relativePath = testManifest.GetPath().filename().u8string(); diff --git a/src/AppInstallerCLITests/Sources.cpp b/src/AppInstallerCLITests/Sources.cpp @@ -8,6 +8,7 @@ #include <AppInstallerDateTime.h> #include <AppInstallerRuntime.h> #include <AppInstallerStrings.h> +#include <AppInstallerErrors.h> #include <winget/Settings.h> using namespace AppInstaller; diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_MSStore.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_MSStore.yaml @@ -0,0 +1,12 @@ +Id: AppInstallerCliTest.TestMSStoreInstaller +Version: 1.0.0.0 +Name: AppInstaller Test Installer +Publisher: Microsoft Corporation +AppMoniker: AICLITestMSStore +License: Test +Installers: + - Arch: neutral + Url: https://ThisIsNotUsed + InstallerType: MSStore + ProductId: 9WZDNCRFJ364 +ManifestVersion: 0.2.0-msstore diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp @@ -3,7 +3,7 @@ #include "pch.h" #include "TestCommon.h" #include <AppInstallerLogging.h> -#include <Manifest/Manifest.h> +#include <Manifest/YamlParser.h> #include <AppInstallerDownloader.h> #include <AppInstallerStrings.h> #include <Workflows/InstallFlow.h> @@ -80,7 +80,7 @@ struct TestSource : public ISource if (input == "TestQueryReturnOne") { - auto manifest = Manifest::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); result.Matches.emplace_back( ResultMatch( std::make_unique<TestApplication>(manifest), @@ -88,13 +88,13 @@ struct TestSource : public ISource } else if (input == "TestQueryReturnTwo") { - auto manifest = Manifest::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); result.Matches.emplace_back( ResultMatch( std::make_unique<TestApplication>(manifest), ApplicationMatchFilter(ApplicationMatchField::Id, MatchType::Exact, "TestQueryReturnTwo"))); - auto manifest2 = Manifest::CreateFromPath(TestDataFile("Manifest-Good.yaml")); + auto manifest2 = YamlParser::CreateFromPath(TestDataFile("Manifest-Good.yaml")); result.Matches.emplace_back( ResultMatch( std::make_unique<TestApplication>(manifest2), @@ -220,6 +220,22 @@ void OverrideForMSIX(TestContext& context) } }); } +void OverrideForMSStore(TestContext& context) +{ + context.Override({ MSStoreInstall, [](TestContext& context) + { + std::filesystem::path temp = std::filesystem::temp_directory_path(); + temp /= "TestMSStoreInstalled.txt"; + std::ofstream file(temp, std::ofstream::out); + file << context.Get<Execution::Data::Installer>()->ProductId; + file.close(); + } }); + + context.Override({ "EnsureFeatureEnabled", [](TestContext&) + { + } }); +} + TEST_CASE("ExeInstallFlowWithTestManifest", "[InstallFlow]") { TestCommon::TempFile installResultPath("TestExeInstalled.txt"); @@ -261,6 +277,28 @@ TEST_CASE("InstallFlowWithNonApplicableArchitecture", "[InstallFlow]") REQUIRE(!std::filesystem::exists(installResultPath.GetPath())); } +TEST_CASE("MSStoreInstallFlowWithTestManifest", "[InstallFlow]") +{ + TestCommon::TempFile installResultPath("TestMSStoreInstalled.txt"); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + OverrideForMSStore(context); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_MSStore.yaml").GetPath().u8string()); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + // Verify Installer is called and parameters are passed in. + REQUIRE(std::filesystem::exists(installResultPath.GetPath())); + std::ifstream installResultFile(installResultPath.GetPath()); + REQUIRE(installResultFile.is_open()); + std::string installResultStr; + std::getline(installResultFile, installResultStr); + REQUIRE(installResultStr.find("9WZDNCRFJ364") != std::string::npos); +} + TEST_CASE("MsixInstallFlow_DownloadFlow", "[InstallFlow]") { TestCommon::TempFile installResultPath("TestMsixInstalled.txt"); @@ -315,7 +353,7 @@ TEST_CASE("ShellExecuteHandlerInstallerArgs", "[InstallFlow]") std::ostringstream installOutput; TestContext context{ installOutput, std::cin }; // Default Msi type with no args passed in, no switches specified in manifest - auto manifest = Manifest::CreateFromPath(TestDataFile("InstallerArgTest_Msi_NoSwitches.yaml")); + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallerArgTest_Msi_NoSwitches.yaml")); context.Add<Data::Installer>(manifest.Installers.at(0)); context.Add<Data::InstallerPath>(TestDataFile("AppInstallerTestExeInstaller.exe")); context << GetInstallerArgs; @@ -328,7 +366,7 @@ TEST_CASE("ShellExecuteHandlerInstallerArgs", "[InstallFlow]") std::ostringstream installOutput; TestContext context{ installOutput, std::cin }; // Msi type with /silent and /log and /custom and /installlocation, no switches specified in manifest - auto manifest = Manifest::CreateFromPath(TestDataFile("InstallerArgTest_Msi_NoSwitches.yaml")); + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallerArgTest_Msi_NoSwitches.yaml")); context.Args.AddArg(Execution::Args::Type::Silent); context.Args.AddArg(Execution::Args::Type::Log, "MyLog.log"); context.Args.AddArg(Execution::Args::Type::InstallLocation, "MyDir"); @@ -344,7 +382,7 @@ TEST_CASE("ShellExecuteHandlerInstallerArgs", "[InstallFlow]") std::ostringstream installOutput; TestContext context{ installOutput, std::cin }; // Msi type with /silent and /log and /custom and /installlocation, switches specified in manifest - auto manifest = Manifest::CreateFromPath(TestDataFile("InstallerArgTest_Msi_WithSwitches.yaml")); + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallerArgTest_Msi_WithSwitches.yaml")); context.Args.AddArg(Execution::Args::Type::Silent); context.Args.AddArg(Execution::Args::Type::Log, "MyLog.log"); context.Args.AddArg(Execution::Args::Type::InstallLocation, "MyDir"); @@ -361,7 +399,7 @@ TEST_CASE("ShellExecuteHandlerInstallerArgs", "[InstallFlow]") std::ostringstream installOutput; TestContext context{ installOutput, std::cin }; // Default Inno type with no args passed in, no switches specified in manifest - auto manifest = Manifest::CreateFromPath(TestDataFile("InstallerArgTest_Inno_NoSwitches.yaml")); + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallerArgTest_Inno_NoSwitches.yaml")); context.Add<Data::Installer>(manifest.Installers.at(0)); context.Add<Data::InstallerPath>(TestDataFile("AppInstallerTestExeInstaller.exe")); context << GetInstallerArgs; @@ -374,7 +412,7 @@ TEST_CASE("ShellExecuteHandlerInstallerArgs", "[InstallFlow]") std::ostringstream installOutput; TestContext context{ installOutput, std::cin }; // Inno type with /silent and /log and /custom and /installlocation, no switches specified in manifest - auto manifest = Manifest::CreateFromPath(TestDataFile("InstallerArgTest_Inno_NoSwitches.yaml")); + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallerArgTest_Inno_NoSwitches.yaml")); context.Args.AddArg(Execution::Args::Type::Silent); context.Args.AddArg(Execution::Args::Type::Log, "MyLog.log"); context.Args.AddArg(Execution::Args::Type::InstallLocation, "MyDir"); @@ -390,7 +428,7 @@ TEST_CASE("ShellExecuteHandlerInstallerArgs", "[InstallFlow]") std::ostringstream installOutput; TestContext context{ installOutput, std::cin }; // Inno type with /silent and /log and /custom and /installlocation, switches specified in manifest - auto manifest = Manifest::CreateFromPath(TestDataFile("InstallerArgTest_Inno_WithSwitches.yaml")); + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallerArgTest_Inno_WithSwitches.yaml")); context.Args.AddArg(Execution::Args::Type::Silent); context.Args.AddArg(Execution::Args::Type::Log, "MyLog.log"); context.Args.AddArg(Execution::Args::Type::InstallLocation, "MyDir"); @@ -407,7 +445,7 @@ TEST_CASE("ShellExecuteHandlerInstallerArgs", "[InstallFlow]") std::ostringstream installOutput; TestContext context{ installOutput, std::cin }; // Override switch specified. The whole arg passed to installer is overridden. - auto manifest = Manifest::CreateFromPath(TestDataFile("InstallerArgTest_Inno_WithSwitches.yaml")); + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallerArgTest_Inno_WithSwitches.yaml")); context.Args.AddArg(Execution::Args::Type::Silent); context.Args.AddArg(Execution::Args::Type::Log, "MyLog.log"); context.Args.AddArg(Execution::Args::Type::InstallLocation, "MyDir"); diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp @@ -2,7 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include "TestCommon.h" -#include "Manifest/Manifest.h" +#include "Manifest/YamlParser.h" #include "AppInstallerSHA256.h" using namespace TestCommon; @@ -30,7 +30,7 @@ bool operator==(const MultiValue& a, const MultiValue& b) TEST_CASE("ReadGoodManifestAndVerifyContents", "[ManifestValidation]") { - Manifest manifest = Manifest::CreateFromPath(TestDataFile("Manifest-Good.yaml")); + Manifest manifest = YamlParser::CreateFromPath(TestDataFile("Manifest-Good.yaml")); REQUIRE(manifest.Id == "microsoft.msixsdk"); REQUIRE(manifest.Name == "MSIX SDK"); @@ -108,7 +108,7 @@ TEST_CASE("ReadGoodManifestAndVerifyContents", "[ManifestValidation]") TEST_CASE("ReadGoodManifestWithSpaces", "[ManifestValidation]") { - Manifest manifest = Manifest::CreateFromPath(TestDataFile("Manifest-Good-Spaces.yaml")); + Manifest manifest = YamlParser::CreateFromPath(TestDataFile("Manifest-Good-Spaces.yaml")); REQUIRE(manifest.Id == "microsoft.msixsdk"); REQUIRE(manifest.Name == "MSIX SDK"); @@ -149,11 +149,11 @@ void TestManifest(const std::filesystem::path& manifestPath, const std::string& { if (expectedMessage.empty()) { - CHECK_NOTHROW(Manifest::CreateFromPath(TestDataFile(manifestPath), true, true)); + CHECK_NOTHROW(YamlParser::CreateFromPath(TestDataFile(manifestPath), true, true)); } else { - CHECK_THROWS_MATCHES(Manifest::CreateFromPath(TestDataFile(manifestPath), true, true), ManifestException, ManifestExceptionMatcher(expectedMessage, expectedWarningOnly)); + CHECK_THROWS_MATCHES(YamlParser::CreateFromPath(TestDataFile(manifestPath), true, true), ManifestException, ManifestExceptionMatcher(expectedMessage, expectedWarningOnly)); } } @@ -225,6 +225,7 @@ TEST_CASE("ReadBadManifests", "[ManifestValidation]") { "Manifest-Bad-VersionInvalid.yaml", "Invalid field value. Field: Version" }, { "Manifest-Bad-VersionMissing.yaml", "Required field missing. Field: Version" }, { "Manifest-Bad-InvalidManifestVersionValue.yaml", "Invalid field value. Field: ManifestVersion" }, + { "InstallFlowTest_MSStore.yaml", "Field value is not supported. Field: InstallerType Value: MSStore" }, }; for (auto const& testCase : TestCases) diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -19,6 +19,8 @@ namespace AppInstaller::Settings return User().Get<Setting::EFExperimentalCmd>() || User().Get<Setting::EFExperimentalArg>(); case Feature::ExperimentalArg: return User().Get<Setting::EFExperimentalArg>(); + case Feature::ExperimentalMSStore: + return User().Get<Setting::EFExperimentalMSStore>(); default: THROW_HR(E_UNEXPECTED); } @@ -32,6 +34,8 @@ namespace AppInstaller::Settings return ExperimentalFeature{ "Command Sample", "experimentalCmd", "https://aka.ms/winget-settings", Feature::ExperimentalCmd }; case Feature::ExperimentalArg: return ExperimentalFeature{ "Argument Sample", "experimentalArg", "https://aka.ms/winget-settings", Feature::ExperimentalArg }; + case Feature::ExperimentalMSStore: + return ExperimentalFeature{ "Microsoft Store Support", "experimentalMSStore", "https://aka.ms/winget-settings", Feature::ExperimentalMSStore }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -36,6 +36,10 @@ #define APPINSTALLER_CLI_ERROR_EXTENSION_PUBLIC_FAILED ((HRESULT)0x8A150018) #define APPINSTALLER_CLI_ERROR_COMMAND_REQUIRES_ADMIN ((HRESULT)0x8A150019) #define APPINSTALLER_CLI_ERROR_SOURCE_NOT_SECURE ((HRESULT)0x8A15001A) +#define APPINSTALLER_CLI_ERROR_MSSTORE_BLOCKED_BY_POLICY ((HRESULT)0x8A15001B) +#define APPINSTALLER_CLI_ERROR_MSSTORE_APP_BLOCKED_BY_POLICY ((HRESULT)0x8A15001C) +#define APPINSTALLER_CLI_ERROR_EXPERIMENTAL_FEATURE_DISABLED ((HRESULT)0x8A15001D) +#define APPINSTALLER_CLI_ERROR_MSSTORE_INSTALL_FAILED ((HRESULT)0x8A15001E) namespace AppInstaller { diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -21,7 +21,8 @@ namespace AppInstaller::Settings None = 0x0, ExperimentalCmd = 0x1, ExperimentalArg = 0x2, - Max = 0x4, // This MUST always be last + ExperimentalMSStore = 0x4, + Max = 0x8, // This MUST always be last }; using Feature_t = std::underlying_type_t<ExperimentalFeature::Feature>; diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -46,6 +46,7 @@ namespace AppInstaller::Settings AutoUpdateTimeInMinutes, EFExperimentalCmd, EFExperimentalArg, + EFExperimentalMSStore, Max }; @@ -77,6 +78,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::AutoUpdateTimeInMinutes, uint32_t, std::chrono::minutes, 5min, ".source.autoUpdateIntervalInMinutes"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalCmd, bool, bool, false, ".experimentalFeatures.experimentalCmd"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalArg, bool, bool, false, ".experimentalFeatures.experimentalArg"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalMSStore, bool, bool, false, ".experimentalFeatures.experimentalMSStore"sv); // Used to deduce the SettingVariant type; making a variant that includes std::monostate and all SettingMapping types. diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp @@ -183,6 +183,12 @@ namespace AppInstaller::Settings { return value; } + + std::optional<SettingMapping<Setting::EFExperimentalMSStore>::value_t> + SettingMapping<Setting::EFExperimentalMSStore>::Validate(const SettingMapping<Setting::EFExperimentalMSStore>::json_t& value) + { + return value; + } } UserSettings::UserSettings() : m_type(UserSettingsType::Default) diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -176,6 +176,7 @@ <ClInclude Include="Manifest\Manifest.h" /> <ClInclude Include="Manifest\ManifestInstaller.h" /> <ClInclude Include="Manifest\ManifestLocalization.h" /> + <ClInclude Include="Manifest\YamlParser.h" /> <ClInclude Include="Manifest\ManifestValidation.h" /> <ClInclude Include="Microsoft\PreIndexedPackageSourceFactory.h" /> <ClInclude Include="Microsoft\Schema\1_0\ChannelTable.h" /> @@ -217,7 +218,7 @@ </ClCompile> <ClCompile Include="Manifest\Manifest.cpp" /> <ClCompile Include="Manifest\ManifestInstaller.cpp" /> - <ClCompile Include="Manifest\ManifestLocalization.cpp" /> + <ClCompile Include="Manifest\YamlParser.cpp" /> <ClCompile Include="Manifest\ManifestValidation.cpp" /> <ClCompile Include="Microsoft\PreIndexedPackageSourceFactory.cpp" /> <ClCompile Include="Microsoft\Schema\1_0\Interface.cpp" /> diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -126,6 +126,9 @@ <ClInclude Include="Manifest\ManifestValidation.h"> <Filter>Manifest</Filter> </ClInclude> + <ClInclude Include="Manifest\YamlParser.h"> + <Filter>Header Files</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -152,9 +155,6 @@ <ClCompile Include="Manifest\ManifestInstaller.cpp"> <Filter>Manifest</Filter> </ClCompile> - <ClCompile Include="Manifest\ManifestLocalization.cpp"> - <Filter>Manifest</Filter> - </ClCompile> <ClCompile Include="Microsoft\Schema\1_0\OneToOneTable.cpp"> <Filter>Microsoft\Schema\1_0</Filter> </ClCompile> @@ -191,6 +191,9 @@ <ClCompile Include="Manifest\ManifestValidation.cpp"> <Filter>Manifest</Filter> </ClCompile> + <ClCompile Include="Manifest\YamlParser.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerRepositoryCore/Manifest/Manifest.cpp b/src/AppInstallerRepositoryCore/Manifest/Manifest.cpp @@ -2,277 +2,41 @@ // Licensed under the MIT License. #include "pch.h" #include "Manifest.h" +#include "ManifestValidation.h" namespace AppInstaller::Manifest { - namespace + ManifestVer::ManifestVer(std::string version, bool fullValidation) : Version(std::move(version), ".") { - std::vector<Manifest::string_t> SplitMultiValueField(const std::string& input) - { - if (input.empty()) - { - return {}; - } - - std::vector<Manifest::string_t> result; - size_t currentPos = 0; - while (currentPos < input.size()) - { - size_t splitPos = input.find(',', currentPos); - if (splitPos == std::string::npos) - { - splitPos = input.size(); - } - - std::string splitVal = input.substr(currentPos, splitPos - currentPos); - Utility::Trim(splitVal); - if (!splitVal.empty()) - { - result.emplace_back(std::move(splitVal)); - } - currentPos = splitPos + 1; - } - - return result; - } - } + bool validationSuccess = true; - std::vector<ValidationError> Manifest::PopulateManifestFields(const YAML::Node& rootNode, bool fullValidation) - { - // Detect manifest version first to determine expected fields - // Use index to access ManifestVersion directly. If there're duplicates or other general errors, it'll be detected in later - // processing of iterating the whole manifest. - // Todo: make ManifestVersion required when all manifests in our repo have been updated to contain a ManifestVersion - if (rootNode["ManifestVersion"]) + if (m_parts.size() > 3) { - auto manifestVersionValue = rootNode["ManifestVersion"].as<std::string>(); - ManifestVersion = ManifestVer(manifestVersionValue, fullValidation); + validationSuccess = false; } else { - ManifestVersion = PreviewManifestVersion; - } - - // Check manifest version is supported - if (ManifestVersion.Major() > MaxSupportedMajorVersion) - { - THROW_EXCEPTION_MSG(ManifestException(APPINSTALLER_CLI_ERROR_UNSUPPORTED_MANIFESTVERSION), "Unsupported ManifestVersion: %S", ManifestVersion.ToString().c_str()); - } - - std::vector<ManifestFieldInfo> fieldInfos = - { - { "ManifestVersion", [this](const YAML::Node&) { /* ManifestVersion already processed */ }, false, - // Regex here is to prevent leading 0s in the version, this also keeps consistent with other versions in the manifest - "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){2}$" }, - }; - - YAML::Node switchesNode; - YAML::Node installersNode; - YAML::Node localizationsNode; - - // Todo: The FieldInfo can be a table with an entry specifying which version the field is introduced - // so that we can query applicable fields given a ManifestVersion - if (ManifestVersion >= PreviewManifestVersion) - { - // Add preview fields - std::vector<ManifestFieldInfo> previewFieldInfos = - { - { "Id", [this](const YAML::Node& value) { Id = value.as<std::string>(); Utility::Trim(Id); }, true, "^[\\S]+\\.[\\S]+$" }, - { "Name", [this](const YAML::Node& value) { Name = value.as<std::string>(); Utility::Trim(Name); }, true }, - { "Version", [this](const YAML::Node& value) { Version = value.as<std::string>(); Utility::Trim(Version); }, true, - /* File name chars not allowed */ "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$" }, - { "Publisher", [this](const YAML::Node& value) { Publisher = value.as<std::string>(); }, true }, - { "AppMoniker", [this](const YAML::Node& value) { AppMoniker = value.as<std::string>(); Utility::Trim(AppMoniker); } }, - { "Channel", [this](const YAML::Node& value) { Channel = value.as<std::string>(); Utility::Trim(Channel); } }, - { "Author", [this](const YAML::Node& value) { Author = value.as<std::string>(); } }, - { "License", [this](const YAML::Node& value) { License = value.as<std::string>(); } }, - { "MinOSVersion", [this](const YAML::Node& value) { MinOSVersion = value.as<std::string>(); Utility::Trim(MinOSVersion); }, false, - "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){0,3}$" }, - { "Tags", [this](const YAML::Node& value) { Tags = SplitMultiValueField(value.as<std::string>()); } }, - { "Commands", [this](const YAML::Node& value) { Commands = SplitMultiValueField(value.as<std::string>()); } }, - { "Protocols", [this](const YAML::Node& value) { Protocols = SplitMultiValueField(value.as<std::string>()); } }, - { "FileExtensions", [this](const YAML::Node& value) { FileExtensions = SplitMultiValueField(value.as<std::string>()); } }, - { "InstallerType", [this](const YAML::Node& value) { InstallerType = ManifestInstaller::ConvertToInstallerTypeEnum(value.as<std::string>()); } }, - { "Description", [this](const YAML::Node& value) { Description = value.as<std::string>(); } }, - { "Homepage", [this](const YAML::Node& value) { Homepage = value.as<std::string>(); } }, - { "LicenseUrl", [this](const YAML::Node& value) { LicenseUrl = value.as<std::string>(); } }, - { "Switches", [&](const YAML::Node& value) { switchesNode = value; } }, - { "Installers", [&](const YAML::Node& value) { installersNode = value; }, true }, - { "Localization", [&](const YAML::Node& value) { localizationsNode = value; } }, - }; - - std::move(previewFieldInfos.begin(), previewFieldInfos.end(), std::inserter(fieldInfos, fieldInfos.end())); - } - - std::vector<ValidationError> resultErrors = ValidateAndProcessFields(rootNode, fieldInfos, fullValidation); - - if (!switchesNode.IsNull()) - { - auto errors = ManifestInstaller::PopulateSwitchesFields(switchesNode, this->Switches, fullValidation, ManifestVersion); - std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); - } - - // Create default ManifestInstaller to be used to populate default value when optional fields are not found. - ManifestInstaller defaultInstaller; - defaultInstaller.InstallerType = this->InstallerType; - defaultInstaller.Switches = this->Switches; - - for (std::size_t i = 0; i < installersNode.size(); i++) { - YAML::Node installerNode = installersNode[i]; - ManifestInstaller installer; - auto errors = installer.PopulateInstallerFields(installerNode, defaultInstaller, fullValidation, ManifestVersion); - std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); - this->Installers.emplace_back(std::move(installer)); - } - - // Create default ManifestLocalization to be used to populate default value when optional fields are not found. - ManifestLocalization defaultLocalization; - defaultLocalization.Description = this->Description; - defaultLocalization.Homepage = this->Homepage; - defaultLocalization.LicenseUrl = this->LicenseUrl; - - if (!localizationsNode.IsNull()) - { - for (std::size_t i = 0; i < localizationsNode.size(); i++) { - YAML::Node localizationNode = localizationsNode[i]; - ManifestLocalization localization; - auto errors = localization.PopulateLocalizationFields(localizationNode, defaultLocalization, fullValidation, ManifestVersion); - std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); - this->Localization.emplace_back(std::move(localization)); - } - } - - // Extra semantic validations after basic validation and field population - if (fullValidation) - { - // Channel is not supported currently - if (!Channel.empty()) - { - resultErrors.emplace_back(ManifestError::FieldNotSupported, "Channel", Channel); - } - - try - { - // Version value should be successfully parsed - Utility::Version test{ Version }; - } - catch (const std::exception&) - { - resultErrors.emplace_back(ManifestError::InvalidFieldValue, "Version", Version); - } - - // License field is required - if (License.empty()) - { - resultErrors.emplace_back(ManifestError::RequiredFieldMissing, "License"); - } - - // Check duplicate installer entry. {installerType, arch, language and scope} combination is the key. - // Todo: use the comparator from ManifestComparator when that one is fully implemented. - auto installerCmp = [](const ManifestInstaller& in1, const ManifestInstaller& in2) - { - if (in1.InstallerType != in2.InstallerType) - { - return in1.InstallerType < in2.InstallerType; - } - - if (in1.Arch != in2.Arch) - { - return in1.Arch < in2.Arch; - } - - if (in1.Language != in2.Language) - { - return in1.Language < in2.Language; - } - - if (in1.Scope != in2.Scope) - { - return in1.Scope < in2.Scope; - } - - return false; - }; - - std::set<ManifestInstaller, decltype(installerCmp)> installerSet(installerCmp); - - for (auto const& installer : Installers) + for (size_t i = 0; i < m_parts.size(); i++) { - if (!installerSet.insert(installer).second) + if (!m_parts[i].Other.empty() && + (i < 2 || fullValidation)) { - resultErrors.emplace_back(ManifestError::DuplicateInstallerEntry); + validationSuccess = false; break; } } } - return resultErrors; - } - - Manifest Manifest::CreateFromPath(const std::filesystem::path& inputFile, bool fullValidation, bool throwOnWarning) - { - Manifest manifest; - std::vector<ValidationError> errors; - - try - { - std::ifstream inputStream(inputFile); - YAML::Node rootNode = YAML::Load(inputStream); - errors = manifest.PopulateManifestFields(rootNode, fullValidation); - } - catch (const ManifestException&) - { - // Prevent ManifestException from being wrapped in another ManifestException - throw; - } - catch (const std::exception& e) - { - THROW_EXCEPTION_MSG(ManifestException(), e.what()); - } - - if (!errors.empty()) + if (!validationSuccess) { - ManifestException ex{ std::move(errors) }; - - if (throwOnWarning || !ex.IsWarningOnly()) - { - THROW_EXCEPTION(ex); - } + std::vector<ValidationError> errors; + errors.emplace_back(ManifestError::InvalidFieldValue, "ManifestVersion", m_version); + THROW_EXCEPTION(ManifestException(std::move(errors))); } - - return manifest; } - Manifest Manifest::Create(const std::string& input, bool fullValidation, bool throwOnWarning) + bool ManifestVer::HasTag() const { - Manifest manifest; - std::vector<ValidationError> errors; - - try - { - YAML::Node rootNode = YAML::Load(input); - errors = manifest.PopulateManifestFields(rootNode, fullValidation); - } - catch (const ManifestException&) - { - // Prevent ManifestException from being wrapped in another ManifestException - throw; - } - catch (const std::exception& e) - { - THROW_EXCEPTION_MSG(ManifestException(), e.what()); - } - - if (!errors.empty()) - { - ManifestException ex{ std::move(errors) }; - - if (throwOnWarning || !ex.IsWarningOnly()) - { - THROW_EXCEPTION(ex); - } - } - - return manifest; + return m_parts.size() == 3 && !m_parts[2].Other.empty(); } } diff --git a/src/AppInstallerRepositoryCore/Manifest/Manifest.h b/src/AppInstallerRepositoryCore/Manifest/Manifest.h @@ -3,15 +3,29 @@ #pragma once #include "ManifestInstaller.h" #include "ManifestLocalization.h" -#include "ManifestValidation.h" #include <AppInstallerStrings.h> - -#include <filesystem> -#include <string> +#include <AppInstallerVersions.h> #include <vector> namespace AppInstaller::Manifest { + // ManifestVer is inherited from Utility::Version and is a more restricted version. + // ManifestVer is used to specify the version of app manifest itself. + // ManifestVer is a 3 part version in the format of [0-65535].[0-65535].[0-65535] + // and optionally a following tag in the format of -[SomeString] for experimental purpose. + struct ManifestVer : public Utility::Version + { + ManifestVer() = default; + + ManifestVer(std::string version, bool fullValidation); + + uint64_t Major() const { return m_parts.size() > 0 ? m_parts[0].Integer : 0; } + uint64_t Minor() const { return m_parts.size() > 1 ? m_parts[1].Integer : 0; } + uint64_t Patch() const { return m_parts.size() > 2 ? m_parts[2].Integer : 0; } + + bool HasTag() const; + }; + // Representation of the parsed manifest file. struct Manifest { @@ -66,14 +80,5 @@ namespace AppInstaller::Manifest std::vector<ManifestInstaller> Installers; std::vector<ManifestLocalization> Localization; - - std::vector<ValidationError> PopulateManifestFields(const YAML::Node& rootNode, bool fullValidation); - - // fullValidation: Bool to set if manifest creation should perform extra validation that client does not need. - // e.g. Channel should be null. Client code does not need this check to work properly. - // throwOnWarning: Bool to indicate if an exception should be thrown with only warnings detected in the manifest. - static Manifest CreateFromPath(const std::filesystem::path& inputFile, bool fullValidation = false, bool throwOnWarning = false); - - static Manifest Create(const std::string& input, bool fullValidation = false, bool throwOnWarning = false); }; } \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Manifest/ManifestInstaller.cpp b/src/AppInstallerRepositoryCore/Manifest/ManifestInstaller.cpp @@ -6,143 +6,6 @@ namespace AppInstaller::Manifest { - std::vector<ValidationError> ManifestInstaller::PopulateInstallerFields( - const YAML::Node& installerNode, - const ManifestInstaller& defaultInstaller, - bool fullValidation, - ManifestVer manifestVersion) - { - YAML::Node switchesNode; - this->InstallerType = defaultInstaller.InstallerType; - this->Scope = "user"; - - std::vector<ManifestFieldInfo> fieldInfos; - - if (manifestVersion >= PreviewManifestVersion) - { - std::vector<ManifestFieldInfo> previewFieldInfos = - { - { "Arch", [this](const YAML::Node& value) { Arch = Utility::ConvertToArchitectureEnum(value.as<std::string>()); }, true }, - { "Url", [this](const YAML::Node& value) { Url = value.as<std::string>(); }, true }, - { "Sha256", [this](const YAML::Node& value) { Sha256 = Utility::SHA256::ConvertToBytes(value.as<std::string>()); }, true, "^[A-Fa-f0-9]{64}$" }, - { "SignatureSha256", [this](const YAML::Node& value) { SignatureSha256 = Utility::SHA256::ConvertToBytes(value.as<std::string>()); } }, - { "Language", [this](const YAML::Node& value) { Language = value.as<std::string>(); } }, - { "Scope", [this](const YAML::Node& value) { Scope = value.as<std::string>(); } }, - { "InstallerType", [this](const YAML::Node& value) { InstallerType = ConvertToInstallerTypeEnum(value.as<std::string>()); } }, - { "Switches", [&](const YAML::Node& value) { switchesNode = value; } }, - }; - - std::move(previewFieldInfos.begin(), previewFieldInfos.end(), std::inserter(fieldInfos, fieldInfos.end())); - } - - auto resultErrors = ValidateAndProcessFields(installerNode, fieldInfos, fullValidation); - - // Populate default known switches - this->Switches = GetDefaultKnownSwitches(this->InstallerType); - - // Override with switches from manifest root if applicable - for (auto const& keyValuePair : defaultInstaller.Switches) - { - this->Switches[keyValuePair.first] = keyValuePair.second; - } - - // Override with switches from installer if applicable - if (!switchesNode.IsNull()) - { - auto errors = PopulateSwitchesFields(switchesNode, this->Switches, fullValidation, manifestVersion); - std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); - } - - // Extra semantic validations after basic validation and field population - if (fullValidation) - { - if (Arch == Utility::Architecture::Unknown) - { - resultErrors.emplace_back(ManifestError::InvalidFieldValue, "Arch"); - } - - if (InstallerType == InstallerTypeEnum::Unknown) - { - resultErrors.emplace_back(ManifestError::InvalidFieldValue, "InstallerType"); - } - - if (InstallerType == InstallerTypeEnum::Exe && - (Switches.find(InstallerSwitchType::SilentWithProgress) == Switches.end() || - Switches.find(InstallerSwitchType::Silent) == Switches.end())) - { - resultErrors.emplace_back(ManifestError::ExeInstallerMissingSilentSwitches, ValidationError::Level::Warning); - } - - // Check empty string before calling IsValidUrl to avoid duplicate error reporting. - if (!Url.empty() && IsValidURL(NULL, Utility::ConvertToUTF16(Url).c_str(), 0) == S_FALSE) - { - resultErrors.emplace_back(ManifestError::InvalidFieldValue, "Url", Url); - } - } - - return resultErrors; - } - - std::vector<ValidationError> ManifestInstaller::PopulateSwitchesFields( - const YAML::Node& switchesNode, - std::map<InstallerSwitchType, string_t>& switches, - bool fullValidation, - ManifestVer manifestVersion) - { - std::vector<ManifestFieldInfo> fieldInfos; - - if (manifestVersion >= PreviewManifestVersion) - { - std::vector<ManifestFieldInfo> previewFieldInfos = - { - { "Custom", [&](const YAML::Node& value) { switches[InstallerSwitchType::Custom] = value.as<std::string>(); } }, - { "Silent", [&](const YAML::Node& value) { switches[InstallerSwitchType::Silent] = value.as<std::string>(); } }, - { "SilentWithProgress", [&](const YAML::Node& value) { switches[InstallerSwitchType::SilentWithProgress] = value.as<std::string>(); } }, - { "Interactive", [&](const YAML::Node& value) { switches[InstallerSwitchType::Interactive] = value.as<std::string>(); } }, - { "Language", [&](const YAML::Node& value) { switches[InstallerSwitchType::Language] = value.as<std::string>(); } }, - { "Log", [&](const YAML::Node& value) { switches[InstallerSwitchType::Log] = value.as<std::string>(); } }, - { "InstallLocation", [&](const YAML::Node& value) { switches[InstallerSwitchType::InstallLocation] = value.as<std::string>(); } }, - }; - - std::move(previewFieldInfos.begin(), previewFieldInfos.end(), std::inserter(fieldInfos, fieldInfos.end())); - } - - return ValidateAndProcessFields(switchesNode, fieldInfos, fullValidation); - } - - std::map<ManifestInstaller::InstallerSwitchType, ManifestInstaller::string_t> ManifestInstaller::GetDefaultKnownSwitches(InstallerTypeEnum installerType) - { - switch (installerType) - { - case ManifestInstaller::InstallerTypeEnum::Burn: - case ManifestInstaller::InstallerTypeEnum::Wix: - case ManifestInstaller::InstallerTypeEnum::Msi: - return - { - {InstallerSwitchType::Silent, string_t("/quiet")}, - {InstallerSwitchType::SilentWithProgress, string_t("/passive")}, - {InstallerSwitchType::Log, string_t("/log \"" + std::string(ARG_TOKEN_LOGPATH) + "\"")}, - {InstallerSwitchType::InstallLocation, string_t("TARGETDIR=\"" + std::string(ARG_TOKEN_INSTALLPATH) + "\"")} - }; - case ManifestInstaller::InstallerTypeEnum::Nullsoft: - return - { - {InstallerSwitchType::Silent, string_t("/S")}, - {InstallerSwitchType::SilentWithProgress, string_t("/S")}, - {InstallerSwitchType::InstallLocation, string_t("/D=\"" + std::string(ARG_TOKEN_INSTALLPATH) + "\"")} - }; - case ManifestInstaller::InstallerTypeEnum::Inno: - return - { - {InstallerSwitchType::Silent, string_t("/VERYSILENT")}, - {InstallerSwitchType::SilentWithProgress, string_t("/SILENT")}, - {InstallerSwitchType::Log, string_t("/LOG=\"" + std::string(ARG_TOKEN_LOGPATH) + "\"")}, - {InstallerSwitchType::InstallLocation, string_t("/DIR=\"" + std::string(ARG_TOKEN_INSTALLPATH) + "\"")} - }; - } - return {}; - } - ManifestInstaller::InstallerTypeEnum ManifestInstaller::ConvertToInstallerTypeEnum(const std::string& in) { std::string inStrLower = Utility::ToLower(in); @@ -180,6 +43,10 @@ namespace AppInstaller::Manifest { result = InstallerTypeEnum::Burn; } + else if (inStrLower == "msstore") + { + result = InstallerTypeEnum::MSStore; + } return result; } @@ -214,6 +81,9 @@ namespace AppInstaller::Manifest case ManifestInstaller::InstallerTypeEnum::Burn: result = "Burn"; break; + case ManifestInstaller::InstallerTypeEnum::MSStore: + result = "MSStore"; + break; } return result; diff --git a/src/AppInstallerRepositoryCore/Manifest/ManifestInstaller.h b/src/AppInstallerRepositoryCore/Manifest/ManifestInstaller.h @@ -3,13 +3,10 @@ #pragma once #include <AppInstallerArchitecture.h> #include <AppInstallerStrings.h> -#include <yaml-cpp/yaml.h> #include <string> #include <map> -#include "ManifestValidation.h" - namespace AppInstaller::Manifest { using namespace std::string_view_literals; @@ -32,6 +29,7 @@ namespace AppInstaller::Manifest Msix, Exe, Burn, + MSStore, Unknown }; @@ -65,6 +63,9 @@ namespace AppInstaller::Manifest // Name TBD string_t Scope; + // Store Product Id + string_t ProductId; + // If present, has more precedence than root InstallerTypeEnum InstallerType; @@ -73,24 +74,6 @@ namespace AppInstaller::Manifest static InstallerTypeEnum ConvertToInstallerTypeEnum(const std::string& in); - static std::map<InstallerSwitchType, string_t> GetDefaultKnownSwitches(InstallerTypeEnum installerType); - - // Populates InstallerSwitches - // The value declared in the manifest takes precedence, then value in the manifest root, then default known values. - static std::vector<ValidationError> PopulateSwitchesFields( - const YAML::Node& switchesNode, - std::map<InstallerSwitchType, string_t>& switches, - bool fullValidation, - ManifestVer manifestVersion); - - // Populates ManifestInstaller - // defaultInstaller: if an optional field is not found in the YAML node, the field will be populated with value from defaultInstaller. - std::vector<ValidationError> PopulateInstallerFields( - const YAML::Node& installerNode, - const ManifestInstaller& defaultInstaller, - bool fullValidation, - ManifestVer manifestVersion); - static std::string InstallerTypeToString(InstallerTypeEnum installerType); }; } \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Manifest/ManifestLocalization.cpp b/src/AppInstallerRepositoryCore/Manifest/ManifestLocalization.cpp @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -#include "pch.h" -#include "ManifestLocalization.h" - -namespace AppInstaller::Manifest -{ - std::vector<ValidationError> ManifestLocalization::PopulateLocalizationFields( - const YAML::Node& localizationNode, - const ManifestLocalization& defaultLocalization, - bool fullValidation, - ManifestVer manifestVersion) - { - // Populates default values first - this->Description = defaultLocalization.Description; - this->Homepage = defaultLocalization.Homepage; - this->LicenseUrl = defaultLocalization.LicenseUrl; - - std::vector<ManifestFieldInfo> fieldInfos; - - if (manifestVersion >= PreviewManifestVersion) - { - std::vector<ManifestFieldInfo> previewFieldInfos = - { - { "Language", [this](const YAML::Node& value) { Language = value.as<std::string>(); }, true }, - { "Description", [this](const YAML::Node& value) { Description = value.as<std::string>(); } }, - { "Homepage", [this](const YAML::Node& value) { Homepage = value.as<std::string>(); } }, - { "LicenseUrl", [this](const YAML::Node& value) { LicenseUrl = value.as<std::string>(); } }, - }; - - std::move(previewFieldInfos.begin(), previewFieldInfos.end(), std::inserter(fieldInfos, fieldInfos.end())); - } - - return ValidateAndProcessFields(localizationNode, fieldInfos, fullValidation); - } -} diff --git a/src/AppInstallerRepositoryCore/Manifest/ManifestLocalization.h b/src/AppInstallerRepositoryCore/Manifest/ManifestLocalization.h @@ -1,9 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once -#include <string> - -#include "ManifestValidation.h" +#include <AppInstallerStrings.h> namespace AppInstaller::Manifest { @@ -20,13 +18,5 @@ namespace AppInstaller::Manifest string_t Homepage; string_t LicenseUrl; - - // Populates ManifestLocalization - // defaultLocalization: if an optional field is not found in the YAML node, the field will be populated with value from defaultLocalization. - std::vector<ValidationError> PopulateLocalizationFields( - const YAML::Node& localizationNode, - const ManifestLocalization& defaultLocalization, - bool fullValidation, - ManifestVer manifestVersion); }; } \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Manifest/ManifestValidation.cpp b/src/AppInstallerRepositoryCore/Manifest/ManifestValidation.cpp @@ -6,126 +6,125 @@ namespace AppInstaller::Manifest { - std::vector<ValidationError> ValidateAndProcessFields( - const YAML::Node& rootNode, - const std::vector<ManifestFieldInfo> fieldInfos, - bool fullValidation) + std::vector<ValidationError> ValidateManifest(const Manifest& manifest) { - std::vector<ValidationError> errors; + std::vector<ValidationError> resultErrors; - if (rootNode.size() == 0) + // Channel is not supported currently + if (!manifest.Channel.empty()) { - errors.emplace_back(ManifestError::InvalidRootNode, "", "", rootNode.Mark().line, rootNode.Mark().column); - return errors; + resultErrors.emplace_back(ManifestError::FieldNotSupported, "Channel", manifest.Channel); } - // Keeps track of already processed fields. Used to check duplicate fields or missing required fields. - std::set<std::string> processedFields; + try + { + // Version value should be successfully parsed + Utility::Version test{ manifest.Version }; + } + catch (const std::exception&) + { + resultErrors.emplace_back(ManifestError::InvalidFieldValue, "Version", manifest.Version); + } - for (auto const& keyValuePair : rootNode) + // License field is required + if (manifest.License.empty()) { - std::string key = keyValuePair.first.as<std::string>(); - YAML::Node valueNode = keyValuePair.second; + resultErrors.emplace_back(ManifestError::RequiredFieldMissing, "License"); + } - // We'll do case insensitive search first and validate correct case later. - auto fieldIter = std::find_if(fieldInfos.begin(), fieldInfos.end(), - [&](auto const& s) - { - return Utility::CaseInsensitiveEquals(s.Name, key); - }); + // Comparation function to check duplicate installer entry. {installerType, arch, language and scope} combination is the key. + // Todo: use the comparator from ManifestComparator when that one is fully implemented. + auto installerCmp = [](const ManifestInstaller& in1, const ManifestInstaller& in2) + { + if (in1.InstallerType != in2.InstallerType) + { + return in1.InstallerType < in2.InstallerType; + } - if (fieldIter != fieldInfos.end()) + if (in1.Arch != in2.Arch) { - ManifestFieldInfo fieldInfo = *fieldIter; + return in1.Arch < in2.Arch; + } - // Make sure the found key is in Pascal Case - if (key != fieldInfo.Name) - { - errors.emplace_back(ManifestError::FieldIsNotPascalCase, key, "", keyValuePair.first.Mark().line, keyValuePair.first.Mark().column); - } + if (in1.Language != in2.Language) + { + return in1.Language < in2.Language; + } - // Make sure it's not a duplicate key - if (!processedFields.insert(fieldInfo.Name).second) - { - errors.emplace_back(ManifestError::FieldDuplicate, fieldInfo.Name, "", keyValuePair.first.Mark().line, keyValuePair.first.Mark().column); - } + if (in1.Scope != in2.Scope) + { + return in1.Scope < in2.Scope; + } - // Validate non empty value is provided for required fields - if (fieldInfo.Required) - { - if (!valueNode.IsDefined() || valueNode.IsNull() || // Should be defined and not null - (valueNode.IsScalar() && valueNode.as<std::string>().empty()) || // Scalar type should have content - ((valueNode.IsMap() || valueNode.IsSequence()) && valueNode.size() == 0)) // Map or sequence type should have size greater than 0 - { - errors.emplace_back(ManifestError::RequiredFieldEmpty, fieldInfo.Name, "", valueNode.Mark().line, valueNode.Mark().column); - } - } + return false; + }; - // Validate value against regex if applicable - if (fullValidation && !fieldInfo.RegEx.empty()) - { - std::string value = valueNode.as<std::string>(); - std::regex pattern{ fieldInfo.RegEx }; - if (!std::regex_match(value, pattern)) - { - errors.emplace_back(ManifestError::InvalidFieldValue, fieldInfo.Name, value, valueNode.Mark().line, valueNode.Mark().column); - continue; - } - } + std::set<ManifestInstaller, decltype(installerCmp)> installerSet(installerCmp); + bool duplicateInstallerFound = false; + + // Validate installers + for (auto const& installer : manifest.Installers) + { + if (!duplicateInstallerFound && !installerSet.insert(installer).second) + { + resultErrors.emplace_back(ManifestError::DuplicateInstallerEntry); + duplicateInstallerFound = true; + } - if (!valueNode.IsNull()) + if (installer.Arch == Utility::Architecture::Unknown) + { + resultErrors.emplace_back(ManifestError::InvalidFieldValue, "Arch"); + } + + if (installer.InstallerType == ManifestInstaller::InstallerTypeEnum::Unknown) + { + resultErrors.emplace_back(ManifestError::InvalidFieldValue, "InstallerType"); + } + + if (installer.InstallerType == ManifestInstaller::InstallerTypeEnum::MSStore) + { + // MSStore type is not supported in community repo + resultErrors.emplace_back( + ManifestError::FieldValueNotSupported, "InstallerType", + ManifestInstaller::InstallerTypeToString(installer.InstallerType)); + + if (installer.ProductId.empty()) { - fieldInfo.ProcessFunc(valueNode); + resultErrors.emplace_back(ManifestError::RequiredFieldMissing, "ProductId"); } } else { - // For full validation, also reports unrecognized fields as warning - if (fullValidation) + // For other types, Url and Sha256 are required + if (installer.Url.empty()) + { + resultErrors.emplace_back(ManifestError::RequiredFieldMissing, "Url"); + } + if (installer.Sha256.empty()) { - errors.emplace_back(ManifestError::FieldUnknown, key, "", keyValuePair.first.Mark().line, keyValuePair.first.Mark().column, ValidationError::Level::Warning); + resultErrors.emplace_back(ManifestError::RequiredFieldMissing, "Sha256"); + } + // ProductId should not be used + if (!installer.ProductId.empty()) + { + resultErrors.emplace_back(ManifestError::FieldNotSupported, "ProductId"); } } - } - // Make sure required fields are provided - for (auto const& fieldInfo : fieldInfos) - { - if (fieldInfo.Required && processedFields.find(fieldInfo.Name) == processedFields.end()) + if (installer.InstallerType == ManifestInstaller::InstallerTypeEnum::Exe && + (installer.Switches.find(ManifestInstaller::InstallerSwitchType::SilentWithProgress) == installer.Switches.end() || + installer.Switches.find(ManifestInstaller::InstallerSwitchType::Silent) == installer.Switches.end())) { - errors.emplace_back(ManifestError::RequiredFieldMissing, fieldInfo.Name); + resultErrors.emplace_back(ManifestError::ExeInstallerMissingSilentSwitches, ValidationError::Level::Warning); } - } - - return errors; - } - ManifestVer::ManifestVer(std::string version, bool fullValidation) : Version(std::move(version), ".") - { - bool validationSuccess = true; - - if (m_parts.size() > 3) - { - validationSuccess = false; - } - else - { - for (size_t i = 0; i < m_parts.size(); i++) + // Check empty string before calling IsValidUrl to avoid duplicate error reporting. + if (!installer.Url.empty() && IsValidURL(NULL, Utility::ConvertToUTF16(installer.Url).c_str(), 0) == S_FALSE) { - if (!m_parts[i].Other.empty() && - (i < 2 || fullValidation)) - { - validationSuccess = false; - break; - } + resultErrors.emplace_back(ManifestError::InvalidFieldValue, "Url", installer.Url); } } - if (!validationSuccess) - { - std::vector<ValidationError> errors; - errors.emplace_back(ManifestError::InvalidFieldValue, "ManifestVersion", m_version); - THROW_EXCEPTION(ManifestException(std::move(errors))); - } + return resultErrors; } } \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Manifest/ManifestValidation.h b/src/AppInstallerRepositoryCore/Manifest/ManifestValidation.h @@ -6,29 +6,12 @@ #include <functional> #include <wil/result.h> #include <AppInstallerErrors.h> -#include <AppInstallerVersions.h> +#include "Manifest.h" namespace YAML { class Node; } namespace AppInstaller::Manifest { - // ManifestVer is inherited from Utility::Version and is a more restricted version. - // ManifestVer is used to specify the version of app manifest itself. - // Currently ManifestVer is a 3 part version in the format of [0-65535].[0-65535].[0-65535] - struct ManifestVer : public Utility::Version - { - ManifestVer() = default; - - ManifestVer(std::string version, bool fullValidation); - - uint64_t Major() { return m_parts.size() > 0 ? m_parts[0].Integer : 0; } - uint64_t Minor() { return m_parts.size() > 1 ? m_parts[1].Integer : 0; } - uint64_t Patch() { return m_parts.size() > 2 ? m_parts[2].Integer : 0; } - }; - - static const uint64_t MaxSupportedMajorVersion = 1; - static const ManifestVer PreviewManifestVersion = ManifestVer("0.1.0", false); - namespace ManifestError { const char* const ErrorMessagePrefix = "Manifest Error: "; @@ -43,6 +26,7 @@ namespace AppInstaller::Manifest const char* const InvalidFieldValue = "Invalid field value."; const char* const ExeInstallerMissingSilentSwitches = "Silent and SilentWithProgress switches are not specified for InstallerType exe. Please make sure the installer can run unattended."; const char* const FieldNotSupported = "Field is not supported."; + const char* const FieldValueNotSupported = "Field value is not supported."; const char* const DuplicateInstallerEntry = "Duplicate installer entry found."; } @@ -81,24 +65,6 @@ namespace AppInstaller::Manifest Message(std::move(message)), Field(std::move(field)), Value(std::move(value)), Line(line), Column(column), ErrorLevel(level) {} }; - // This struct contains individual app manifest field info - struct ManifestFieldInfo - { - std::string Name; - std::function<void(const YAML::Node&)> ProcessFunc; - bool Required = false; - std::string RegEx = {}; - }; - - // This method takes YAML root node and list of manifest field info. - // Yaml-cpp does not support case insensitive search and it allows duplicate keys. If duplicate keys exist, - // the value is undefined. So in this method, we will iterate through the node map and process each individual - // pair ourselves. This also helps with generating aggregated error rather than throwing on first failure. - std::vector<ValidationError> ValidateAndProcessFields( - const YAML::Node& rootNode, - const std::vector<ManifestFieldInfo> fieldInfos, - bool fullValidation); - struct ManifestException : public wil::ResultException { ManifestException(std::vector<ValidationError>&& errors = {}, HRESULT hr = APPINSTALLER_CLI_ERROR_MANIFEST_FAILED) : @@ -181,4 +147,6 @@ namespace AppInstaller::Manifest mutable std::string m_manifestErrorMessage; bool m_warningOnly; }; + + std::vector<ValidationError> ValidateManifest(const Manifest& manifest); } \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Manifest/YamlParser.cpp b/src/AppInstallerRepositoryCore/Manifest/YamlParser.cpp @@ -0,0 +1,433 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "YamlParser.h" + +namespace AppInstaller::Manifest +{ + namespace + { + std::vector<Manifest::string_t> SplitMultiValueField(const std::string& input) + { + if (input.empty()) + { + return {}; + } + + std::vector<Manifest::string_t> result; + size_t currentPos = 0; + while (currentPos < input.size()) + { + size_t splitPos = input.find(',', currentPos); + if (splitPos == std::string::npos) + { + splitPos = input.size(); + } + + std::string splitVal = input.substr(currentPos, splitPos - currentPos); + Utility::Trim(splitVal); + if (!splitVal.empty()) + { + result.emplace_back(std::move(splitVal)); + } + currentPos = splitPos + 1; + } + + return result; + } + } + + void YamlParser::PrepareManifestFieldInfos(const ManifestVer& manifestVer) + { + RootFieldInfos = + { + { "ManifestVersion", PreviewManifestVersion, [this](const YAML::Node&) { /* ManifestVersion already processed */ }, false, + // Regex here is to prevent leading 0s in the version, this also keeps consistent with other versions in the manifest + "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){2}$" }, + { "Id", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->Id = value.as<std::string>(); Utility::Trim(m_p_manifest->Id); }, true, "^[\\S]+\\.[\\S]+$" }, + { "Name", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->Name = value.as<std::string>(); Utility::Trim(m_p_manifest->Name); }, true }, + { "Version", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->Version = value.as<std::string>(); Utility::Trim(m_p_manifest->Version); }, true, + /* File name chars not allowed */ "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$" }, + { "Publisher", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->Publisher = value.as<std::string>(); }, true }, + { "AppMoniker", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->AppMoniker = value.as<std::string>(); Utility::Trim(m_p_manifest->AppMoniker); } }, + { "Channel", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->Channel = value.as<std::string>(); Utility::Trim(m_p_manifest->Channel); } }, + { "Author", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->Author = value.as<std::string>(); } }, + { "License", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->License = value.as<std::string>(); } }, + { "MinOSVersion", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->MinOSVersion = value.as<std::string>(); Utility::Trim(m_p_manifest->MinOSVersion); }, false, + "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){0,3}$" }, + { "Tags", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->Tags = SplitMultiValueField(value.as<std::string>()); } }, + { "Commands", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->Commands = SplitMultiValueField(value.as<std::string>()); } }, + { "Protocols", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->Protocols = SplitMultiValueField(value.as<std::string>()); } }, + { "FileExtensions", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->FileExtensions = SplitMultiValueField(value.as<std::string>()); } }, + { "InstallerType", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->InstallerType = ManifestInstaller::ConvertToInstallerTypeEnum(value.as<std::string>()); } }, + { "Description", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->Description = value.as<std::string>(); } }, + { "Homepage", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->Homepage = value.as<std::string>(); } }, + { "LicenseUrl", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->LicenseUrl = value.as<std::string>(); } }, + { "Switches", PreviewManifestVersion, [this](const YAML::Node& value) { *m_p_switchesNode = value; } }, + { "Installers", PreviewManifestVersion, [this](const YAML::Node& value) { *m_p_installersNode = value; }, true }, + { "Localization", PreviewManifestVersion, [this](const YAML::Node& value) { *m_p_localizationsNode = value; } }, + }; + + InstallerFieldInfos = + { + { "Arch", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_installer->Arch = Utility::ConvertToArchitectureEnum(value.as<std::string>()); }, true }, + { "Url", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_installer->Url = value.as<std::string>(); } }, + { "Sha256", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_installer->Sha256 = Utility::SHA256::ConvertToBytes(value.as<std::string>()); }, false, "^[A-Fa-f0-9]{64}$" }, + { "SignatureSha256", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_installer->SignatureSha256 = Utility::SHA256::ConvertToBytes(value.as<std::string>()); }, false, "^[A-Fa-f0-9]{64}$" }, + { "Language", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_installer->Language = value.as<std::string>(); } }, + { "Scope", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_installer->Scope = value.as<std::string>(); } }, + { "InstallerType", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_installer->InstallerType = ManifestInstaller::ConvertToInstallerTypeEnum(value.as<std::string>()); } }, + { "ProductId", PreviewManifestVersionMSStore, [this](const YAML::Node& value) { m_p_installer->ProductId = value.as<std::string>(); } }, + { "Switches", PreviewManifestVersion, [this](const YAML::Node& value) { *m_p_switchesNode = value; } }, + }; + + SwitchesFieldInfos = + { + { "Custom", PreviewManifestVersion, [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::Custom] = value.as<std::string>(); } }, + { "Silent", PreviewManifestVersion, [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::Silent] = value.as<std::string>(); } }, + { "SilentWithProgress", PreviewManifestVersion, [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::SilentWithProgress] = value.as<std::string>(); } }, + { "Interactive", PreviewManifestVersion, [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::Interactive] = value.as<std::string>(); } }, + { "Language", PreviewManifestVersion, [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::Language] = value.as<std::string>(); } }, + { "Log", PreviewManifestVersion, [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::Log] = value.as<std::string>(); } }, + { "InstallLocation", PreviewManifestVersion, [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::InstallLocation] = value.as<std::string>(); } }, + }; + + LocalizationFieldInfos = + { + { "Language", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_localization->Language = value.as<std::string>(); }, true }, + { "Description", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_localization->Description = value.as<std::string>(); } }, + { "Homepage", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_localization->Homepage = value.as<std::string>(); } }, + { "LicenseUrl", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_localization->LicenseUrl = value.as<std::string>(); } }, + }; + + FilterManifestFieldInfos(RootFieldInfos, manifestVer); + FilterManifestFieldInfos(InstallerFieldInfos, manifestVer); + FilterManifestFieldInfos(SwitchesFieldInfos, manifestVer); + FilterManifestFieldInfos(LocalizationFieldInfos, manifestVer); + } + + void YamlParser::FilterManifestFieldInfos( + std::vector<ManifestFieldInfo>& source, + const ManifestVer& manifestVer) + { + auto it = std::remove_if(source.begin(), source.end(), + [&](ManifestFieldInfo field) + { + if (field.VerIntroduced.HasTag()) + { + // Tagged version should have exact match + return field.VerIntroduced != manifestVer; + } + else + { + return manifestVer < field.VerIntroduced; + } + }); + source.erase(it, source.end()); + } + + Manifest YamlParser::CreateFromPath(const std::filesystem::path& inputFile, bool fullValidation, bool throwOnWarning) + { + Manifest manifest; + std::vector<ValidationError> errors; + + try + { + std::ifstream inputStream(inputFile); + YAML::Node rootNode = YAML::Load(inputStream); + YamlParser parser; + errors = parser.ParseManifest(rootNode, manifest, fullValidation); + } + catch (const ManifestException&) + { + // Prevent ManifestException from being wrapped in another ManifestException + throw; + } + catch (const std::exception& e) + { + THROW_EXCEPTION_MSG(ManifestException(), e.what()); + } + + if (!errors.empty()) + { + ManifestException ex{ std::move(errors) }; + + if (throwOnWarning || !ex.IsWarningOnly()) + { + THROW_EXCEPTION(ex); + } + } + + return manifest; + } + + Manifest YamlParser::Create(const std::string& input, bool fullValidation, bool throwOnWarning) + { + Manifest manifest; + std::vector<ValidationError> errors; + + try + { + YAML::Node rootNode = YAML::Load(input); + YamlParser parser; + errors = parser.ParseManifest(rootNode, manifest, fullValidation); + } + catch (const ManifestException&) + { + // Prevent ManifestException from being wrapped in another ManifestException + throw; + } + catch (const std::exception& e) + { + THROW_EXCEPTION_MSG(ManifestException(), e.what()); + } + + if (!errors.empty()) + { + ManifestException ex{ std::move(errors) }; + + if (throwOnWarning || !ex.IsWarningOnly()) + { + THROW_EXCEPTION(ex); + } + } + + return manifest; + } + + std::vector<ValidationError> YamlParser::ParseManifest(const YAML::Node& rootNode, Manifest& manifest, bool fullValidation) + { + // Detect manifest version first to determine expected fields + // Use index to access ManifestVersion directly. If there're duplicates or other general errors, it'll be detected in later + // processing of iterating the whole manifest. + // Todo: make ManifestVersion required when all manifests in our repo have been updated to contain a ManifestVersion + if (rootNode["ManifestVersion"]) + { + auto manifestVersionValue = rootNode["ManifestVersion"].as<std::string>(); + manifest.ManifestVersion = ManifestVer(manifestVersionValue, false); + } + else + { + manifest.ManifestVersion = PreviewManifestVersion; + } + + // Check manifest version is supported + if (manifest.ManifestVersion.Major() > MaxSupportedMajorVersion) + { + THROW_EXCEPTION_MSG(ManifestException(APPINSTALLER_CLI_ERROR_UNSUPPORTED_MANIFESTVERSION), "Unsupported ManifestVersion: %S", manifest.ManifestVersion.ToString().c_str()); + } + + PrepareManifestFieldInfos(manifest.ManifestVersion); + + // Populate root fields + YAML::Node switchesNode; + YAML::Node installersNode; + YAML::Node localizationsNode; + m_p_switchesNode = &switchesNode; + m_p_installersNode = &installersNode; + m_p_localizationsNode = &localizationsNode; + m_p_manifest = &manifest; + auto resultErrors = ValidateAndProcessFields(rootNode, RootFieldInfos, fullValidation); + + // Populate root switches + if (!switchesNode.IsNull()) + { + m_p_switches = &manifest.Switches; + auto errors = ValidateAndProcessFields(switchesNode, SwitchesFieldInfos, fullValidation); + std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); + } + + // Populate installers + for (std::size_t i = 0; i < installersNode.size(); i++) + { + YAML::Node installerNode = installersNode[i]; + ManifestInstaller installer; + YAML::Node installerSwitchesNode; + + // Populate defaults + installer.InstallerType = manifest.InstallerType; + installer.Scope = "user"; + + m_p_installer = &installer; + m_p_switchesNode = &installerSwitchesNode; + auto errors = ValidateAndProcessFields(installerNode, InstallerFieldInfos, fullValidation); + std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); + + // Populate default known switches + installer.Switches = GetDefaultKnownSwitches(installer.InstallerType); + + // Override with switches from manifest root if applicable + for (auto const& keyValuePair : manifest.Switches) + { + installer.Switches[keyValuePair.first] = keyValuePair.second; + } + + // Override with switches from installer declaration if applicable + if (!installerSwitchesNode.IsNull()) + { + m_p_switches = &installer.Switches; + auto switchesErrors = ValidateAndProcessFields(installerSwitchesNode, SwitchesFieldInfos, fullValidation); + std::move(switchesErrors.begin(), switchesErrors.end(), std::inserter(resultErrors, resultErrors.end())); + } + + manifest.Installers.emplace_back(std::move(installer)); + } + + // Populate localization fields + if (!localizationsNode.IsNull()) + { + for (std::size_t i = 0; i < localizationsNode.size(); i++) + { + YAML::Node localizationNode = localizationsNode[i]; + ManifestLocalization localization; + + // Populates default values from root first + localization.Description = manifest.Description; + localization.Homepage = manifest.Homepage; + localization.LicenseUrl = manifest.LicenseUrl; + + m_p_localization = &localization; + auto errors = ValidateAndProcessFields(localizationNode, LocalizationFieldInfos, fullValidation); + std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); + manifest.Localization.emplace_back(std::move(localization)); + } + } + + // Extra semantic validations after basic validation and field population + if (fullValidation) + { + auto errors = ValidateManifest(manifest); + std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); + } + + return resultErrors; + } + + std::vector<ValidationError> YamlParser::ValidateAndProcessFields( + const YAML::Node& rootNode, + const std::vector<ManifestFieldInfo>& fieldInfos, + bool fullValidation) + { + std::vector<ValidationError> errors; + + if (rootNode.size() == 0) + { + errors.emplace_back(ManifestError::InvalidRootNode, "", "", rootNode.Mark().line, rootNode.Mark().column); + return errors; + } + + // Keeps track of already processed fields. Used to check duplicate fields or missing required fields. + std::set<std::string> processedFields; + + for (auto const& keyValuePair : rootNode) + { + std::string key = keyValuePair.first.as<std::string>(); + YAML::Node valueNode = keyValuePair.second; + + // We'll do case insensitive search first and validate correct case later. + auto fieldIter = std::find_if(fieldInfos.begin(), fieldInfos.end(), + [&](auto const& s) + { + return Utility::CaseInsensitiveEquals(s.Name, key); + }); + + if (fieldIter != fieldInfos.end()) + { + ManifestFieldInfo fieldInfo = *fieldIter; + + // Make sure the found key is in Pascal Case + if (key != fieldInfo.Name) + { + errors.emplace_back(ManifestError::FieldIsNotPascalCase, key, "", keyValuePair.first.Mark().line, keyValuePair.first.Mark().column); + } + + // Make sure it's not a duplicate key + if (!processedFields.insert(fieldInfo.Name).second) + { + errors.emplace_back(ManifestError::FieldDuplicate, fieldInfo.Name, "", keyValuePair.first.Mark().line, keyValuePair.first.Mark().column); + } + + // Validate non empty value is provided for required fields + if (fieldInfo.Required) + { + if (!valueNode.IsDefined() || valueNode.IsNull() || // Should be defined and not null + (valueNode.IsScalar() && valueNode.as<std::string>().empty()) || // Scalar type should have content + ((valueNode.IsMap() || valueNode.IsSequence()) && valueNode.size() == 0)) // Map or sequence type should have size greater than 0 + { + errors.emplace_back(ManifestError::RequiredFieldEmpty, fieldInfo.Name, "", valueNode.Mark().line, valueNode.Mark().column); + } + } + + // Validate value against regex if applicable + if (fullValidation && !fieldInfo.RegEx.empty()) + { + std::string value = valueNode.as<std::string>(); + std::regex pattern{ fieldInfo.RegEx }; + if (!std::regex_match(value, pattern)) + { + errors.emplace_back(ManifestError::InvalidFieldValue, fieldInfo.Name, value, valueNode.Mark().line, valueNode.Mark().column); + continue; + } + } + + if (!valueNode.IsNull()) + { + fieldInfo.ProcessFunc(valueNode); + } + } + else + { + // For full validation, also reports unrecognized fields as warning + if (fullValidation) + { + errors.emplace_back(ManifestError::FieldUnknown, key, "", keyValuePair.first.Mark().line, keyValuePair.first.Mark().column, ValidationError::Level::Warning); + } + } + } + + // Make sure required fields are provided + for (auto const& fieldInfo : fieldInfos) + { + if (fieldInfo.Required && processedFields.find(fieldInfo.Name) == processedFields.end()) + { + errors.emplace_back(ManifestError::RequiredFieldMissing, fieldInfo.Name); + } + } + + return errors; + } + + std::map<ManifestInstaller::InstallerSwitchType, ManifestInstaller::string_t> YamlParser::GetDefaultKnownSwitches( + ManifestInstaller::InstallerTypeEnum installerType) + { + switch (installerType) + { + case ManifestInstaller::InstallerTypeEnum::Burn: + case ManifestInstaller::InstallerTypeEnum::Wix: + case ManifestInstaller::InstallerTypeEnum::Msi: + return + { + {ManifestInstaller::InstallerSwitchType::Silent, ManifestInstaller::string_t("/quiet")}, + {ManifestInstaller::InstallerSwitchType::SilentWithProgress, ManifestInstaller::string_t("/passive")}, + {ManifestInstaller::InstallerSwitchType::Log, ManifestInstaller::string_t("/log \"" + std::string(ARG_TOKEN_LOGPATH) + "\"")}, + {ManifestInstaller::InstallerSwitchType::InstallLocation, ManifestInstaller::string_t("TARGETDIR=\"" + std::string(ARG_TOKEN_INSTALLPATH) + "\"")} + }; + case ManifestInstaller::InstallerTypeEnum::Nullsoft: + return + { + {ManifestInstaller::InstallerSwitchType::Silent, ManifestInstaller::string_t("/S")}, + {ManifestInstaller::InstallerSwitchType::SilentWithProgress, ManifestInstaller::string_t("/S")}, + {ManifestInstaller::InstallerSwitchType::InstallLocation, ManifestInstaller::string_t("/D=\"" + std::string(ARG_TOKEN_INSTALLPATH) + "\"")} + }; + case ManifestInstaller::InstallerTypeEnum::Inno: + return + { + {ManifestInstaller::InstallerSwitchType::Silent, ManifestInstaller::string_t("/VERYSILENT")}, + {ManifestInstaller::InstallerSwitchType::SilentWithProgress, ManifestInstaller::string_t("/SILENT")}, + {ManifestInstaller::InstallerSwitchType::Log, ManifestInstaller::string_t("/LOG=\"" + std::string(ARG_TOKEN_LOGPATH) + "\"")}, + {ManifestInstaller::InstallerSwitchType::InstallLocation, ManifestInstaller::string_t("/DIR=\"" + std::string(ARG_TOKEN_INSTALLPATH) + "\"")} + }; + } + return {}; + } +}+ \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Manifest/YamlParser.h b/src/AppInstallerRepositoryCore/Manifest/YamlParser.h @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ManifestValidation.h" +#include "Manifest.h" + +#include <filesystem> + +namespace AppInstaller::Manifest +{ + static const uint64_t MaxSupportedMajorVersion = 1; + static const ManifestVer PreviewManifestVersion = ManifestVer("0.1.0", false); + static const ManifestVer PreviewManifestVersionMSStore = ManifestVer("0.2.0-msstore", false); + + struct YamlParser + { + // fullValidation: Bool to set if manifest creation should perform extra validation that client does not need. + // e.g. Channel should be null. Client code does not need this check to work properly. + // throwOnWarning: Bool to indicate if an exception should be thrown with only warnings detected in the manifest. + static Manifest CreateFromPath(const std::filesystem::path& inputFile, bool fullValidation = false, bool throwOnWarning = false); + + static Manifest Create(const std::string& input, bool fullValidation = false, bool throwOnWarning = false); + + private: + // These pointers are referenced in the processing functions in manifest field info table. + YAML::Node* m_p_installersNode = nullptr; + YAML::Node* m_p_switchesNode = nullptr; + YAML::Node* m_p_localizationsNode = nullptr; + AppInstaller::Manifest::Manifest* m_p_manifest = nullptr; + AppInstaller::Manifest::ManifestInstaller* m_p_installer = nullptr; + std::map<ManifestInstaller::InstallerSwitchType, Utility::NormalizedString>* m_p_switches = nullptr; + AppInstaller::Manifest::ManifestLocalization* m_p_localization = nullptr; + + // This struct contains individual app manifest field info + struct ManifestFieldInfo + { + std::string Name; + ManifestVer VerIntroduced; + std::function<void(const YAML::Node&)> ProcessFunc; + bool Required = false; + std::string RegEx = {}; + }; + + std::vector<ManifestFieldInfo> RootFieldInfos; + std::vector<ManifestFieldInfo> InstallerFieldInfos; + std::vector<ManifestFieldInfo> SwitchesFieldInfos; + std::vector<ManifestFieldInfo> LocalizationFieldInfos; + + std::vector<ValidationError> ParseManifest(const YAML::Node& rootNode, Manifest& manifest, bool fullValidation); + + static std::map<ManifestInstaller::InstallerSwitchType, Utility::NormalizedString> GetDefaultKnownSwitches( + ManifestInstaller::InstallerTypeEnum installerType); + + // This method takes YAML root node and list of manifest field info. + // Yaml-cpp does not support case insensitive search and it allows duplicate keys. If duplicate keys exist, + // the value is undefined. So in this method, we will iterate through the node map and process each individual + // pair ourselves. This also helps with generating aggregated error rather than throwing on first failure. + static std::vector<ValidationError> ValidateAndProcessFields( + const YAML::Node& rootNode, + const std::vector<ManifestFieldInfo>& fieldInfos, + bool fullValidation); + + void PrepareManifestFieldInfos(const ManifestVer& manifestVer); + void FilterManifestFieldInfos(std::vector<ManifestFieldInfo>& source, const ManifestVer& manifestVer); + }; +}+ \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp @@ -2,8 +2,8 @@ // Licensed under the MIT License. #include "pch.h" #include "SQLiteIndex.h" - #include "Schema/MetadataTable.h" +#include "Manifest/YamlParser.h" namespace AppInstaller::Repository::Microsoft { @@ -128,7 +128,7 @@ namespace AppInstaller::Repository::Microsoft { AICLI_LOG(Repo, Info, << "Adding manifest from file [" << manifestPath << "]"); - Manifest::Manifest manifest = Manifest::Manifest::CreateFromPath(manifestPath); + Manifest::Manifest manifest = Manifest::YamlParser::CreateFromPath(manifestPath); AddManifest(manifest, relativePath); } @@ -149,7 +149,7 @@ namespace AppInstaller::Repository::Microsoft { AICLI_LOG(Repo, Info, << "Updating manifest from file [" << manifestPath << "]"); - Manifest::Manifest manifest = Manifest::Manifest::CreateFromPath(manifestPath); + Manifest::Manifest manifest = Manifest::YamlParser::CreateFromPath(manifestPath); return UpdateManifest(manifest, relativePath); } @@ -175,7 +175,7 @@ namespace AppInstaller::Repository::Microsoft { AICLI_LOG(Repo, Info, << "Removing manifest from file [" << manifestPath << "]"); - Manifest::Manifest manifest = Manifest::Manifest::CreateFromPath(manifestPath); + Manifest::Manifest manifest = Manifest::YamlParser::CreateFromPath(manifestPath); RemoveManifest(manifest, relativePath); } diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "Microsoft/SQLiteIndexSource.h" #include "Microsoft/PreIndexedPackageSourceFactory.h" +#include "Manifest/YamlParser.h" namespace AppInstaller::Repository::Microsoft @@ -58,12 +59,12 @@ namespace AppInstaller::Repository::Microsoft std::string manifestContents = manifestStream.str(); AICLI_LOG(Repo, Verbose, << "Manifest contents: " << manifestContents); - return Manifest::Manifest::Create(manifestContents); + return Manifest::YamlParser::Create(manifestContents); } else { AICLI_LOG(Repo, Info, << "Opening manifest from local file: " << fullPath); - return Manifest::Manifest::CreateFromPath(fullPath); + return Manifest::YamlParser::CreateFromPath(fullPath); } } diff --git a/src/WinGetUtil/Exports.cpp b/src/WinGetUtil/Exports.cpp @@ -8,7 +8,7 @@ #include <AppInstallerLogging.h> #include <AppInstallerStrings.h> #include <AppInstallerTelemetry.h> -#include <Manifest/Manifest.h> +#include <Manifest/YamlParser.h> #include <Microsoft/SQLiteIndex.h> using namespace AppInstaller::Utility; @@ -167,7 +167,7 @@ extern "C" try { - (void)Manifest::CreateFromPath(manifestPath, true, true); + (void)YamlParser::CreateFromPath(manifestPath, true, true); *succeeded = TRUE; } catch (const ManifestException& e)