commit 3a4d0071b6d1e3aad1bca0fc8b0f0a59d1c9ba79 parent 9bfe947dab783ccc24960a2bf0cd8f4bd5692201 Author: yao-msft <50888816+yao-msft@users.noreply.github.com> Date: Fri, 3 Sep 2021 17:05:26 -0700 Add support for remaining manifest v1.1 fields (#1427) Diffstat:
42 files changed, 1646 insertions(+), 56 deletions(-)
diff --git a/azure-pipelines.yml b/azure-pipelines.yml @@ -222,7 +222,7 @@ jobs: name: HTTPSDevCert displayName: 'Download Kestrel Certificate' inputs: - secureFile: 'HTTPSDevCert.pfx' + secureFile: 'HTTPSDevCertV2.pfx' - task: MSBuild@1 displayName: Build MSIX Test Installer File diff --git a/schemas/JSON/manifests/v1.0.0/manifest.installer.1.0.0.json b/schemas/JSON/manifests/v1.0.0/manifest.installer.1.0.0.json @@ -138,7 +138,9 @@ "type": "integer", "not": { "enum": [ 0 ] - } + }, + "minimum": -2147483648, + "maximum": 4294967295 }, "maxItems": 16, "uniqueItems": true, diff --git a/schemas/JSON/manifests/v1.0.0/manifest.singleton.1.0.0.json b/schemas/JSON/manifests/v1.0.0/manifest.singleton.1.0.0.json @@ -150,7 +150,9 @@ "type": "integer", "not": { "enum": [ 0 ] - } + }, + "minimum": -2147483648, + "maximum": 4294967295 }, "maxItems": 16, "uniqueItems": true, diff --git a/schemas/JSON/manifests/v1.1.0/manifest.defaultLocale.1.1.0.json b/schemas/JSON/manifests/v1.1.0/manifest.defaultLocale.1.1.0.json @@ -19,7 +19,7 @@ "type": "object", "properties": { "AgreementLabel": { - "type": "string", + "type": [ "string", "null" ], "minLength": 1, "maxLength": 100, "description": "The label of the Agreement. i.e. EULA, AgeRating, etc. This field should be localized. Either Agreement or AgreementUrl is required. When we show the agreements, we would Bold the AgreementLabel" @@ -144,6 +144,16 @@ }, "maxItems": 128 }, + "ReleaseNotes": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 10000, + "description": "The package release notes" + }, + "ReleaseNotesUrl": { + "$ref": "#/definitions/Url", + "description": "The package release notes url" + }, "ManifestType": { "type": "string", "default": "defaultLocale", diff --git a/schemas/JSON/manifests/v1.1.0/manifest.installer.1.1.0.json b/schemas/JSON/manifests/v1.1.0/manifest.installer.1.1.0.json @@ -138,7 +138,9 @@ "type": "integer", "not": { "enum": [ 0 ] - } + }, + "minimum": -2147483648, + "maximum": 4294967295 }, "maxItems": 16, "uniqueItems": true, @@ -275,6 +277,124 @@ "uniqueItems": true, "description": "List of appx or msix installer restricted capabilities" }, + "Market": { + "type": "string", + "pattern": "^[A-Z]{2}$", + "description": "The installer target market" + }, + "MarketArray": { + "type": [ "array", "null" ], + "uniqueItems": true, + "maxItems": 256, + "items": { + "$ref": "#/definitions/Market" + }, + "description": "Array of markets" + }, + "Markets": { + "description": "The installer markets", + "type": [ "object", "null" ], + "oneOf": [ + { + "properties": { + "AllowedMarkets": { + "$ref": "#/definitions/MarketArray" + } + }, + "required": [ "AllowedMarkets" ] + }, + { + "properties": { + "ExcludedMarkets": { + "$ref": "#/definitions/MarketArray" + } + }, + "required": [ "ExcludedMarkets" ] + } + ] + }, + "InstallerAbortsTerminal": { + "type": [ "boolean", "null" ], + "description": "Indicates whether the installer will abort terminal. Default is false" + }, + "ReleaseDate": { + "type": [ "string", "null" ], + "format": "date", + "description": "The installer release date" + }, + "InstallLocationRequired": { + "type": [ "boolean", "null" ], + "description": "Indicates whether the installer requires an install location provided" + }, + "RequireExplicitUpgrade": { + "type": [ "boolean", "null" ], + "description": "Indicates whether the installer should be pinned by default from upgrade" + }, + "UnsupportedOSArchitectures": { + "type": [ "array", "null" ], + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "x86", + "x64", + "arm", + "arm64" + ] + }, + "description": "List of OS architectures the installer does not support" + }, + "AppsAndFeaturesEntry": { + "type": "object", + "properties": { + "DisplayName": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 256, + "description": "The DisplayName registry value" + }, + "Publisher": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 256, + "description": "The Publisher registry value" + }, + "DisplayVersion": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 128, + "description": "The DisplayVersion registry value" + }, + "ProductCode": { + "$ref": "#/definitions/ProductCode" + }, + "UpgradeCode": { + "$ref": "#/definitions/ProductCode" + }, + "InstallerType": { + "$ref": "#/definitions/InstallerType" + } + }, + "description": "Various key values under installer's ARP entry" + }, + "AppsAndFeaturesEntries": { + "type": [ "array", "null" ], + "uniqueItems": true, + "maxItems": 128, + "items": { + "$ref": "#/definitions/AppsAndFeaturesEntry" + }, + "description": "List of ARP entries." + }, + "ElevationRequirement": { + "type": [ "string", "null" ], + "enum": [ + "elevationRequired", + "elevationProhibited", + "elevatesSelf" + ], + "description": "The installer's elevation requirement" + }, "Installer": { "type": "object", "properties": { @@ -355,6 +475,30 @@ }, "RestrictedCapabilities": { "$ref": "#/definitions/RestrictedCapabilities" + }, + "Markets": { + "$ref": "#/definitions/Markets" + }, + "InstallerAbortsTerminal": { + "$ref": "#/definitions/InstallerAbortsTerminal" + }, + "ReleaseDate": { + "$ref": "#/definitions/ReleaseDate" + }, + "InstallLocationRequired": { + "$ref": "#/definitions/InstallLocationRequired" + }, + "RequireExplicitUpgrade": { + "$ref": "#/definitions/RequireExplicitUpgrade" + }, + "UnsupportedOSArchitectures": { + "$ref": "#/definitions/UnsupportedOSArchitectures" + }, + "AppsAndFeaturesEntries": { + "$ref": "#/definitions/AppsAndFeaturesEntries" + }, + "ElevationRequirement": { + "$ref": "#/definitions/ElevationRequirement" } }, "required": [ @@ -426,13 +570,37 @@ "RestrictedCapabilities": { "$ref": "#/definitions/RestrictedCapabilities" }, + "Markets": { + "$ref": "#/definitions/Markets" + }, + "InstallerAbortsTerminal": { + "$ref": "#/definitions/InstallerAbortsTerminal" + }, + "ReleaseDate": { + "$ref": "#/definitions/ReleaseDate" + }, + "InstallLocationRequired": { + "$ref": "#/definitions/InstallLocationRequired" + }, + "RequireExplicitUpgrade": { + "$ref": "#/definitions/RequireExplicitUpgrade" + }, + "UnsupportedOSArchitectures": { + "$ref": "#/definitions/UnsupportedOSArchitectures" + }, + "AppsAndFeaturesEntries": { + "$ref": "#/definitions/AppsAndFeaturesEntries" + }, + "ElevationRequirement": { + "$ref": "#/definitions/ElevationRequirement" + }, "Installers": { "type": "array", "items": { "$ref": "#/definitions/Installer" }, "minItems": 1, - "maxItems": 128 + "maxItems": 1024 }, "ManifestType": { "type": "string", diff --git a/schemas/JSON/manifests/v1.1.0/manifest.locale.1.1.0.json b/schemas/JSON/manifests/v1.1.0/manifest.locale.1.1.0.json @@ -19,7 +19,7 @@ "type": "object", "properties": { "AgreementLabel": { - "type": "string", + "type": [ "string", "null" ], "minLength": 1, "maxLength": 100, "description": "The label of the Agreement. i.e. EULA, AgeRating, etc. This field should be localized. Either Agreement or AgreementUrl is required. When we show the agreements, we would Bold the AgreementLabel" @@ -143,6 +143,16 @@ }, "maxItems": 128 }, + "ReleaseNotes": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 10000, + "description": "The package release notes" + }, + "ReleaseNotesUrl": { + "$ref": "#/definitions/Url", + "description": "The package release notes url" + }, "ManifestType": { "type": "string", "default": "locale", diff --git a/schemas/JSON/manifests/v1.1.0/manifest.singleton.1.1.0.json b/schemas/JSON/manifests/v1.1.0/manifest.singleton.1.1.0.json @@ -37,7 +37,7 @@ "type": "object", "properties": { "AgreementLabel": { - "type": "string", + "type": [ "string", "null" ], "minLength": 1, "maxLength": 100, "description": "The label of the Agreement. i.e. EULA, AgeRating, etc. This field should be localized. Either Agreement or AgreementUrl is required. When we show the agreements, we would Bold the AgreementLabel" @@ -171,7 +171,9 @@ "type": "integer", "not": { "enum": [ 0 ] - } + }, + "minimum": -2147483648, + "maximum": 4294967295 }, "maxItems": 16, "uniqueItems": true, @@ -307,6 +309,124 @@ "uniqueItems": true, "description": "List of appx or msix installer restricted capabilities" }, + "Market": { + "type": "string", + "pattern": "^[A-Z]{2}$", + "description": "The installer target market" + }, + "MarketArray": { + "type": [ "array", "null" ], + "uniqueItems": true, + "maxItems": 256, + "items": { + "$ref": "#/definitions/Market" + }, + "description": "Array of markets" + }, + "Markets": { + "description": "The installer markets", + "type": [ "object", "null" ], + "oneOf": [ + { + "properties": { + "AllowedMarkets": { + "$ref": "#/definitions/MarketArray" + } + }, + "required": [ "AllowedMarkets" ] + }, + { + "properties": { + "ExcludedMarkets": { + "$ref": "#/definitions/MarketArray" + } + }, + "required": [ "ExcludedMarkets" ] + } + ] + }, + "InstallerAbortsTerminal": { + "type": [ "boolean", "null" ], + "description": "Indicates whether the installer will abort terminal. Default is false" + }, + "ReleaseDate": { + "type": [ "string", "null" ], + "format": "date", + "description": "The installer release date" + }, + "InstallLocationRequired": { + "type": [ "boolean", "null" ], + "description": "Indicates whether the installer requires an install location provided" + }, + "RequireExplicitUpgrade": { + "type": [ "boolean", "null" ], + "description": "Indicates whether the installer should be pinned by default from upgrade" + }, + "UnsupportedOSArchitectures": { + "type": [ "array", "null" ], + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "x86", + "x64", + "arm", + "arm64" + ] + }, + "description": "List of OS architectures the installer does not support" + }, + "AppsAndFeaturesEntry": { + "type": "object", + "properties": { + "DisplayName": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 256, + "description": "The DisplayName registry value" + }, + "Publisher": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 256, + "description": "The Publisher registry value" + }, + "DisplayVersion": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 128, + "description": "The DisplayVersion registry value" + }, + "ProductCode": { + "$ref": "#/definitions/ProductCode" + }, + "UpgradeCode": { + "$ref": "#/definitions/ProductCode" + }, + "InstallerType": { + "$ref": "#/definitions/InstallerType" + } + }, + "description": "Various key values under installer's ARP entry" + }, + "AppsAndFeaturesEntries": { + "type": [ "array", "null" ], + "uniqueItems": true, + "maxItems": 128, + "items": { + "$ref": "#/definitions/AppsAndFeaturesEntry" + }, + "description": "List of ARP entries." + }, + "ElevationRequirement": { + "type": [ "string", "null" ], + "enum": [ + "elevationRequired", + "elevationProhibited", + "elevatesSelf" + ], + "description": "The installer's elevation requirement" + }, "Installer": { "type": "object", "properties": { @@ -387,6 +507,30 @@ }, "RestrictedCapabilities": { "$ref": "#/definitions/RestrictedCapabilities" + }, + "Markets": { + "$ref": "#/definitions/Markets" + }, + "InstallerAbortsTerminal": { + "$ref": "#/definitions/InstallerAbortsTerminal" + }, + "ReleaseDate": { + "$ref": "#/definitions/ReleaseDate" + }, + "InstallLocationRequired": { + "$ref": "#/definitions/InstallLocationRequired" + }, + "RequireExplicitUpgrade": { + "$ref": "#/definitions/RequireExplicitUpgrade" + }, + "UnsupportedOSArchitectures": { + "$ref": "#/definitions/UnsupportedOSArchitectures" + }, + "AppsAndFeaturesEntries": { + "$ref": "#/definitions/AppsAndFeaturesEntries" + }, + "ElevationRequirement": { + "$ref": "#/definitions/ElevationRequirement" } }, "required": [ @@ -496,6 +640,16 @@ }, "maxItems": 128 }, + "ReleaseNotes": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 10000, + "description": "The package release notes" + }, + "ReleaseNotesUrl": { + "$ref": "#/definitions/Url", + "description": "The package release notes url" + }, "Channel": { "$ref": "#/definitions/Channel" }, @@ -550,6 +704,30 @@ "RestrictedCapabilities": { "$ref": "#/definitions/RestrictedCapabilities" }, + "Markets": { + "$ref": "#/definitions/Markets" + }, + "InstallerAbortsTerminal": { + "$ref": "#/definitions/InstallerAbortsTerminal" + }, + "ReleaseDate": { + "$ref": "#/definitions/ReleaseDate" + }, + "InstallLocationRequired": { + "$ref": "#/definitions/InstallLocationRequired" + }, + "RequireExplicitUpgrade": { + "$ref": "#/definitions/RequireExplicitUpgrade" + }, + "UnsupportedOSArchitectures": { + "$ref": "#/definitions/UnsupportedOSArchitectures" + }, + "AppsAndFeaturesEntries": { + "$ref": "#/definitions/AppsAndFeaturesEntries" + }, + "ElevationRequirement": { + "$ref": "#/definitions/ElevationRequirement" + }, "Installers": { "type": "array", "items": { diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -194,10 +194,13 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ShowCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelAgreements); WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelAuthor); + WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelCopyright); + WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelCopyrightUrl); WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelDescription); WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelInstaller); WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelInstallerLocale); WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelInstallerProductId); + WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelInstallerReleaseDate); WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelInstallerSha256); WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelInstallerType); WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelInstallerUrl); @@ -205,7 +208,12 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelLicenseUrl); WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelMoniker); WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelPackageUrl); + WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelPrivacyUrl); WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelPublisher); + WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelPublisherSupportUrl); + WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelPublisherUrl); + WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelReleaseNotes); + WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelReleaseNotesUrl); WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelVersion); WINGET_DEFINE_RESOURCE_STRINGID(ShowVersion); WINGET_DEFINE_RESOURCE_STRINGID(SilentArgumentDescription); diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -419,7 +419,6 @@ namespace AppInstaller::CLI::Workflow break; case InstallerTypeEnum::MSStore: context << - EnsureFeatureEnabled(Settings::ExperimentalFeature::Feature::ExperimentalMSStore) << EnsureStorePolicySatisfied << (isUpdate ? MSStoreUpdate : MSStoreInstall); break; diff --git a/src/AppInstallerCLICore/Workflows/ShowFlow.cpp b/src/AppInstallerCLICore/Workflows/ShowFlow.cpp @@ -24,6 +24,16 @@ namespace AppInstaller::CLI::Workflow // TODO: Come up with a prettier format info << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelVersion << " " << manifest.Version << std::endl; info << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelPublisher << " " << manifest.CurrentLocalization.Get<Manifest::Localization::Publisher>() << std::endl; + auto publisherUrl = manifest.CurrentLocalization.Get<Manifest::Localization::PublisherUrl>(); + if (!publisherUrl.empty()) + { + info << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelPublisherUrl << " " << publisherUrl << std::endl; + } + auto publisherSupportUrl = manifest.CurrentLocalization.Get<Manifest::Localization::PublisherSupportUrl>(); + if (!publisherSupportUrl.empty()) + { + info << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelPublisherSupportUrl << " " << publisherSupportUrl << std::endl; + } auto author = manifest.CurrentLocalization.Get<Manifest::Localization::Author>(); if (!author.empty()) { @@ -54,6 +64,31 @@ namespace AppInstaller::CLI::Workflow { info << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelLicenseUrl << " " << licenseUrl << std::endl; } + auto privacyUrl = manifest.CurrentLocalization.Get<Manifest::Localization::PrivacyUrl>(); + if (!privacyUrl.empty()) + { + info << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelPrivacyUrl << " " << privacyUrl << std::endl; + } + auto copyright = manifest.CurrentLocalization.Get<Manifest::Localization::Copyright>(); + if (!copyright.empty()) + { + info << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelCopyright << " " << copyright << std::endl; + } + auto copyrightUrl = manifest.CurrentLocalization.Get<Manifest::Localization::CopyrightUrl>(); + if (!copyrightUrl.empty()) + { + info << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelCopyrightUrl << " " << copyrightUrl << std::endl; + } + auto releaseNotes = manifest.CurrentLocalization.Get<Manifest::Localization::ReleaseNotes>(); + if (!releaseNotes.empty()) + { + info << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelReleaseNotes << " " << releaseNotes << std::endl; + } + auto releaseNotesUrl = manifest.CurrentLocalization.Get<Manifest::Localization::ReleaseNotesUrl>(); + if (!releaseNotesUrl.empty()) + { + info << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelReleaseNotesUrl << " " << releaseNotesUrl << std::endl; + } auto agreements = manifest.CurrentLocalization.Get<Manifest::Localization::Agreements>(); if (!agreements.empty()) { @@ -105,6 +140,10 @@ namespace AppInstaller::CLI::Workflow { info << " " << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelInstallerProductId << " " << installer->ProductId << std::endl; } + if (!installer->ReleaseDate.empty()) + { + info << " " << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelInstallerReleaseDate << " " << installer->ReleaseDate << std::endl; + } if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::Dependencies)) { diff --git a/src/AppInstallerCLIE2ETests/BaseCommand.cs b/src/AppInstallerCLIE2ETests/BaseCommand.cs @@ -58,10 +58,6 @@ namespace AppInstallerCLIE2ETests experimentalArg = status, experimentalCmd = status, experimentalMSStore = status, - list = status, - upgrade = status, - uninstall = status, - packagedAPI = status, } }; diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs @@ -26,10 +26,6 @@ namespace AppInstallerCLIE2ETests public const string TestSourceName = @"TestSource"; public const string TestSourceUrl = @"https://localhost:5001/TestKit"; - // Todo: not needed if switch to use prod index for source tests - public const string IndexPackageRootCert = "IndexPackageIntRoot.cer"; - public const string IndexPackageRootCertThumbprint = "d17697cc206ed26e1a51f5bb96e9356d6d610b74"; - public const string AICLIPackageFamilyName = "WinGetDevCLI_8wekyb3d8bbwe"; public const string AICLIPackageName = "WinGetDevCLI"; public const string AICLIAppId = "WinGetDev"; diff --git a/src/AppInstallerCLIE2ETests/SetUpFixture.cs b/src/AppInstallerCLIE2ETests/SetUpFixture.cs @@ -67,7 +67,6 @@ namespace AppInstallerCLIE2ETests ShouldRevertDefaultFileTypeRiskOnExit = DecreaseFileTypeRisk(".exe;.msi", false); Assert.True(TestCommon.RunCommand("certutil.exe", "-addstore -f \"TRUSTEDPEOPLE\" " + TestCommon.GetTestDataFile(Constants.AppInstallerTestCert)), "Add AppInstallerTestCert"); - Assert.True(TestCommon.RunCommand("certutil.exe", "-addstore -f \"ROOT\" " + TestCommon.GetTestDataFile(Constants.IndexPackageRootCert)), "Add IndexPackageRootCert"); if (TestCommon.PackagedContext) { @@ -114,7 +113,6 @@ namespace AppInstallerCLIE2ETests } TestCommon.RunCommand("certutil.exe", $"-delstore \"TRUSTEDPEOPLE\" {Constants.AppInstallerTestCertThumbprint}"); - TestCommon.RunCommand("certutil.exe", $"-delstore \"ROOT\" {Constants.IndexPackageRootCertThumbprint}"); TestCommon.PublishE2ETestLogs(); diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -1050,4 +1050,28 @@ Do you agree to the terms?</value> <data name="HeaderArgumentNotApplicableWithoutSource" xml:space="preserve"> <value>The optional header is not applicable without specifying a source</value> </data> + <data name="ShowLabelInstallerReleaseDate" xml:space="preserve"> + <value>Release Date:</value> + </data> + <data name="ShowLabelPublisherUrl" xml:space="preserve"> + <value>Publisher Url:</value> + </data> + <data name="ShowLabelPublisherSupportUrl" xml:space="preserve"> + <value>Publisher Support Url:</value> + </data> + <data name="ShowLabelPrivacyUrl" xml:space="preserve"> + <value>Privacy Url:</value> + </data> + <data name="ShowLabelCopyright" xml:space="preserve"> + <value>Copyright:</value> + </data> + <data name="ShowLabelCopyrightUrl" xml:space="preserve"> + <value>Copyright Url:</value> + </data> + <data name="ShowLabelReleaseNotes" xml:space="preserve"> + <value>Release Notes:</value> + </data> + <data name="ShowLabelReleaseNotesUrl" xml:space="preserve"> + <value>Release Notes Url:</value> + </data> </root> \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -484,6 +484,9 @@ <CopyFileToFolders Include="TestData\Manifest-Bad-ProductCodeOnMSIX.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Bad-AppsAndFeaturesEntriesOnMSIX.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\Manifest-Good-SystemReferenceComplex.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> @@ -523,6 +526,9 @@ <CopyFileToFolders Include="TestData\ManifestV1-Singleton.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\ManifestV1_1-Singleton.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\MultiFileManifestV1\ManifestV1-MultiFile-DefaultLocale.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> @@ -535,6 +541,18 @@ <CopyFileToFolders Include="TestData\MultiFileManifestV1\ManifestV1-MultiFile-Version.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\MultiFileManifestV1_1\ManifestV1_1-MultiFile-DefaultLocale.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\MultiFileManifestV1_1\ManifestV1_1-MultiFile-Installer.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\MultiFileManifestV1_1\ManifestV1_1-MultiFile-Locale.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\MultiFileManifestV1_1\ManifestV1_1-MultiFile-Version.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\Installer_Exe_Dependencies.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -19,6 +19,9 @@ <Filter Include="TestData\MultiFileManifestV1"> <UniqueIdentifier>{69fcd25c-e737-4d28-a6d1-39ce491bf293}</UniqueIdentifier> </Filter> + <Filter Include="TestData\MultiFileManifestV1_1"> + <UniqueIdentifier>{81fadc81-4327-4b9e-b588-97155b770aa3}</UniqueIdentifier> + </Filter> </ItemGroup> <ItemGroup> <ClInclude Include="pch.h"> @@ -396,6 +399,9 @@ <CopyFileToFolders Include="TestData\Manifest-Bad-ProductCodeOnMSIX.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Bad-AppsAndFeaturesEntriesOnMSIX.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\Manifest-Bad-InvalidUpdateBehavior.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> @@ -450,6 +456,9 @@ <CopyFileToFolders Include="TestData\ManifestV1-Singleton.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\ManifestV1_1-Singleton.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\MultiFileManifestV1\ManifestV1-MultiFile-DefaultLocale.yaml"> <Filter>TestData\MultiFileManifestV1</Filter> </CopyFileToFolders> @@ -462,6 +471,18 @@ <CopyFileToFolders Include="TestData\MultiFileManifestV1\ManifestV1-MultiFile-Version.yaml"> <Filter>TestData\MultiFileManifestV1</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\MultiFileManifestV1_1\ManifestV1_1-MultiFile-DefaultLocale.yaml"> + <Filter>TestData\MultiFileManifestV1</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\MultiFileManifestV1_1\ManifestV1_1-MultiFile-Installer.yaml"> + <Filter>TestData\MultiFileManifestV1</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\MultiFileManifestV1_1\ManifestV1_1-MultiFile-Locale.yaml"> + <Filter>TestData\MultiFileManifestV1</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\MultiFileManifestV1_1\ManifestV1_1-MultiFile-Version.yaml"> + <Filter>TestData\MultiFileManifestV1</Filter> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\Manifest-Bad-Channel-NotSupported.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> diff --git a/src/AppInstallerCLITests/RestInterface_1_0.cpp b/src/AppInstallerCLITests/RestInterface_1_0.cpp @@ -54,7 +54,6 @@ namespace struct GoodManifest_AllFields { - public: utility::string_t GetSampleManifest_AllFields() { utility::string_t id = L"Foo.Bar"; diff --git a/src/AppInstallerCLITests/RestInterface_1_1.cpp b/src/AppInstallerCLITests/RestInterface_1_1.cpp @@ -31,6 +31,270 @@ namespace return result; } + + struct GoodManifest_AllFields + { + utility::string_t GetSampleManifest_AllFields() + { + utility::string_t id = L"Foo.Bar"; + return _XPLATSTR( + R"delimiter( + { + "Data": { + "PackageIdentifier": "Foo.Bar", + "Versions": [ + { + "PackageVersion": "3.0.0abc", + "DefaultLocale": { + "PackageLocale": "en-US", + "Publisher": "Foo", + "PublisherUrl": "http://publisher.net", + "PublisherSupportUrl": "http://publisherSupport.net", + "PrivacyUrl": "http://packagePrivacyUrl.net", + "Author": "FooBar", + "PackageName": "Bar", + "PackageUrl": "http://packageUrl.net", + "License": "Foo Bar License", + "LicenseUrl": "http://licenseUrl.net", + "Copyright": "Foo Bar Copyright", + "CopyrightUrl": "http://copyrightUrl.net", + "ShortDescription": "Foo bar is a foo bar.", + "Description": "Foo bar is a placeholder.", + "Tags": [ + "FooBar", + "Foo", + "Bar" + ], + "Moniker": "FooBarMoniker", + "ReleaseNotes": "Default release notes", + "ReleaseNotesUrl": "https://DefaultReleaseNotes.net", + "Agreements": [{ + "AgreementLabel": "DefaultLabel", + "Agreement": "DefaultText", + "AgreementUrl": "https://DefaultAgreementUrl.net" + }] + }, + "Channel": "", + "Locales": [ + { + "PackageLocale": "fr-Fr", + "Publisher": "Foo French", + "PublisherUrl": "http://publisher-fr.net", + "PublisherSupportUrl": "http://publisherSupport-fr.net", + "PrivacyUrl": "http://packagePrivacyUrl-fr.net", + "Author": "FooBar French", + "PackageName": "Bar", + "PackageUrl": "http://packageUrl-fr.net", + "License": "Foo Bar License", + "LicenseUrl": "http://licenseUrl-fr.net", + "Copyright": "Foo Bar Copyright", + "CopyrightUrl": "http://copyrightUrl-fr.net", + "ShortDescription": "Foo bar is a foo bar French.", + "Description": "Foo bar is a placeholder French.", + "Tags": [ + "FooBarFr", + "FooFr", + "BarFr" + ], + "ReleaseNotes": "Release notes", + "ReleaseNotesUrl": "https://ReleaseNotes.net", + "Agreements": [{ + "AgreementLabel": "Label", + "Agreement": "Text", + "AgreementUrl": "https://AgreementUrl.net" + }] + } + ], + "Installers": [ + { + "InstallerSha256": "011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6", + "InstallerUrl": "http://foobar.exe", + "Architecture": "x86", + "InstallerLocale": "en-US", + "Platform": [ + "Windows.Desktop" + ], + "MinimumOSVersion": "1078", + "InstallerType": "msi", + "Scope": "user", + "InstallModes": [ + "interactive" + ], + "InstallerSwitches": { + "Silent": "/s", + "SilentWithProgress": "/s", + "Interactive": "/i", + "InstallLocation": "C:\\Users\\User1", + "Log": "/l", + "Upgrade": "/u", + "Custom": "/custom" + }, + "InstallerSuccessCodes": [ + 0 + ], + "UpgradeBehavior": "install", + "Commands": [ + "command1" + ], + "Protocols": [ + "protocol1" + ], + "FileExtensions": [ + ".file-extension" + ], + "Dependencies": { + "WindowsFeatures": [ + "feature1" + ], + "WindowsLibraries": [ + "library1" + ], + "PackageDependencies": [ + { + "PackageIdentifier": "Foo.Baz", + "MinimumVersion": "2.0.0" + } + ], + "ExternalDependencies": [ + "FooBarBaz" + ] + }, + "ProductCode": "5b6e0f8a-3bbf-4a17-aefd-024c2b3e075d", + "ReleaseDate": "2021-01-01", + "InstallerAbortsTerminal": true, + "InstallLocationRequired": true, + "RequireExplicitUpgrade": true, + "UnsupportedOSArchitectures": [ "arm" ], + "ElevationRequirement": "elevatesSelf", + "AppsAndFeaturesEntries": [{ + "DisplayName": "DisplayName", + "DisplayVersion": "DisplayVersion", + "Publisher": "Publisher", + "ProductCode": "ProductCode", + "UpgradeCode": "UpgradeCode", + "InstallerType": "exe" + }], + "Markets" : { + "AllowedMarkets": [ "US" ] + } + } + ] + } + ] + }, + "ContinuationToken": "abcd" + })delimiter"); + } + + void VerifyLocalizations_AllFields(Manifest manifest) + { + REQUIRE(manifest.DefaultLocalization.Locale == "en-US"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Publisher>() == "Foo"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PublisherUrl>() == "http://publisher.net"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PublisherSupportUrl>() == "http://publisherSupport.net"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PrivacyUrl>() == "http://packagePrivacyUrl.net"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Author>() == "FooBar"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PackageName>() == "Bar"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PackageUrl>() == "http://packageUrl.net"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::License>() == "Foo Bar License"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::LicenseUrl>() == "http://licenseUrl.net"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Copyright>() == "Foo Bar Copyright"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::CopyrightUrl>() == "http://copyrightUrl.net"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::ShortDescription>() == "Foo bar is a foo bar."); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Description>() == "Foo bar is a placeholder."); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Tags>().size() == 3); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Tags>().at(0) == "FooBar"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Tags>().at(1) == "Foo"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Tags>().at(2) == "Bar"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::ReleaseNotes>() == "Default release notes"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::ReleaseNotesUrl>() == "https://DefaultReleaseNotes.net"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Agreements>().size() == 1); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Agreements>().at(0).Label == "DefaultLabel"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Agreements>().at(0).AgreementText == "DefaultText"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Agreements>().at(0).AgreementUrl == "https://DefaultAgreementUrl.net"); + + REQUIRE(manifest.Localizations.size() == 1); + ManifestLocalization frenchLocalization = manifest.Localizations.at(0); + REQUIRE(frenchLocalization.Locale == "fr-Fr"); + REQUIRE(frenchLocalization.Get<Localization::Publisher>() == "Foo French"); + REQUIRE(frenchLocalization.Get<Localization::PublisherUrl>() == "http://publisher-fr.net"); + REQUIRE(frenchLocalization.Get<Localization::PublisherSupportUrl>() == "http://publisherSupport-fr.net"); + REQUIRE(frenchLocalization.Get<Localization::PrivacyUrl>() == "http://packagePrivacyUrl-fr.net"); + REQUIRE(frenchLocalization.Get<Localization::Author>() == "FooBar French"); + REQUIRE(frenchLocalization.Get<Localization::PackageName>() == "Bar"); + REQUIRE(frenchLocalization.Get<Localization::PackageUrl>() == "http://packageUrl-fr.net"); + REQUIRE(frenchLocalization.Get<Localization::License>() == "Foo Bar License"); + REQUIRE(frenchLocalization.Get<Localization::LicenseUrl>() == "http://licenseUrl-fr.net"); + REQUIRE(frenchLocalization.Get<Localization::Copyright>() == "Foo Bar Copyright"); + REQUIRE(frenchLocalization.Get<Localization::CopyrightUrl>() == "http://copyrightUrl-fr.net"); + REQUIRE(frenchLocalization.Get<Localization::ShortDescription>() == "Foo bar is a foo bar French."); + REQUIRE(frenchLocalization.Get<Localization::Description>() == "Foo bar is a placeholder French."); + REQUIRE(frenchLocalization.Get<Localization::Tags>().size() == 3); + REQUIRE(frenchLocalization.Get<Localization::Tags>().at(0) == "FooBarFr"); + REQUIRE(frenchLocalization.Get<Localization::Tags>().at(1) == "FooFr"); + REQUIRE(frenchLocalization.Get<Localization::Tags>().at(2) == "BarFr"); + REQUIRE(frenchLocalization.Get<Localization::ReleaseNotes>() == "Release notes"); + REQUIRE(frenchLocalization.Get<Localization::ReleaseNotesUrl>() == "https://ReleaseNotes.net"); + REQUIRE(frenchLocalization.Get<Localization::Agreements>().size() == 1); + REQUIRE(frenchLocalization.Get<Localization::Agreements>().at(0).Label == "Label"); + REQUIRE(frenchLocalization.Get<Localization::Agreements>().at(0).AgreementText == "Text"); + REQUIRE(frenchLocalization.Get<Localization::Agreements>().at(0).AgreementUrl == "https://AgreementUrl.net"); + } + + void VerifyInstallers_AllFields(Manifest manifest) + { + REQUIRE(manifest.Installers.size() == 1); + + ManifestInstaller actualInstaller = manifest.Installers.at(0); + REQUIRE(actualInstaller.Sha256 == AppInstaller::Utility::SHA256::ConvertToBytes("011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6")); + REQUIRE(actualInstaller.Url == "http://foobar.exe"); + REQUIRE(actualInstaller.Arch == Architecture::X86); + REQUIRE(actualInstaller.Locale == "en-US"); + REQUIRE(actualInstaller.Platform.size() == 1); + REQUIRE(actualInstaller.Platform[0] == PlatformEnum::Desktop); + REQUIRE(actualInstaller.MinOSVersion == "1078"); + REQUIRE(actualInstaller.InstallerType == InstallerTypeEnum::Msi); + REQUIRE(actualInstaller.Scope == ScopeEnum::User); + REQUIRE(actualInstaller.InstallModes.size() == 1); + REQUIRE(actualInstaller.InstallModes.at(0) == InstallModeEnum::Interactive); + REQUIRE(actualInstaller.Switches.size() == 7); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::Silent) == "/s"); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::SilentWithProgress) == "/s"); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::Interactive) == "/i"); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::InstallLocation) == "C:\\Users\\User1"); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::Log) == "/l"); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::Update) == "/u"); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::Custom) == "/custom"); + REQUIRE(actualInstaller.InstallerSuccessCodes.size() == 1); + REQUIRE(actualInstaller.InstallerSuccessCodes.at(0) == 0); + REQUIRE(actualInstaller.UpdateBehavior == UpdateBehaviorEnum::Install); + REQUIRE(actualInstaller.Commands.at(0) == "command1"); + REQUIRE(actualInstaller.Protocols.at(0) == "protocol1"); + REQUIRE(actualInstaller.FileExtensions.at(0) == ".file-extension"); + REQUIRE(actualInstaller.Dependencies.HasExactDependency(DependencyType::WindowsFeature, "feature1")); + REQUIRE(actualInstaller.Dependencies.HasExactDependency(DependencyType::WindowsLibrary, "library1")); + REQUIRE(actualInstaller.Dependencies.HasExactDependency(DependencyType::Package, "Foo.Baz", "2.0.0")); + REQUIRE(actualInstaller.Dependencies.HasExactDependency(DependencyType::External, "FooBarBaz")); + REQUIRE(actualInstaller.PackageFamilyName == ""); + REQUIRE(actualInstaller.ProductCode == "5b6e0f8a-3bbf-4a17-aefd-024c2b3e075d"); + REQUIRE(actualInstaller.ReleaseDate == "2021-01-01"); + REQUIRE(actualInstaller.InstallerAbortsTerminal); + REQUIRE(actualInstaller.InstallLocationRequired); + REQUIRE(actualInstaller.RequireExplicitUpgrade); + REQUIRE(actualInstaller.ElevationRequirement == ElevationRequirementEnum::ElevatesSelf); + REQUIRE(actualInstaller.UnsupportedOSArchitectures.size() == 1); + REQUIRE(actualInstaller.UnsupportedOSArchitectures.at(0) == Architecture::Arm); + REQUIRE(actualInstaller.AppsAndFeaturesEntries.size() == 1); + REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).DisplayName == "DisplayName"); + REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).DisplayVersion == "DisplayVersion"); + REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).Publisher == "Publisher"); + REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).ProductCode == "ProductCode"); + REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).UpgradeCode == "UpgradeCode"); + REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).InstallerType == InstallerTypeEnum::Exe); + REQUIRE(actualInstaller.Markets.AllowedMarkets.size() == 1); + REQUIRE(actualInstaller.Markets.AllowedMarkets.at(0) == "US"); + } + }; } TEST_CASE("Search_BadResponse_UnsupportedPackageMatchFields", "[RestSource][Interface_1_1]") @@ -267,4 +531,23 @@ TEST_CASE("GetManifests_GoodResponse_MSStoreType", "[RestSource][Interface_1_1]" REQUIRE(manifest.Installers.size() == 1); REQUIRE(manifest.Installers.at(0).InstallerType == InstallerTypeEnum::MSStore); REQUIRE(manifest.Installers.at(0).ProductId == "9nblggh4nns1"); +} + +TEST_CASE("GetManifests_GoodResponse_V1_1", "[RestSource][Interface_1_0]") +{ + GoodManifest_AllFields sampleManifest; + utility::string_t sample = sampleManifest.GetSampleManifest_AllFields(); + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, std::move(sample)) }; + Interface v1_1{ TestRestUriString, {}, {}, std::move(helper) }; + std::vector<Manifest> manifests = v1_1.GetManifests("Foo.Bar"); + REQUIRE(manifests.size() == 1); + + // Verify manifest is populated + Manifest manifest = manifests[0]; + REQUIRE(manifest.Id == "Foo.Bar"); + REQUIRE(manifest.Version == "3.0.0abc"); + REQUIRE(manifest.Moniker == "FooBarMoniker"); + REQUIRE(manifest.Channel == ""); + sampleManifest.VerifyLocalizations_AllFields(manifest); + sampleManifest.VerifyInstallers_AllFields(manifest); } \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/Manifest-Bad-AppsAndFeaturesEntriesOnMSIX.yaml b/src/AppInstallerCLITests/TestData/Manifest-Bad-AppsAndFeaturesEntriesOnMSIX.yaml @@ -0,0 +1,22 @@ +# Bad manifest. Invalid Arch value +PackageIdentifier: microsoft.msixsdk +PackageName: MSIX SDK +PackageVersion: 1.7.32 +Publisher: Microsoft +License: Test +ShortDescription: desc +PackageLocale: en-US +Installers: + - Architecture: x86 + InstallerType: MSIX + InstallerUrl: https://rubengustorage.blob.core.windows.net/publiccontainer/msixsdk-x86.zip + InstallerSha256: 98B67758CEAFFCBB3FE47838FD0A8D7BD581C2650842D6B2B0E0D49A23270CCD + AppsAndFeaturesEntries: + - DisplayName: DisplayName + DisplayVersion: DisplayVersion + Publisher: Publisher + ProductCode: ProductCode + UpgradeCode: UpgradeCode + +ManifestType: singleton +ManifestVersion: 1.1.0+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/ManifestV1_1-Singleton.yaml b/src/AppInstallerCLITests/TestData/ManifestV1_1-Singleton.yaml @@ -0,0 +1,154 @@ +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.7.32 +PackageLocale: en-US +Publisher: Microsoft +PublisherUrl: https://www.microsoft.com +PublisherSupportUrl: https://www.microsoft.com/support +PrivacyUrl: https://www.microsoft.com/privacy +Author: Microsoft +PackageName: MSIX SDK +PackageUrl: https://www.microsoft.com/msixsdk/home +License: MIT License +LicenseUrl: https://www.microsoft.com/msixsdk/license +Copyright: Copyright Microsoft Corporation +CopyrightUrl: https://www.microsoft.com/msixsdk/copyright +ShortDescription: This is MSIX SDK +Description: The MSIX SDK project is an effort to enable developers +Moniker: msixsdk +Tags: + - "appxsdk" + - "msixsdk" +ReleaseNotes: Default release notes +ReleaseNotesUrl: https://DefaultReleaseNotes.net +Agreements: + - AgreementLabel: DefaultLabel + Agreement: DefaultText + AgreementUrl: https://DefaultAgreementUrl.net +InstallerLocale: en-US +Platform: + - Windows.Desktop + - Windows.Universal +MinimumOSVersion: 10.0.0.0 +InstallerType: zip +Scope: machine +InstallModes: + - interactive + - silent + - silentWithProgress +InstallerSwitches: + Custom: /custom + SilentWithProgress: /silentwithprogress + Silent: /silence + Interactive: /interactive + Log: /log=<LOGPATH> + InstallLocation: /dir=<INSTALLPATH> + Upgrade: /upgrade +InstallerSuccessCodes: + - 1 + - 0x80070005 +UpgradeBehavior: uninstallPrevious +Commands: + - makemsix + - makeappx +Protocols: + - protocol1 + - protocol2 +FileExtensions: + - appx + - msix + - appxbundle + - msixbundle +Dependencies: + WindowsFeatures: + - IIS + WindowsLibraries: + - VC Runtime + PackageDependencies: + - PackageIdentifier: Microsoft.MsixSdkDep + MinimumVersion: 1.0.0 + ExternalDependencies: + - Outside dependencies +Capabilities: + - internetClient +RestrictedCapabilities: + - runFullTrust +PackageFamilyName: Microsoft.DesktopAppInstaller_8wekyb3d8bbwe +ProductCode: "{Foo}" +ReleaseDate: 2021-01-01 +InstallerAbortsTerminal: true +InstallLocationRequired: true +RequireExplicitUpgrade: true +ElevationRequirement: elevatesSelf +UnsupportedOSArchitectures: + - arm +AppsAndFeaturesEntries: + - DisplayName: DisplayName + DisplayVersion: DisplayVersion + Publisher: Publisher + ProductCode: ProductCode + UpgradeCode: UpgradeCode + InstallerType: exe +Markets: + AllowedMarkets: + - US + +Installers: + - Architecture: x86 + InstallerLocale: en-GB + Platform: + - Windows.Desktop + MinimumOSVersion: 10.0.1.0 + InstallerType: msix + InstallerUrl: https://www.microsoft.com/msixsdk/msixsdkx86.msix + InstallerSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + SignatureSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + Scope: user + InstallModes: + - interactive + InstallerSwitches: + Custom: /c + SilentWithProgress: /sp + Silent: /s + Interactive: /i + Log: /l=<LOGPATH> + InstallLocation: /d=<INSTALLPATH> + Upgrade: /u + UpgradeBehavior: install + Commands: + - makemsixPreview + - makeappxPreview + Protocols: + - protocol1preview + - protocol2preview + FileExtensions: + - appxbundle + - msixbundle + - appx + - msix + Dependencies: + WindowsFeatures: + - PreviewIIS + WindowsLibraries: + - Preview VC Runtime + PackageDependencies: + - PackageIdentifier: Microsoft.MsixSdkDepPreview + ExternalDependencies: + - Preview Outside dependencies + PackageFamilyName: Microsoft.DesktopAppInstallerPreview_8wekyb3d8bbwe + Capabilities: + - internetClientPreview + RestrictedCapabilities: + - runFullTrustPreview + ReleaseDate: 2021-02-02 + InstallerAbortsTerminal: false + InstallLocationRequired: false + RequireExplicitUpgrade: false + ElevationRequirement: elevationRequired + UnsupportedOSArchitectures: + - arm64 + Markets: + ExcludedMarkets: + - "US" + +ManifestType: singleton +ManifestVersion: 1.1.0 diff --git a/src/AppInstallerCLITests/TestData/MultiFileManifestV1_1/ManifestV1_1-MultiFile-DefaultLocale.yaml b/src/AppInstallerCLITests/TestData/MultiFileManifestV1_1/ManifestV1_1-MultiFile-DefaultLocale.yaml @@ -0,0 +1,29 @@ +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.7.32 +PackageLocale: en-US +Publisher: Microsoft +PublisherUrl: https://www.microsoft.com +PublisherSupportUrl: https://www.microsoft.com/support +PrivacyUrl: https://www.microsoft.com/privacy +Author: Microsoft +PackageName: MSIX SDK +PackageUrl: https://www.microsoft.com/msixsdk/home +License: MIT License +LicenseUrl: https://www.microsoft.com/msixsdk/license +Copyright: Copyright Microsoft Corporation +CopyrightUrl: https://www.microsoft.com/msixsdk/copyright +ShortDescription: This is MSIX SDK +Description: The MSIX SDK project is an effort to enable developers +Moniker: msixsdk +Tags: + - "appxsdk" + - "msixsdk" +ReleaseNotes: Default release notes +ReleaseNotesUrl: https://DefaultReleaseNotes.net +Agreements: + - AgreementLabel: DefaultLabel + Agreement: DefaultText + AgreementUrl: https://DefaultAgreementUrl.net + +ManifestType: defaultLocale +ManifestVersion: 1.1.0 diff --git a/src/AppInstallerCLITests/TestData/MultiFileManifestV1_1/ManifestV1_1-MultiFile-Installer.yaml b/src/AppInstallerCLITests/TestData/MultiFileManifestV1_1/ManifestV1_1-MultiFile-Installer.yaml @@ -0,0 +1,135 @@ +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.7.32 +InstallerLocale: en-US +Platform: + - Windows.Desktop + - Windows.Universal +MinimumOSVersion: 10.0.0.0 +InstallerType: zip +Scope: machine +InstallModes: + - interactive + - silent + - silentWithProgress +InstallerSwitches: + Custom: /custom + SilentWithProgress: /silentwithprogress + Silent: /silence + Interactive: /interactive + Log: /log=<LOGPATH> + InstallLocation: /dir=<INSTALLPATH> + Upgrade: /upgrade +InstallerSuccessCodes: + - 1 + - 0x80070005 +UpgradeBehavior: uninstallPrevious +Commands: + - makemsix + - makeappx +Protocols: + - protocol1 + - protocol2 +FileExtensions: + - appx + - msix + - appxbundle + - msixbundle +Dependencies: + WindowsFeatures: + - IIS + WindowsLibraries: + - VC Runtime + PackageDependencies: + - PackageIdentifier: Microsoft.MsixSdkDep + MinimumVersion: 1.0.0 + ExternalDependencies: + - Outside dependencies +Capabilities: + - internetClient +RestrictedCapabilities: + - runFullTrust +PackageFamilyName: Microsoft.DesktopAppInstaller_8wekyb3d8bbwe +ProductCode: "{Foo}" +ReleaseDate: 2021-01-01 +InstallerAbortsTerminal: true +InstallLocationRequired: true +RequireExplicitUpgrade: true +ElevationRequirement: elevatesSelf +UnsupportedOSArchitectures: + - arm +AppsAndFeaturesEntries: + - DisplayName: DisplayName + DisplayVersion: DisplayVersion + Publisher: Publisher + ProductCode: ProductCode + UpgradeCode: UpgradeCode + InstallerType: exe +Markets: + AllowedMarkets: + - "US" + +Installers: + - Architecture: x86 + InstallerLocale: en-GB + Platform: + - Windows.Desktop + MinimumOSVersion: 10.0.1.0 + InstallerType: msix + InstallerUrl: https://www.microsoft.com/msixsdk/msixsdkx86.msix + InstallerSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + SignatureSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + Scope: user + InstallModes: + - interactive + InstallerSwitches: + Custom: /c + SilentWithProgress: /sp + Silent: /s + Interactive: /i + Log: /l=<LOGPATH> + InstallLocation: /d=<INSTALLPATH> + Upgrade: /u + UpgradeBehavior: install + Commands: + - makemsixPreview + - makeappxPreview + Protocols: + - protocol1preview + - protocol2preview + FileExtensions: + - appxbundle + - msixbundle + - appx + - msix + Dependencies: + WindowsFeatures: + - PreviewIIS + WindowsLibraries: + - Preview VC Runtime + PackageDependencies: + - PackageIdentifier: Microsoft.MsixSdkDepPreview + ExternalDependencies: + - Preview Outside dependencies + PackageFamilyName: Microsoft.DesktopAppInstallerPreview_8wekyb3d8bbwe + Capabilities: + - internetClientPreview + RestrictedCapabilities: + - runFullTrustPreview + ReleaseDate: 2021-02-02 + InstallerAbortsTerminal: false + InstallLocationRequired: false + RequireExplicitUpgrade: false + ElevationRequirement: elevationRequired + UnsupportedOSArchitectures: + - arm64 + Markets: + ExcludedMarkets: + - "US" + - Architecture: x64 + InstallerType: exe + InstallerUrl: https://www.microsoft.com/msixsdk/msixsdkx64.exe + InstallerSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + ProductCode: "{Bar}" + +ManifestType: installer +ManifestVersion: 1.1.0 diff --git a/src/AppInstallerCLITests/TestData/MultiFileManifestV1_1/ManifestV1_1-MultiFile-Locale.yaml b/src/AppInstallerCLITests/TestData/MultiFileManifestV1_1/ManifestV1_1-MultiFile-Locale.yaml @@ -0,0 +1,28 @@ +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.7.32 +PackageLocale: en-GB +Publisher: Microsoft UK +PublisherUrl: https://www.microsoft.com/UK +PublisherSupportUrl: https://www.microsoft.com/support/UK +PrivacyUrl: https://www.microsoft.com/privacy/UK +Author: Microsoft UK +PackageName: MSIX SDK UK +PackageUrl: https://www.microsoft.com/msixsdk/home/UK +License: MIT License UK +LicenseUrl: https://www.microsoft.com/msixsdk/license/UK +Copyright: Copyright Microsoft Corporation UK +CopyrightUrl: https://www.microsoft.com/msixsdk/copyright/UK +ShortDescription: This is MSIX SDK UK +Description: The MSIX SDK project is an effort to enable developers UK +Tags: + - "appxsdkUK" + - "msixsdkUK" +ReleaseNotes: Release notes +ReleaseNotesUrl: https://ReleaseNotes.net +Agreements: + - AgreementLabel: Label + Agreement: Text + AgreementUrl: https://AgreementUrl.net + +ManifestType: locale +ManifestVersion: 1.1.0 diff --git a/src/AppInstallerCLITests/TestData/MultiFileManifestV1_1/ManifestV1_1-MultiFile-Version.yaml b/src/AppInstallerCLITests/TestData/MultiFileManifestV1_1/ManifestV1_1-MultiFile-Version.yaml @@ -0,0 +1,6 @@ +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.7.32 +DefaultLocale: en-US + +ManifestType: version +ManifestVersion: 1.1.0 diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp @@ -514,10 +514,6 @@ void OverrideForMSStore(TestContext& context, bool isUpdate) } }); } - context.Override({ "EnsureFeatureEnabled", [](TestContext&) - { - } }); - context.Override({ Workflow::EnsureStorePolicySatisfied, [](TestContext&) { } }); diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp @@ -254,6 +254,7 @@ TEST_CASE("ReadBadManifests", "[ManifestValidation]") { "Manifest-Bad-ProductCodeOnMSIX.yaml", "The specified installer type does not support ProductCode. Field: InstallerType Value: Msix" }, { "Manifest-Bad-InvalidUpdateBehavior.yaml", "Invalid field value. Field: UpdateBehavior" }, { "Manifest-Bad-InvalidLocale.yaml", "The locale value is not a well formed bcp47 language tag." }, + { "Manifest-Bad-AppsAndFeaturesEntriesOnMSIX.yaml", "The specified installer type does not write to Apps and Features entry." } }; for (auto const& testCase : TestCases) @@ -339,7 +340,7 @@ void CopyTestDataFilesToFolder(const std::vector<std::string>& testDataFiles, co } } -void VerifyV1ManifestContent(const Manifest& manifest, bool isSingleton) +void VerifyV1ManifestContent(const Manifest& manifest, bool isSingleton, ManifestVer manifestVer = { s_ManifestVersionV1 }) { REQUIRE(manifest.Id == "microsoft.msixsdk"); REQUIRE(manifest.Version == "1.7.32"); @@ -359,6 +360,17 @@ void VerifyV1ManifestContent(const Manifest& manifest, bool isSingleton) REQUIRE(manifest.DefaultLocalization.Get<Localization::Description>() == "The MSIX SDK project is an effort to enable developers"); REQUIRE(manifest.Moniker == "msixsdk"); REQUIRE(manifest.DefaultLocalization.Get<Localization::Tags>() == MultiValue{ "appxsdk", "msixsdk" }); + + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_1 }) + { + REQUIRE(manifest.DefaultLocalization.Get<Localization::ReleaseNotes>() == "Default release notes"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::ReleaseNotesUrl>() == "https://DefaultReleaseNotes.net"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Agreements>().size() == 1); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Agreements>().at(0).Label == "DefaultLabel"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Agreements>().at(0).AgreementText == "DefaultText"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Agreements>().at(0).AgreementUrl == "https://DefaultAgreementUrl.net"); + } + REQUIRE(manifest.DefaultInstallerInfo.Locale == "en-US"); REQUIRE(manifest.DefaultInstallerInfo.Platform == std::vector<PlatformEnum>{ PlatformEnum::Desktop, PlatformEnum::Universal }); REQUIRE(manifest.DefaultInstallerInfo.MinOSVersion == "10.0.0.0"); @@ -393,6 +405,26 @@ void VerifyV1ManifestContent(const Manifest& manifest, bool isSingleton) REQUIRE(manifest.DefaultInstallerInfo.PackageFamilyName == "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe"); REQUIRE(manifest.DefaultInstallerInfo.ProductCode == "{Foo}"); + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_1 }) + { + REQUIRE(manifest.DefaultInstallerInfo.ReleaseDate == "2021-01-01"); + REQUIRE(manifest.DefaultInstallerInfo.InstallerAbortsTerminal); + REQUIRE(manifest.DefaultInstallerInfo.InstallLocationRequired); + REQUIRE(manifest.DefaultInstallerInfo.RequireExplicitUpgrade); + REQUIRE(manifest.DefaultInstallerInfo.ElevationRequirement == ElevationRequirementEnum::ElevatesSelf); + REQUIRE(manifest.DefaultInstallerInfo.UnsupportedOSArchitectures.size() == 1); + REQUIRE(manifest.DefaultInstallerInfo.UnsupportedOSArchitectures.at(0) == Architecture::Arm); + REQUIRE(manifest.DefaultInstallerInfo.AppsAndFeaturesEntries.size() == 1); + REQUIRE(manifest.DefaultInstallerInfo.AppsAndFeaturesEntries.at(0).DisplayName == "DisplayName"); + REQUIRE(manifest.DefaultInstallerInfo.AppsAndFeaturesEntries.at(0).DisplayVersion == "DisplayVersion"); + REQUIRE(manifest.DefaultInstallerInfo.AppsAndFeaturesEntries.at(0).Publisher == "Publisher"); + REQUIRE(manifest.DefaultInstallerInfo.AppsAndFeaturesEntries.at(0).ProductCode == "ProductCode"); + REQUIRE(manifest.DefaultInstallerInfo.AppsAndFeaturesEntries.at(0).UpgradeCode == "UpgradeCode"); + REQUIRE(manifest.DefaultInstallerInfo.AppsAndFeaturesEntries.at(0).InstallerType == InstallerTypeEnum::Exe); + REQUIRE(manifest.DefaultInstallerInfo.Markets.AllowedMarkets.size() == 1); + REQUIRE(manifest.DefaultInstallerInfo.Markets.AllowedMarkets.at(0) == "US"); + } + if (isSingleton) { REQUIRE(manifest.Installers.size() == 1); @@ -439,6 +471,21 @@ void VerifyV1ManifestContent(const Manifest& manifest, bool isSingleton) REQUIRE(installer1.RestrictedCapabilities == MultiValue{ "runFullTrustPreview" }); REQUIRE(installer1.PackageFamilyName == "Microsoft.DesktopAppInstallerPreview_8wekyb3d8bbwe"); + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_1 }) + { + REQUIRE(installer1.ReleaseDate == "2021-02-02"); + REQUIRE_FALSE(installer1.InstallerAbortsTerminal); + REQUIRE_FALSE(installer1.InstallLocationRequired); + REQUIRE_FALSE(installer1.RequireExplicitUpgrade); + REQUIRE(installer1.ElevationRequirement == ElevationRequirementEnum::ElevationRequired); + REQUIRE(installer1.UnsupportedOSArchitectures.size() == 1); + REQUIRE(installer1.UnsupportedOSArchitectures.at(0) == Architecture::Arm64); + REQUIRE(installer1.AppsAndFeaturesEntries.size() == 0); + REQUIRE(installer1.Markets.AllowedMarkets.size() == 0); + REQUIRE(installer1.Markets.ExcludedMarkets.size() == 1); + REQUIRE(installer1.Markets.ExcludedMarkets.at(0) == "US"); + } + if (!isSingleton) { ManifestInstaller installer2 = manifest.Installers.at(1); @@ -448,6 +495,26 @@ void VerifyV1ManifestContent(const Manifest& manifest, bool isSingleton) REQUIRE(installer2.Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); REQUIRE(installer2.ProductCode == "{Bar}"); + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_1 }) + { + REQUIRE(installer2.ReleaseDate == "2021-01-01"); + REQUIRE(installer2.InstallerAbortsTerminal); + REQUIRE(installer2.InstallLocationRequired); + REQUIRE(installer2.RequireExplicitUpgrade); + REQUIRE(installer2.ElevationRequirement == ElevationRequirementEnum::ElevatesSelf); + REQUIRE(installer2.UnsupportedOSArchitectures.size() == 1); + REQUIRE(installer2.UnsupportedOSArchitectures.at(0) == Architecture::Arm); + REQUIRE(installer2.AppsAndFeaturesEntries.size() == 1); + REQUIRE(installer2.AppsAndFeaturesEntries.at(0).DisplayName == "DisplayName"); + REQUIRE(installer2.AppsAndFeaturesEntries.at(0).DisplayVersion == "DisplayVersion"); + REQUIRE(installer2.AppsAndFeaturesEntries.at(0).Publisher == "Publisher"); + REQUIRE(installer2.AppsAndFeaturesEntries.at(0).ProductCode == "ProductCode"); + REQUIRE(installer2.AppsAndFeaturesEntries.at(0).UpgradeCode == "UpgradeCode"); + REQUIRE(installer2.AppsAndFeaturesEntries.at(0).InstallerType == InstallerTypeEnum::Exe); + REQUIRE(installer2.Markets.AllowedMarkets.size() == 1); + REQUIRE(installer2.Markets.AllowedMarkets.at(0) == "US"); + } + // Localization REQUIRE(manifest.Localizations.size() == 1); ManifestLocalization localization1 = manifest.Localizations.at(0); @@ -492,6 +559,29 @@ TEST_CASE("ValidateV1GoodManifestAndVerifyContents", "[ManifestValidation]") VerifyV1ManifestContent(mergedManifest, false); } +TEST_CASE("ValidateV1_1GoodManifestAndVerifyContents", "[ManifestValidation]") +{ + TempDirectory singletonDirectory{ "SingletonManifest" }; + CopyTestDataFilesToFolder({ "ManifestV1_1-Singleton.yaml" }, singletonDirectory); + Manifest singletonManifest = YamlParser::CreateFromPath(singletonDirectory, true, true); + VerifyV1ManifestContent(singletonManifest, true, ManifestVer{s_ManifestVersionV1_1}); + + TempDirectory multiFileDirectory{ "MultiFileManifest" }; + CopyTestDataFilesToFolder({ + "ManifestV1_1-MultiFile-Version.yaml", + "ManifestV1_1-MultiFile-Installer.yaml", + "ManifestV1_1-MultiFile-DefaultLocale.yaml", + "ManifestV1_1-MultiFile-Locale.yaml" }, multiFileDirectory); + + TempFile mergedManifestFile{ "merged.yaml" }; + Manifest multiFileManifest = YamlParser::CreateFromPath(multiFileDirectory, true, true, mergedManifestFile); + VerifyV1ManifestContent(multiFileManifest, false, ManifestVer{ s_ManifestVersionV1_1 }); + + // Read from merged manifest should have the same content as multi file manifest + Manifest mergedManifest = YamlParser::CreateFromPath(mergedManifestFile); + VerifyV1ManifestContent(mergedManifest, false, ManifestVer{ s_ManifestVersionV1_1 }); +} + YamlManifestInfo CreateYamlManifestInfo(std::string testDataFile) { YamlManifestInfo result; diff --git a/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp @@ -230,6 +230,26 @@ namespace AppInstaller::Manifest return result; } + ElevationRequirementEnum ConvertToElevationRequirementEnum(const std::string& in) + { + ElevationRequirementEnum result = ElevationRequirementEnum::Unknown; + + if (Utility::CaseInsensitiveEquals(in, "elevationRequired")) + { + result = ElevationRequirementEnum::ElevationRequired; + } + else if (Utility::CaseInsensitiveEquals(in, "elevationProhibited")) + { + result = ElevationRequirementEnum::ElevationProhibited; + } + else if (Utility::CaseInsensitiveEquals(in, "elevatesSelf")) + { + result = ElevationRequirementEnum::ElevatesSelf; + } + + return result; + } + ManifestTypeEnum ConvertToManifestTypeEnum(const std::string& in) { if (in == "singleton") @@ -319,6 +339,18 @@ namespace AppInstaller::Manifest ); } + bool DoesInstallerTypeWriteAppsAndFeaturesEntry(InstallerTypeEnum installerType) + { + return ( + installerType == InstallerTypeEnum::Exe || + installerType == InstallerTypeEnum::Inno || + installerType == InstallerTypeEnum::Msi || + installerType == InstallerTypeEnum::Nullsoft || + installerType == InstallerTypeEnum::Wix || + installerType == InstallerTypeEnum::Burn + ); + } + bool IsInstallerTypeCompatible(InstallerTypeEnum type1, InstallerTypeEnum type2) { // Unknown type cannot be compatible with any other diff --git a/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp @@ -18,13 +18,17 @@ namespace AppInstaller::Manifest::YamlParser enum class YamlScalarType { String, - Int + Int, + Bool }; // List of fields that use non string scalar types const std::map<std::string_view, YamlScalarType> ManifestFieldTypes= { - { "InstallerSuccessCodes"sv, YamlScalarType::Int } + { "InstallerSuccessCodes"sv, YamlScalarType::Int }, + { "InstallerAbortsTerminal"sv, YamlScalarType::Bool }, + { "InstallLocationRequired"sv, YamlScalarType::Bool }, + { "RequireExplicitUpgrade"sv, YamlScalarType::Bool }, }; YamlScalarType GetManifestScalarValueType(const std::string& key) @@ -44,6 +48,10 @@ namespace AppInstaller::Manifest::YamlParser { return Json::Value(scalarNode.as<int>()); } + else if (scalarType == YamlScalarType::Bool) + { + return Json::Value(scalarNode.as<bool>()); + } else { return Json::Value(scalarNode.as<std::string>()); diff --git a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp @@ -102,6 +102,11 @@ namespace AppInstaller::Manifest resultErrors.emplace_back(ManifestError::InstallerTypeDoesNotSupportProductCode, "InstallerType", InstallerTypeToString(installer.InstallerType)); } + if (!installer.AppsAndFeaturesEntries.empty() && !DoesInstallerTypeWriteAppsAndFeaturesEntry(installer.InstallerType)) + { + resultErrors.emplace_back(ManifestError::InstallerTypeDoesNotWriteAppsAndFeaturesEntry, "InstallerType", InstallerTypeToString(installer.InstallerType)); + } + if (installer.InstallerType == InstallerTypeEnum::MSStore) { if (fullValidation) @@ -152,6 +157,11 @@ namespace AppInstaller::Manifest { resultErrors.emplace_back(ManifestError::InvalidBcp47Value, "InstallerLocale", installer.Locale); } + + if (!installer.Markets.AllowedMarkets.empty() && !installer.Markets.ExcludedMarkets.empty()) + { + resultErrors.emplace_back(ManifestError::BothAllowedAndExcludedMarketsDefined); + } } // Validate localizations diff --git a/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp b/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp @@ -101,6 +101,20 @@ namespace AppInstaller::Manifest return result; } + + std::vector<AppInstaller::Utility::Architecture> ProcessArchitectureSequenceNode(const YAML::Node& node) + { + THROW_HR_IF(E_INVALIDARG, !node.IsSequence()); + + std::vector<AppInstaller::Utility::Architecture> result; + + for (auto const& entry : node.Sequence()) + { + result.emplace_back(Utility::ConvertToArchitectureEnum(entry.as<std::string>())); + } + + return result; + } } std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetRootFieldProcessInfo(const ManifestVer& manifestVersion) @@ -249,6 +263,23 @@ namespace AppInstaller::Manifest std::move(v1InstallerFields.begin(), v1InstallerFields.end(), std::inserter(result, result.end())); } } + + if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_1 }) + { + std::vector<FieldProcessInfo> fields_v1_1 = + { + { "InstallerAbortsTerminal", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->InstallerAbortsTerminal = value.as<bool>(); return {}; } }, + { "InstallLocationRequired", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->InstallLocationRequired = value.as<bool>(); return {}; } }, + { "RequireExplicitUpgrade", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->RequireExplicitUpgrade = value.as<bool>(); return {}; } }, + { "ReleaseDate", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->ReleaseDate = Utility::Trim(value.as<std::string>()); return {}; } }, + { "UnsupportedOSArchitectures", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->UnsupportedOSArchitectures = ProcessArchitectureSequenceNode(value); return {}; } }, + { "ElevationRequirement", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->ElevationRequirement = ConvertToElevationRequirementEnum(value.as<std::string>()); return {}; } }, + { "Markets", [this](const YAML::Node& value)->ValidationErrors { return ProcessMarketsNode(value); } }, + { "AppsAndFeaturesEntries", [this](const YAML::Node& value)->ValidationErrors { return ProcessAppsAndFeaturesEntriesNode(value); } }, + }; + + std::move(fields_v1_1.begin(), fields_v1_1.end(), std::inserter(result, result.end())); + } } return result; @@ -348,6 +379,8 @@ namespace AppInstaller::Manifest std::vector<FieldProcessInfo> fields_v1_1 = { { "Agreements", [this](const YAML::Node& value)->ValidationErrors { return ProcessAgreementsNode(value); } }, + { "ReleaseNotes", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add<Localization::ReleaseNotes>(value.as<std::string>()); return {}; } }, + { "ReleaseNotesUrl", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add<Localization::ReleaseNotesUrl>(value.as<std::string>()); return {}; } }, }; std::move(fields_v1_1.begin(), fields_v1_1.end(), std::inserter(result, result.end())); @@ -417,6 +450,42 @@ namespace AppInstaller::Manifest return result; } + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetMarketsFieldProcessInfo(const ManifestVer& manifestVersion) + { + std::vector<FieldProcessInfo> result = {}; + + if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_1 }) + { + result = + { + { "AllowedMarkets", [this](const YAML::Node& value)->ValidationErrors { m_p_markets->AllowedMarkets = ProcessStringSequenceNode(value); return {}; } }, + { "ExcludedMarkets", [this](const YAML::Node& value)->ValidationErrors { m_p_markets->ExcludedMarkets = ProcessStringSequenceNode(value); return {}; } }, + }; + } + + return result; + } + + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetAppsAndFeaturesEntryFieldProcessInfo(const ManifestVer& manifestVersion) + { + std::vector<FieldProcessInfo> result = {}; + + if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_1 }) + { + result = + { + { "DisplayName", [this](const YAML::Node& value)->ValidationErrors { m_p_appsAndFeaturesEntry->DisplayName = Utility::Trim(value.as<std::string>()); return {}; } }, + { "Publisher", [this](const YAML::Node& value)->ValidationErrors { m_p_appsAndFeaturesEntry->Publisher = Utility::Trim(value.as<std::string>()); return {}; } }, + { "DisplayVersion", [this](const YAML::Node& value)->ValidationErrors { m_p_appsAndFeaturesEntry->DisplayVersion = Utility::Trim(value.as<std::string>()); return {}; } }, + { "ProductCode", [this](const YAML::Node& value)->ValidationErrors { m_p_appsAndFeaturesEntry->ProductCode = Utility::Trim(value.as<std::string>()); return {}; } }, + { "UpgradeCode", [this](const YAML::Node& value)->ValidationErrors { m_p_appsAndFeaturesEntry->UpgradeCode = Utility::Trim(value.as<std::string>()); return {}; } }, + { "InstallerType", [this](const YAML::Node& value)->ValidationErrors { m_p_appsAndFeaturesEntry->InstallerType = ConvertToInstallerTypeEnum(value.as<std::string>()); return {}; } }, + }; + } + + return result; + } + ValidationErrors ManifestYamlPopulator::ValidateAndProcessFields( const YAML::Node& rootNode, const std::vector<FieldProcessInfo>& fieldInfos) @@ -496,7 +565,7 @@ namespace AppInstaller::Manifest m_p_packageDependency = &packageDependency; auto errors = ValidateAndProcessFields(entry, PackageDependenciesFieldInfos); std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); - m_p_dependencyList->Add(std::move(std::move(packageDependency))); + m_p_dependencyList->Add(std::move(packageDependency)); } return resultErrors; @@ -504,6 +573,8 @@ namespace AppInstaller::Manifest std::vector<ValidationError> ManifestYamlPopulator::ProcessAgreementsNode(const YAML::Node& agreementsNode) { + THROW_HR_IF(E_INVALIDARG, !agreementsNode.IsSequence()); + ValidationErrors resultErrors; std::vector<Agreement> agreements; @@ -524,6 +595,36 @@ namespace AppInstaller::Manifest return resultErrors; } + std::vector<ValidationError> ManifestYamlPopulator::ProcessMarketsNode(const YAML::Node& marketsNode) + { + MarketsInfo markets; + m_p_markets = &markets; + auto errors = ValidateAndProcessFields(marketsNode, MarketsFieldInfos); + m_p_installer->Markets = markets; + return errors; + } + + std::vector<ValidationError> ManifestYamlPopulator::ProcessAppsAndFeaturesEntriesNode(const YAML::Node& appsAndFeaturesEntriesNode) + { + THROW_HR_IF(E_INVALIDARG, !appsAndFeaturesEntriesNode.IsSequence()); + + ValidationErrors resultErrors; + std::vector<AppsAndFeaturesEntry> appsAndFeaturesEntries; + + for (auto const& entry : appsAndFeaturesEntriesNode.Sequence()) + { + AppsAndFeaturesEntry appsAndFeaturesEntry; + m_p_appsAndFeaturesEntry = &appsAndFeaturesEntry; + auto errors = ValidateAndProcessFields(entry, AppsAndFeaturesEntryFieldInfos); + std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); + appsAndFeaturesEntries.emplace_back(std::move(appsAndFeaturesEntry)); + } + + m_p_installer->AppsAndFeaturesEntries = appsAndFeaturesEntries; + + return resultErrors; + } + ValidationErrors ManifestYamlPopulator::PopulateManifestInternal(const YAML::Node& rootNode, Manifest& manifest, const ManifestVer& manifestVersion, bool fullValidation) { m_fullValidation = fullValidation; @@ -540,6 +641,8 @@ namespace AppInstaller::Manifest PackageDependenciesFieldInfos = GetPackageDependenciesFieldProcessInfo(manifestVersion); LocalizationFieldInfos = GetLocalizationFieldProcessInfo(manifestVersion); AgreementFieldInfos = GetAgreementFieldProcessInfo(manifestVersion); + MarketsFieldInfos = GetMarketsFieldProcessInfo(manifestVersion); + AppsAndFeaturesEntryFieldInfos = GetAppsAndFeaturesEntryFieldProcessInfo(manifestVersion); // Populate root m_p_manifest = &manifest; @@ -557,9 +660,10 @@ namespace AppInstaller::Manifest { ManifestInstaller installer = manifest.DefaultInstallerInfo; - // Clear these defaults as PackageFamilyName and ProductCode needs to be copied based on InstallerType + // Clear these defaults as PackageFamilyName, ProductCode, AppsAndFeaturesEntries need to be copied based on InstallerType installer.PackageFamilyName.clear(); installer.ProductCode.clear(); + installer.AppsAndFeaturesEntries.clear(); // Clear dependencies as installer overrides root dependencies installer.Dependencies.Clear(); @@ -578,6 +682,11 @@ namespace AppInstaller::Manifest installer.ProductCode = manifest.DefaultInstallerInfo.ProductCode; } + if (installer.AppsAndFeaturesEntries.empty() && DoesInstallerTypeWriteAppsAndFeaturesEntry(installer.InstallerType)) + { + installer.AppsAndFeaturesEntries = manifest.DefaultInstallerInfo.AppsAndFeaturesEntries; + } + // If there are no dependencies on installer use default ones if (!installer.Dependencies.HasAny()) { diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h @@ -104,6 +104,14 @@ namespace AppInstaller::Manifest Desktop, }; + enum class ElevationRequirementEnum + { + Unknown, + ElevationRequired, + ElevationProhibited, + ElevatesSelf, + }; + enum class ManifestTypeEnum { Singleton, @@ -134,6 +142,22 @@ namespace AppInstaller::Manifest Dependency(DependencyType type) : Type(type) {} }; + struct AppsAndFeaturesEntry + { + string_t DisplayName; + string_t Publisher; + string_t DisplayVersion; + string_t ProductCode; + string_t UpgradeCode; + InstallerTypeEnum InstallerType = InstallerTypeEnum::Unknown; + }; + + struct MarketsInfo + { + std::vector<string_t> AllowedMarkets; + std::vector<string_t> ExcludedMarkets; + }; + struct DependencyList { DependencyList() = default; @@ -245,6 +269,8 @@ namespace AppInstaller::Manifest PlatformEnum ConvertToPlatformEnum(const std::string& in); + ElevationRequirementEnum ConvertToElevationRequirementEnum(const std::string& in); + ManifestTypeEnum ConvertToManifestTypeEnum(const std::string& in); std::string_view InstallerTypeToString(InstallerTypeEnum installerType); @@ -257,6 +283,9 @@ namespace AppInstaller::Manifest // Gets a value indicating whether the given installer type uses the ProductCode system reference. bool DoesInstallerTypeUseProductCode(InstallerTypeEnum installerType); + // Gets a value indicating whether the given installer type writes ARP entry. + bool DoesInstallerTypeWriteAppsAndFeaturesEntry(InstallerTypeEnum installerType); + // Checks whether 2 installer types are compatible. E.g. inno and exe are update compatible bool IsInstallerTypeCompatible(InstallerTypeEnum type1, InstallerTypeEnum type2); diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h b/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h @@ -72,5 +72,21 @@ namespace AppInstaller::Manifest std::vector<string_t> RestrictedCapabilities; DependencyList Dependencies; + + bool InstallerAbortsTerminal = false; + + string_t ReleaseDate; + + bool InstallLocationRequired = false; + + bool RequireExplicitUpgrade = false; + + std::vector<AppInstaller::Utility::Architecture> UnsupportedOSArchitectures; + + std::vector<AppsAndFeaturesEntry> AppsAndFeaturesEntries; + + ElevationRequirementEnum ElevationRequirement = ElevationRequirementEnum::Unknown; + + MarketsInfo Markets; }; } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestLocalization.h b/src/AppInstallerCommonCore/Public/winget/ManifestLocalization.h @@ -26,6 +26,8 @@ namespace AppInstaller::Manifest Description, Tags, Agreements, + ReleaseNotes, + ReleaseNotesUrl, Max }; diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h b/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h @@ -31,6 +31,7 @@ namespace AppInstaller::Manifest const char* const DuplicateInstallerEntry = "Duplicate installer entry found."; const char* const InstallerTypeDoesNotSupportPackageFamilyName = "The specified installer type does not support PackageFamilyName."; const char* const InstallerTypeDoesNotSupportProductCode = "The specified installer type does not support ProductCode."; + const char* const InstallerTypeDoesNotWriteAppsAndFeaturesEntry = "The specified installer type does not write to Apps and Features entry."; const char* const IncompleteMultiFileManifest = "The multi file manifest is incomplete. A multi file manifest must contain at least version, installer and defaultLocale manifest."; const char* const InconsistentMultiFileManifestFieldValue = "The multi file manifest has inconsistent field values."; const char* const DuplicateMultiFileManifestType = "The multi file manifest should contain only one file with the particular ManifestType."; @@ -39,6 +40,7 @@ namespace AppInstaller::Manifest const char* const InconsistentMultiFileManifestDefaultLocale = "DefaultLocale value in version manifest does not match PackageLocale value in defaultLocale manifest."; const char* const FieldFailedToProcess = "Failed to process field."; const char* const InvalidBcp47Value = "The locale value is not a well formed bcp47 language tag."; + const char* const BothAllowedAndExcludedMarketsDefined = "Both AllowedMarkets and ExcludedMarkets defined."; } struct ValidationError diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestYamlPopulator.h b/src/AppInstallerCommonCore/Public/winget/ManifestYamlPopulator.h @@ -33,6 +33,8 @@ namespace AppInstaller::Manifest std::vector<FieldProcessInfo> PackageDependenciesFieldInfos; std::vector<FieldProcessInfo> LocalizationFieldInfos; std::vector<FieldProcessInfo> AgreementFieldInfos; + std::vector<FieldProcessInfo> MarketsFieldInfos; + std::vector<FieldProcessInfo> AppsAndFeaturesEntryFieldInfos; // These pointers are referenced in the processing functions in manifest field process info table. AppInstaller::Manifest::Manifest* m_p_manifest = nullptr; @@ -42,6 +44,8 @@ namespace AppInstaller::Manifest AppInstaller::Manifest::Dependency* m_p_packageDependency = nullptr; AppInstaller::Manifest::ManifestLocalization* m_p_localization = nullptr; AppInstaller::Manifest::Agreement* m_p_agreement = nullptr; + AppInstaller::Manifest::MarketsInfo* m_p_markets = nullptr; + AppInstaller::Manifest::AppsAndFeaturesEntry* m_p_appsAndFeaturesEntry = nullptr; // Cache of Installers node and Localization node YAML::Node const* m_p_installersNode = nullptr; @@ -54,6 +58,8 @@ namespace AppInstaller::Manifest std::vector<FieldProcessInfo> GetPackageDependenciesFieldProcessInfo(const ManifestVer& manifestVersion); std::vector<FieldProcessInfo> GetLocalizationFieldProcessInfo(const ManifestVer& manifestVersion, bool forRootFields = false); std::vector<FieldProcessInfo> GetAgreementFieldProcessInfo(const ManifestVer& manifestVersion); + std::vector<FieldProcessInfo> GetMarketsFieldProcessInfo(const ManifestVer& manifestVersion); + std::vector<FieldProcessInfo> GetAppsAndFeaturesEntryFieldProcessInfo(const ManifestVer& manifestVersion); // This method takes YAML root node and list of manifest field info. // Yaml lib does not support case insensitive search and it allows duplicate keys. If duplicate keys exist, @@ -66,6 +72,8 @@ namespace AppInstaller::Manifest void ProcessDependenciesNode(DependencyType type, const YAML::Node& rootNode); std::vector<ValidationError> ProcessPackageDependenciesNode(const YAML::Node& rootNode); std::vector<ValidationError> ProcessAgreementsNode(const YAML::Node& agreementsNode); + std::vector<ValidationError> ProcessMarketsNode(const YAML::Node& marketsNode); + std::vector<ValidationError> ProcessAppsAndFeaturesEntriesNode(const YAML::Node& appsAndFeaturesEntriesNode); std::vector<ValidationError> PopulateManifestInternal(const YAML::Node& rootNode, Manifest& manifest, const ManifestVer& manifestVersion, bool fullValidation); }; diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer.h @@ -3,6 +3,7 @@ #pragma once #include <winget/Manifest.h> #include <cpprest/json.h> +#include "Rest\Schema\JsonHelper.h" namespace AppInstaller::Repository::Rest::Schema::V1_0::Json { @@ -13,14 +14,28 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json std::vector<Manifest::Manifest> Deserialize(const web::json::value& dataJsonObject) const; protected: + + template <Manifest::Localization L> + inline void TryParseStringLocaleField(Manifest::ManifestLocalization& manifestLocale, const web::json::value& localeJsonObject, std::string_view localeJsonFieldName) const + { + auto value = JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(localeJsonFieldName)); + + if (JsonHelper::IsValidNonEmptyStringValue(value)) + { + manifestLocale.Add<L>(value.value()); + } + } + std::optional<std::vector<Manifest::Manifest>> DeserializeVersion(const web::json::value& dataJsonObject) const; - std::optional<Manifest::ManifestLocalization> DeserializeLocale(const web::json::value& localeJsonObject) const; + virtual std::optional<Manifest::ManifestLocalization> DeserializeLocale(const web::json::value& localeJsonObject) const; virtual std::optional<Manifest::ManifestInstaller> DeserializeInstaller(const web::json::value& installerJsonObject) const; std::optional<Manifest::DependencyList> DeserializeDependency(const web::json::value& dependenciesJsonObject) const; virtual Manifest::InstallerTypeEnum ConvertToInstallerType(std::string_view in) const; + + std::vector<Manifest::string_t> ConvertToManifestStringArray(const std::vector<std::string>& values) const; }; } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer_1_0.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer_1_0.cpp @@ -83,28 +83,6 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json constexpr std::string_view Capabilities = "Capabilities"sv; constexpr std::string_view RestrictedCapabilities = "RestrictedCapabilities"sv; - std::vector<Manifest::string_t> ConvertToManifestStringArray(const std::vector<std::string>& values) - { - std::vector<Manifest::string_t> result; - for (const auto& value : values) - { - result.emplace_back(value); - } - - return result; - } - - template <Manifest::Localization L> - void TryParseStringLocaleField(Manifest::ManifestLocalization& manifestLocale, const web::json::value& localeJsonObject, std::string_view localeJsonFieldName) - { - auto value = JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(localeJsonFieldName)); - - if (JsonHelper::IsValidNonEmptyStringValue(value)) - { - manifestLocale.Add<L>(value.value()); - } - } - void TryParseInstallerSwitchField( std::map<InstallerSwitchType, Utility::NormalizedString>& installerSwitches, InstallerSwitchType switchType, @@ -524,4 +502,15 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json return InstallerTypeEnum::Unknown; } + + std::vector<Manifest::string_t> ManifestDeserializer::ConvertToManifestStringArray(const std::vector<std::string>& values) const + { + std::vector<Manifest::string_t> result; + for (const auto& value : values) + { + result.emplace_back(value); + } + + return result; + } } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_1/Json/ManifestDeserializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_1/Json/ManifestDeserializer.h @@ -8,10 +8,11 @@ namespace AppInstaller::Repository::Rest::Schema::V1_1::Json // Manifest Deserializer. struct ManifestDeserializer : public V1_0::Json::ManifestDeserializer { - // TODO: override DeserializeLocale, DeserializeInstaller accordingly to add new v1.1 fields protected: std::optional<Manifest::ManifestInstaller> DeserializeInstaller(const web::json::value& installerJsonObject) const override; + std::optional<Manifest::ManifestLocalization> DeserializeLocale(const web::json::value& localeJsonObject) const override; + Manifest::InstallerTypeEnum ConvertToInstallerType(std::string_view in) const override; }; } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_1/Json/ManifestDeserializer_1_1.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_1/Json/ManifestDeserializer_1_1.cpp @@ -12,6 +12,30 @@ namespace AppInstaller::Repository::Rest::Schema::V1_1::Json { // Installer constexpr std::string_view MSStoreProductIdentifier = "MSStoreProductIdentifier"sv; + constexpr std::string_view ReleaseDate = "ReleaseDate"sv; + constexpr std::string_view InstallerAbortsTerminal = "InstallerAbortsTerminal"sv; + constexpr std::string_view InstallLocationRequired = "InstallLocationRequired"sv; + constexpr std::string_view RequireExplicitUpgrade = "RequireExplicitUpgrade"sv; + constexpr std::string_view UnsupportedOSArchitectures = "UnsupportedOSArchitectures"sv; + constexpr std::string_view AppsAndFeaturesEntries = "AppsAndFeaturesEntries"sv; + constexpr std::string_view DisplayName = "DisplayName"sv; + constexpr std::string_view Publisher = "Publisher"sv; + constexpr std::string_view DisplayVersion = "DisplayVersion"sv; + constexpr std::string_view ProductCode = "ProductCode"sv; + constexpr std::string_view UpgradeCode = "UpgradeCode"sv; + constexpr std::string_view InstallerType = "InstallerType"sv; + constexpr std::string_view Markets = "Markets"sv; + constexpr std::string_view AllowedMarkets = "AllowedMarkets"sv; + constexpr std::string_view ExcludedMarkets = "ExcludedMarkets"sv; + constexpr std::string_view ElevationRequirement = "ElevationRequirement"sv; + + // Locale + constexpr std::string_view ReleaseNotes = "ReleaseNotes"sv; + constexpr std::string_view ReleaseNotesUrl = "ReleaseNotesUrl"sv; + constexpr std::string_view Agreements = "Agreements"sv; + constexpr std::string_view AgreementLabel = "AgreementLabel"sv; + constexpr std::string_view Agreement = "Agreement"sv; + constexpr std::string_view AgreementUrl = "AgreementUrl"sv; } Manifest::InstallerTypeEnum ManifestDeserializer::ConvertToInstallerType(std::string_view in) const @@ -35,6 +59,110 @@ namespace AppInstaller::Repository::Rest::Schema::V1_1::Json auto& installer = result.value(); installer.ProductId = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(MSStoreProductIdentifier)).value_or(""); + installer.ReleaseDate = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(ReleaseDate)).value_or(""); + installer.InstallerAbortsTerminal = JsonHelper::GetRawBoolValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerAbortsTerminal)).value_or(false); + installer.InstallLocationRequired = JsonHelper::GetRawBoolValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallLocationRequired)).value_or(false); + installer.RequireExplicitUpgrade = JsonHelper::GetRawBoolValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(RequireExplicitUpgrade)).value_or(false); + installer.ElevationRequirement = Manifest::ConvertToElevationRequirementEnum( + JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(ElevationRequirement)).value_or("")); + + // list of unsupported OS architectures + std::optional<std::reference_wrapper<const web::json::array>> unsupportedOSArchitectures = JsonHelper::GetRawJsonArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(UnsupportedOSArchitectures)); + if (unsupportedOSArchitectures) + { + for (auto& archValue : unsupportedOSArchitectures.value().get()) + { + std::optional<std::string> arch = JsonHelper::GetRawStringValueFromJsonValue(archValue); + if (JsonHelper::IsValidNonEmptyStringValue(arch)) + { + auto archEnum = Utility::ConvertToArchitectureEnum(arch.value()); + + if (archEnum == Utility::Architecture::Neutral) + { + AICLI_LOG(Repo, Error, << "Unsupported OS architectures cannot contain neutral value."); + return {}; + } + + if (archEnum != Utility::Architecture::Unknown) + { + installer.UnsupportedOSArchitectures.emplace_back(archEnum); + } + } + } + } + + // Apps and Features Entries + std::optional<std::reference_wrapper<const web::json::array>> arpEntriesNode = JsonHelper::GetRawJsonArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(AppsAndFeaturesEntries)); + if (arpEntriesNode) + { + for (auto& arpEntryNode : arpEntriesNode.value().get()) + { + AppsAndFeaturesEntry arpEntry; + arpEntry.DisplayName = JsonHelper::GetRawStringValueFromJsonNode(arpEntryNode, JsonHelper::GetUtilityString(DisplayName)).value_or(""); + arpEntry.Publisher = JsonHelper::GetRawStringValueFromJsonNode(arpEntryNode, JsonHelper::GetUtilityString(Publisher)).value_or(""); + arpEntry.DisplayVersion = JsonHelper::GetRawStringValueFromJsonNode(arpEntryNode, JsonHelper::GetUtilityString(DisplayVersion)).value_or(""); + arpEntry.ProductCode = JsonHelper::GetRawStringValueFromJsonNode(arpEntryNode, JsonHelper::GetUtilityString(ProductCode)).value_or(""); + arpEntry.UpgradeCode = JsonHelper::GetRawStringValueFromJsonNode(arpEntryNode, JsonHelper::GetUtilityString(UpgradeCode)).value_or(""); + arpEntry.InstallerType = Manifest::ConvertToInstallerTypeEnum(JsonHelper::GetRawStringValueFromJsonNode(arpEntryNode, JsonHelper::GetUtilityString(InstallerType)).value_or("")); + + // Only add when at least one field is valid + if (!arpEntry.DisplayName.empty() || !arpEntry.Publisher.empty() || !arpEntry.DisplayVersion.empty() || + !arpEntry.ProductCode.empty() || !arpEntry.UpgradeCode.empty() || arpEntry.InstallerType != InstallerTypeEnum::Unknown) + { + installer.AppsAndFeaturesEntries.emplace_back(std::move(arpEntry)); + } + } + } + + // Markets + std::optional<std::reference_wrapper<const web::json::value>> marketsNode = JsonHelper::GetJsonValueFromNode(installerJsonObject, JsonHelper::GetUtilityString(Markets)); + if (marketsNode && !marketsNode.value().get().is_null()) + { + installer.Markets.ExcludedMarkets = V1_0::Json::ManifestDeserializer::ConvertToManifestStringArray( + JsonHelper::GetRawStringArrayFromJsonNode(marketsNode.value().get(), JsonHelper::GetUtilityString(ExcludedMarkets))); + installer.Markets.AllowedMarkets = V1_0::Json::ManifestDeserializer::ConvertToManifestStringArray( + JsonHelper::GetRawStringArrayFromJsonNode(marketsNode.value().get(), JsonHelper::GetUtilityString(AllowedMarkets))); + } + } + + return result; + } + + std::optional<Manifest::ManifestLocalization> ManifestDeserializer::DeserializeLocale(const web::json::value& localeJsonObject) const + { + auto result = V1_0::Json::ManifestDeserializer::DeserializeLocale(localeJsonObject); + + if (result) + { + auto& locale = result.value(); + + TryParseStringLocaleField<Manifest::Localization::ReleaseNotes>(locale, localeJsonObject, ReleaseNotes); + TryParseStringLocaleField<Manifest::Localization::ReleaseNotesUrl>(locale, localeJsonObject, ReleaseNotesUrl); + + // Agreements + auto agreementsNode = JsonHelper::GetRawJsonArrayFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(Agreements)); + if (agreementsNode) + { + std::vector<Manifest::Agreement> agreements; + for (auto const& agreementNode : agreementsNode.value().get()) + { + Manifest::Agreement agreementEntry; + + agreementEntry.Label = JsonHelper::GetRawStringValueFromJsonNode(agreementNode, JsonHelper::GetUtilityString(AgreementLabel)).value_or(""); + agreementEntry.AgreementText = JsonHelper::GetRawStringValueFromJsonNode(agreementNode, JsonHelper::GetUtilityString(Agreement)).value_or(""); + agreementEntry.AgreementUrl = JsonHelper::GetRawStringValueFromJsonNode(agreementNode, JsonHelper::GetUtilityString(AgreementUrl)).value_or(""); + + if (!agreementEntry.Label.empty() || !agreementEntry.AgreementText.empty() || !agreementEntry.AgreementUrl.empty()) + { + agreements.emplace_back(std::move(agreementEntry)); + } + } + + if (!agreements.empty()) + { + locale.Add<Manifest::Localization::Agreements>(std::move(agreements)); + } + } } return result; diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/InformationResponseDeserializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/InformationResponseDeserializer.cpp @@ -110,7 +110,10 @@ namespace AppInstaller::Repository::Rest::Schema agreementEntry.Url = std::move(url.value()); } - info.SourceAgreements.emplace_back(std::move(agreementEntry)); + if (!agreementEntry.Label.empty() || !agreementEntry.Text.empty() || !agreementEntry.Url.empty()) + { + info.SourceAgreements.emplace_back(std::move(agreementEntry)); + } } } } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/JsonHelper.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/JsonHelper.cpp @@ -52,6 +52,28 @@ namespace AppInstaller::Repository::Rest::Schema return value.as_integer(); } + std::optional<bool> JsonHelper::GetRawBoolValueFromJsonValue(const web::json::value& value) + { + if (value.is_null() || !value.is_boolean()) + { + return {}; + } + + return value.as_bool(); + } + + std::optional<bool> JsonHelper::GetRawBoolValueFromJsonNode(const web::json::value& node, const utility::string_t& keyName) + { + std::optional<std::reference_wrapper<const web::json::value>> jsonValue = GetJsonValueFromNode(node, keyName); + + if (jsonValue) + { + return GetRawBoolValueFromJsonValue(jsonValue.value().get()); + } + + return {}; + } + std::optional<std::reference_wrapper<const web::json::array>> JsonHelper::GetRawJsonArrayFromJsonNode(const web::json::value& node, const utility::string_t& keyName) { std::optional<std::reference_wrapper<const web::json::value>> jsonValue = GetJsonValueFromNode(node, keyName); diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/JsonHelper.h b/src/AppInstallerRepositoryCore/Rest/Schema/JsonHelper.h @@ -15,9 +15,13 @@ namespace AppInstaller::Repository::Rest::Schema static std::optional<std::string> GetRawStringValueFromJsonNode(const web::json::value& node, const utility::string_t& keyName); + static std::optional<bool> GetRawBoolValueFromJsonValue(const web::json::value& value); + + static std::optional<bool> GetRawBoolValueFromJsonNode(const web::json::value& node, const utility::string_t& keyName); + static std::optional<std::reference_wrapper<const web::json::array>> GetRawJsonArrayFromJsonNode(const web::json::value& node, const utility::string_t& keyName); - static std::optional<int> GetRawIntValueFromJsonValue(const web::json::value& node); + static std::optional<int> GetRawIntValueFromJsonValue(const web::json::value& value); static utility::string_t GetUtilityString(std::string_view nodeName);