winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 12e4cd3963c9cac65a29d9cc1ec953acab945729
parent f6062a574d2243d3a38e5764cd48b558a5bbf9bb
Author: Ruben Guerrero <rubengu@microsoft.com>
Date:   Thu,  1 Feb 2024 07:24:41 -0800

Shadow Manifest (#4104)

This PR introduces shadow manifests.

A shadow manifest is a file used to create a merged manifest that
contains extra information about the package. For now, the shadow
manifest contains icon information. The file is meant to be created
programmatically and can be done via `ManifestShadow` from the
`WinGetUtilnterop` nuget package.

The shadow will be added to the merged manifest when a directory
contains a shadow manifest (ManifestType = shadow) and the create
options allow using shadow manifest. If the flag is not set and a shadow
manifest file is found it will fail. There can only be one shadow
manifest file in the directory.

The shadow manifest gets processed and merged into the `Manifest` and
the root YAML node. It will only merge information if the processed
merged manifest doesn't already contain it. For example, if the manifest
already contains a localization node with a specific `PackageLocale`
that have `Icons` and the shadow manifest also contains a localization
node with the same `PackageLocale` value with `Icons` it won't get
merged.

A serialized shadow manifest looks like this (ignore values):
```
PackageIdentifier: Package.package
PackageVersion: 1.0
ManifestType: shadow
ManifestVersion: 1.5
PackageLocale: en-US
Icons:
- IconUrl: iconUrl
  IconFileType: fileType
  IconResolution: iconResolution
  IconTheme: iconTheme
  IconSha256: iconSha256
Localization:
- PackageLocale: es-MX
  Icons:
  - IconUrl: iconUrl-esMX
    IconFileType: fileType-esMX
    IconResolution: iconResolution-esMX
    IconTheme: iconTheme-esMX
    IconSha256: iconSha256-esMX
- PackageLocale: de-DE
  Icons:
  - IconUrl: iconUrl-de-DE
    IconFileType: fileType-de-DE
    IconResolution: iconResolution-de-DE
    IconTheme: iconTheme-de-DE
    IconSha256: iconSha256-de-DE
```
###### Microsoft Reviewers: [Open in
CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/microsoft/winget-cli/pull/4104)
Diffstat:
M.github/actions/spelling/expect.txt | 6++++++
M.github/actions/spelling/patterns.txt | 3+++
Mazure-pipelines.yml | 1+
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 36++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 33+++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/TestData/ManifestV1_5-Singleton.yaml | 2+-
Msrc/AppInstallerCLITests/TestData/ManifestV1_6-Singleton.yaml | 2+-
Msrc/AppInstallerCLITests/TestData/ManifestV1_7-Singleton.yaml | 2+-
Msrc/AppInstallerCLITests/TestData/MultiFileManifestV1_5/ManifestV1_5-MultiFile-DefaultLocale.yaml | 2+-
Msrc/AppInstallerCLITests/TestData/MultiFileManifestV1_5/ManifestV1_5-MultiFile-Locale.yaml | 2+-
Msrc/AppInstallerCLITests/TestData/MultiFileManifestV1_6/ManifestV1_6-MultiFile-DefaultLocale.yaml | 2+-
Msrc/AppInstallerCLITests/TestData/MultiFileManifestV1_6/ManifestV1_6-MultiFile-Locale.yaml | 2+-
Msrc/AppInstallerCLITests/TestData/MultiFileManifestV1_7/ManifestV1_7-MultiFile-DefaultLocale.yaml | 2+-
Msrc/AppInstallerCLITests/TestData/MultiFileManifestV1_7/ManifestV1_7-MultiFile-Locale.yaml | 2+-
Asrc/AppInstallerCLITests/TestData/Node-Mapping.yaml | 14++++++++++++++
Asrc/AppInstallerCLITests/TestData/Node-Merge.yaml | 8++++++++
Asrc/AppInstallerCLITests/TestData/Node-Merge2.yaml | 7+++++++
Asrc/AppInstallerCLITests/TestData/Shadow/V1_5/ManifestV1_5-Shadow-DefaultLocale.yaml | 10++++++++++
Asrc/AppInstallerCLITests/TestData/Shadow/V1_5/ManifestV1_5-Shadow-Installer.yaml | 20++++++++++++++++++++
Asrc/AppInstallerCLITests/TestData/Shadow/V1_5/ManifestV1_5-Shadow-Locale.yaml | 6++++++
Asrc/AppInstallerCLITests/TestData/Shadow/V1_5/ManifestV1_5-Shadow-Locale2.yaml | 12++++++++++++
Asrc/AppInstallerCLITests/TestData/Shadow/V1_5/ManifestV1_5-Shadow-Shadow.yaml | 27+++++++++++++++++++++++++++
Asrc/AppInstallerCLITests/TestData/Shadow/V1_5/ManifestV1_5-Shadow-Shadow2.yaml | 12++++++++++++
Msrc/AppInstallerCLITests/YamlManifest.cpp | 1357++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Msrc/AppInstallerCommonCore/Manifest/ManifestCommon.cpp | 4++++
Msrc/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp | 6++++++
Msrc/AppInstallerCommonCore/Manifest/ManifestValidation.cpp | 3++-
Msrc/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp | 328+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/AppInstallerCommonCore/Manifest/YamlParser.cpp | 48+++++++++++++++++++++++++++++++++++++++++++++---
Msrc/AppInstallerCommonCore/Public/winget/ManifestCommon.h | 2++
Msrc/AppInstallerCommonCore/Public/winget/ManifestValidation.h | 1+
Msrc/AppInstallerCommonCore/Public/winget/ManifestYamlPopulator.h | 53++++++++++++++++++++++++++++++-----------------------
Msrc/AppInstallerSharedLib/Public/winget/Yaml.h | 11+++++++++++
Msrc/AppInstallerSharedLib/Yaml.cpp | 141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/WinGetUtil/Exports.cpp | 13++++++++++++-
Msrc/WinGetUtil/WinGetUtil.h | 2++
Asrc/WinGetUtilInterop.UnitTests/APIUnitTests/ManifestUnitTests.cs | 160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/WinGetUtilInterop.UnitTests/Common/FactSkipx64CI.cs | 28++++++++++++++++++++++++++++
Msrc/WinGetUtilInterop.UnitTests/ManifestUnitTest/ManifestEqualityUnitTests.cs | 2+-
Msrc/WinGetUtilInterop.UnitTests/ManifestUnitTest/V1ManifestReadTest.cs | 2+-
Asrc/WinGetUtilInterop.UnitTests/TestCollateral/ExpectedShadowManifest.yaml | 26++++++++++++++++++++++++++
Msrc/WinGetUtilInterop.UnitTests/WinGetUtilInterop.UnitTests.csproj | 24+++++++++++++++++++++++-
Msrc/WinGetUtilInterop/Common/Enums.cs | 5+++++
Asrc/WinGetUtilInterop/Common/Helpers.cs | 40++++++++++++++++++++++++++++++++++++++++
Msrc/WinGetUtilInterop/Manifest/V1/Manifest.cs | 40+++++++++++++++++++++++++---------------
Msrc/WinGetUtilInterop/Manifest/V1/ManifestLocalization.cs | 42++++++++++++++++++++++++++++++++++++++++++
Asrc/WinGetUtilInterop/Manifest/V1/ManifestShadow.cs | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/WinGetUtilInterop/Manifest/V1/ManifestShadowLocalization.cs | 48++++++++++++++++++++++++++++++++++++++++++++++++
48 files changed, 2121 insertions(+), 637 deletions(-)

diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -136,6 +136,7 @@ foldc foldcase FOLDERID FORPARSING +foundfr fundraiser fuzzer fzanollo @@ -144,6 +145,7 @@ GESMBH GHS gity goku +Gomu GRPICONDIR GRPICONDIRENTRY guiddef @@ -270,6 +272,7 @@ mylog mysilent mysilentwithprogress nameof +Nami nativehandle NBLGGH NESTEDINSTALLER @@ -357,6 +360,7 @@ REGSAM relativefilepath remoting reparse +repeatedkey restsource RGBQUAD rgex @@ -366,6 +370,7 @@ riid roblox ronomon rosoft +Roronoa rowids roy runspace @@ -391,6 +396,7 @@ Sideload SIGNATUREHASH similarissues similaritytolerance +Skipx Sku SMTO sortof diff --git a/.github/actions/spelling/patterns.txt b/.github/actions/spelling/patterns.txt @@ -160,3 +160,6 @@ ReplaceWhileCopying\(L.*\) # ignore long runs of a single character: \b([A-Za-z])\g{-1}{3,}\b + +# devil fruits +\s([A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,})\s\g{-1}\sno Mi diff --git a/azure-pipelines.yml b/azure-pipelines.yml @@ -315,6 +315,7 @@ jobs: codeCoverageEnabled: true platform: '$(buildPlatform)' configuration: '$(BuildConfiguration)' + diagnosticsEnabled: true condition: succeededOrFailed() - task: VSTest@2 diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -888,6 +888,42 @@ <CopyFileToFolders Include="TestData\notepad.ico"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Shadow\V1_5\ManifestV1_5-Shadow-DefaultLocale.yaml"> + <DeploymentContent Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</DeploymentContent> + <FileType>Document</FileType> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Shadow\V1_5\ManifestV1_5-Shadow-Locale.yaml"> + <DeploymentContent Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</DeploymentContent> + <FileType>Document</FileType> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Shadow\V1_5\ManifestV1_5-Shadow-Locale2.yaml"> + <DeploymentContent Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</DeploymentContent> + <FileType>Document</FileType> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Shadow\V1_5\ManifestV1_5-Shadow-Shadow.yaml"> + <DeploymentContent Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</DeploymentContent> + <FileType>Document</FileType> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Shadow\V1_5\ManifestV1_5-Shadow-Shadow2.yaml"> + <DeploymentContent Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</DeploymentContent> + <FileType>Document</FileType> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Shadow\V1_5\ManifestV1_5-Shadow-Installer.yaml"> + <DeploymentContent Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</DeploymentContent> + <FileType>Document</FileType> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Node-Merge.yaml"> + <DeploymentContent Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</DeploymentContent> + <FileType>Document</FileType> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Node-Merge2.yaml"> + <DeploymentContent Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</DeploymentContent> + <FileType>Document</FileType> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Node-Mapping.yaml"> + <DeploymentContent Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</DeploymentContent> + <FileType>Document</FileType> + </CopyFileToFolders> </ItemGroup> <ItemGroup> <ProjectReference Include="..\AppInstallerCLICore\AppInstallerCLICore.vcxproj"> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -46,6 +46,12 @@ <Filter Include="Source Files\Repository"> <UniqueIdentifier>{13d4d227-0f04-4e57-a663-c3c535438ab3}</UniqueIdentifier> </Filter> + <Filter Include="TestData\Shadow"> + <UniqueIdentifier>{6f8102b7-ea5f-4379-bb59-067ced63d970}</UniqueIdentifier> + </Filter> + <Filter Include="TestData\Shadow\V1_5"> + <UniqueIdentifier>{6bd68492-3f96-4023-af35-63249cd18158}</UniqueIdentifier> + </Filter> </ItemGroup> <ItemGroup> <ClInclude Include="pch.h"> @@ -939,5 +945,32 @@ <CopyFileToFolders Include="TestData\notepad.ico"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Shadow\V1_5\ManifestV1_5-Shadow-DefaultLocale.yaml"> + <Filter>TestData\Shadow\V1_5</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Shadow\V1_5\ManifestV1_5-Shadow-Locale.yaml"> + <Filter>TestData\Shadow\V1_5</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Shadow\V1_5\ManifestV1_5-Shadow-Locale2.yaml"> + <Filter>TestData\Shadow\V1_5</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Shadow\V1_5\ManifestV1_5-Shadow-Shadow.yaml"> + <Filter>TestData\Shadow\V1_5</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Shadow\V1_5\ManifestV1_5-Shadow-Shadow2.yaml"> + <Filter>TestData\Shadow\V1_5</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Shadow\V1_5\ManifestV1_5-Shadow-Installer.yaml"> + <Filter>TestData\Shadow\V1_5</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Node-Merge.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Node-Merge2.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Node-Mapping.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> </ItemGroup> </Project> \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/ManifestV1_5-Singleton.yaml b/src/AppInstallerCLITests/TestData/ManifestV1_5-Singleton.yaml @@ -26,7 +26,7 @@ Documentations: - DocumentLabel: Default document label DocumentUrl: https://DefaultDocumentUrl.com Icons: - - IconUrl: https://testIcon + - IconUrl: https://testIcon-en-US IconFileType: ico IconResolution: custom IconTheme: default diff --git a/src/AppInstallerCLITests/TestData/ManifestV1_6-Singleton.yaml b/src/AppInstallerCLITests/TestData/ManifestV1_6-Singleton.yaml @@ -26,7 +26,7 @@ Documentations: - DocumentLabel: Default document label DocumentUrl: https://DefaultDocumentUrl.com Icons: - - IconUrl: https://testIcon + - IconUrl: https://testIcon-en-US IconFileType: ico IconResolution: custom IconTheme: default diff --git a/src/AppInstallerCLITests/TestData/ManifestV1_7-Singleton.yaml b/src/AppInstallerCLITests/TestData/ManifestV1_7-Singleton.yaml @@ -26,7 +26,7 @@ Documentations: - DocumentLabel: Default document label DocumentUrl: https://DefaultDocumentUrl.com Icons: - - IconUrl: https://testIcon + - IconUrl: https://testIcon-en-US IconFileType: ico IconResolution: custom IconTheme: default diff --git a/src/AppInstallerCLITests/TestData/MultiFileManifestV1_5/ManifestV1_5-MultiFile-DefaultLocale.yaml b/src/AppInstallerCLITests/TestData/MultiFileManifestV1_5/ManifestV1_5-MultiFile-DefaultLocale.yaml @@ -26,7 +26,7 @@ Documentations: - DocumentLabel: Default document label DocumentUrl: https://DefaultDocumentUrl.com Icons: - - IconUrl: https://testIcon + - IconUrl: https://testIcon-en-US IconFileType: ico IconResolution: custom IconTheme: default diff --git a/src/AppInstallerCLITests/TestData/MultiFileManifestV1_5/ManifestV1_5-MultiFile-Locale.yaml b/src/AppInstallerCLITests/TestData/MultiFileManifestV1_5/ManifestV1_5-MultiFile-Locale.yaml @@ -29,7 +29,7 @@ Documentations: - DocumentLabel: Default document label DocumentUrl: https://DefaultDocumentUrl.com Icons: - - IconUrl: https://localeTestIcon + - IconUrl: https://localeTestIcon-en-GB IconFileType: png IconResolution: 32x32 IconTheme: light diff --git a/src/AppInstallerCLITests/TestData/MultiFileManifestV1_6/ManifestV1_6-MultiFile-DefaultLocale.yaml b/src/AppInstallerCLITests/TestData/MultiFileManifestV1_6/ManifestV1_6-MultiFile-DefaultLocale.yaml @@ -26,7 +26,7 @@ Documentations: - DocumentLabel: Default document label DocumentUrl: https://DefaultDocumentUrl.com Icons: - - IconUrl: https://testIcon + - IconUrl: https://testIcon-en-US IconFileType: ico IconResolution: custom IconTheme: default diff --git a/src/AppInstallerCLITests/TestData/MultiFileManifestV1_6/ManifestV1_6-MultiFile-Locale.yaml b/src/AppInstallerCLITests/TestData/MultiFileManifestV1_6/ManifestV1_6-MultiFile-Locale.yaml @@ -29,7 +29,7 @@ Documentations: - DocumentLabel: Default document label DocumentUrl: https://DefaultDocumentUrl.com Icons: - - IconUrl: https://localeTestIcon + - IconUrl: https://localeTestIcon-en-GB IconFileType: png IconResolution: 32x32 IconTheme: light diff --git a/src/AppInstallerCLITests/TestData/MultiFileManifestV1_7/ManifestV1_7-MultiFile-DefaultLocale.yaml b/src/AppInstallerCLITests/TestData/MultiFileManifestV1_7/ManifestV1_7-MultiFile-DefaultLocale.yaml @@ -26,7 +26,7 @@ Documentations: - DocumentLabel: Default document label DocumentUrl: https://DefaultDocumentUrl.com Icons: - - IconUrl: https://testIcon + - IconUrl: https://testIcon-en-US IconFileType: ico IconResolution: custom IconTheme: default diff --git a/src/AppInstallerCLITests/TestData/MultiFileManifestV1_7/ManifestV1_7-MultiFile-Locale.yaml b/src/AppInstallerCLITests/TestData/MultiFileManifestV1_7/ManifestV1_7-MultiFile-Locale.yaml @@ -29,7 +29,7 @@ Documentations: - DocumentLabel: Default document label DocumentUrl: https://DefaultDocumentUrl.com Icons: - - IconUrl: https://localeTestIcon + - IconUrl: https://localeTestIcon-en-GB IconFileType: png IconResolution: 32x32 IconTheme: light diff --git a/src/AppInstallerCLITests/TestData/Node-Mapping.yaml b/src/AppInstallerCLITests/TestData/Node-Mapping.yaml @@ -0,0 +1,14 @@ +key: value + +repeatedkey: repeated value +RepeatedKey: repeated value +RepeatedKey: repeated value + +MergeNode: + Custom: /custom + SilentWithProgress: /silentwithprogress + Silent: /silence + +MergeNode2: + silentWithProgress: /silentwithprogress + Interactive: /interactive diff --git a/src/AppInstallerCLITests/TestData/Node-Merge.yaml b/src/AppInstallerCLITests/TestData/Node-Merge.yaml @@ -0,0 +1,8 @@ +StrawHats: + - Name: Monkey D Luffy + Bounty: 3,000,000,000 + - Name: Roronoa Zoro + Bounty: 1,111,000,000 + - Name: Nami + Bounty: 366,000,000 + diff --git a/src/AppInstallerCLITests/TestData/Node-Merge2.yaml b/src/AppInstallerCLITests/TestData/Node-Merge2.yaml @@ -0,0 +1,7 @@ +StrawHats: + - name: Monkey d Luffy + Bounty: 150,000,000 + Fruit: Gomu Gomu no Mi + - Name: Nico Robin + Bounty: 930,000,000 + Fruit: Hana Hana no Mi diff --git a/src/AppInstallerCLITests/TestData/Shadow/V1_5/ManifestV1_5-Shadow-DefaultLocale.yaml b/src/AppInstallerCLITests/TestData/Shadow/V1_5/ManifestV1_5-Shadow-DefaultLocale.yaml @@ -0,0 +1,10 @@ +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.7.32 +PackageLocale: en-US +PackageName: MSIX SDK +License: MIT License +ShortDescription: This is MSIX SDK +Description: The MSIX SDK project is an effort to enable developers +Publisher: Microsoft +ManifestType: defaultLocale +ManifestVersion: 1.5.0 diff --git a/src/AppInstallerCLITests/TestData/Shadow/V1_5/ManifestV1_5-Shadow-Installer.yaml b/src/AppInstallerCLITests/TestData/Shadow/V1_5/ManifestV1_5-Shadow-Installer.yaml @@ -0,0 +1,19 @@ +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.7.32 +InstallerLocale: en-US +Installers: + - Architecture: x64 + InstallerType: exe + InstallerUrl: https://www.microsoft.com/msixsdk/msixsdkx64.exe + InstallerSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + ProductCode: "{Bar}" + InstallerSwitches: + Custom: /custom + SilentWithProgress: /silentwithprogress + Silent: /silence + Interactive: /interactive + Log: /log=<LOGPATH> + InstallLocation: /dir=<INSTALLPATH> + Upgrade: /upgrade +ManifestType: installer +ManifestVersion: 1.5.0+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/Shadow/V1_5/ManifestV1_5-Shadow-Locale.yaml b/src/AppInstallerCLITests/TestData/Shadow/V1_5/ManifestV1_5-Shadow-Locale.yaml @@ -0,0 +1,6 @@ +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.7.32 +PackageLocale: en-GB +Description: The MSIX SDK project is an effort to enable developers UK +ManifestType: locale +ManifestVersion: 1.5.0 diff --git a/src/AppInstallerCLITests/TestData/Shadow/V1_5/ManifestV1_5-Shadow-Locale2.yaml b/src/AppInstallerCLITests/TestData/Shadow/V1_5/ManifestV1_5-Shadow-Locale2.yaml @@ -0,0 +1,12 @@ +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.7.32 +PackageLocale: es-MX +Description: The MSIX SDK project is an effort to enable developers MX +ManifestType: locale +ManifestVersion: 1.5.0 +Icons: + - IconUrl: https://localeTestIcon-es-MX + IconFileType: png + IconResolution: 32x32 + IconTheme: light + IconSha256: 4444444444444444444444444444444444444444444444444444444444444444 diff --git a/src/AppInstallerCLITests/TestData/Shadow/V1_5/ManifestV1_5-Shadow-Shadow.yaml b/src/AppInstallerCLITests/TestData/Shadow/V1_5/ManifestV1_5-Shadow-Shadow.yaml @@ -0,0 +1,26 @@ +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.7.32 +ManifestType: shadow +ManifestVersion: 1.5.0 +PackageLocale: en-US +Icons: + - IconUrl: https://shadowIcon-default + IconFileType: ico + IconResolution: custom + IconTheme: default + IconSha256: 1111111111111111111111111111111111111111111111111111111111111111 +Localization: + - PackageLocale: en-gb + Icons: + - IconUrl: https://shadowIcon-en-GB + IconFileType: png + IconResolution: 32x32 + IconTheme: light + IconSha256: 2222222222222222222222222222222222222222222222222222222222222222 + - PackageLocale: fr-FR + Icons: + - IconUrl: https://shadowIcon-fr-FR + IconFileType: jpeg + IconResolution: 20x20 + IconTheme: dark + IconSha256: 3333333333333333333333333333333333333333333333333333333333333333+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/Shadow/V1_5/ManifestV1_5-Shadow-Shadow2.yaml b/src/AppInstallerCLITests/TestData/Shadow/V1_5/ManifestV1_5-Shadow-Shadow2.yaml @@ -0,0 +1,11 @@ +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.7.32 +ManifestType: shadow +ManifestVersion: 1.5.0 +PackageLocale: en-US +Icons: + - IconUrl: https://shadowIcon-default2 + IconFileType: ico + IconResolution: custom + IconTheme: default + IconSha256: 1111111111111111111111111111111111111111111111111111111111111111+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp @@ -14,41 +14,613 @@ using namespace AppInstaller::Manifest::YamlWriter; using namespace AppInstaller::Utility; using namespace AppInstaller::YAML; -using MultiValue = std::vector<NormalizedString>; -bool operator==(const MultiValue& a, const MultiValue& b) +namespace { - if (a.size() != b.size()) + using MultiValue = std::vector<NormalizedString>; + bool operator==(const MultiValue& a, const MultiValue& b) { - return false; + if (a.size() != b.size()) + { + return false; + } + + for (size_t i = 0; i < a.size(); ++i) + { + if (a[i] != b[i]) + { + return false; + } + } + + return true; + } + + void ValidateError( + const ValidationError& error, + ValidationError::Level level, + AppInstaller::StringResource::StringId message, + std::string field, + std::string value) + { + REQUIRE(level == error.ErrorLevel); + REQUIRE(message == error.Message); + REQUIRE(field == error.Context); + REQUIRE(value == error.Value); + } + + void ValidateError(const ValidationError& error, ValidationError::Level level, AppInstaller::StringResource::StringId message) + { + ValidateError(error, level, message, std::string(), std::string()); } - for (size_t i = 0; i < a.size(); ++i) + struct ManifestExceptionMatcher : public Catch::MatcherBase<ManifestException> { - if (a[i] != b[i]) + ManifestExceptionMatcher(std::string expectedMessage, bool expectedWarningOnly = false) : + m_expectedMessage(expectedMessage), m_expectedWarningOnly(expectedWarningOnly) {} + + // Performs the test for this matcher + bool match(ManifestException const& e) const override { - return false; + return e.GetManifestErrorMessage().find(m_expectedMessage) != std::string::npos && + e.IsWarningOnly() == m_expectedWarningOnly; } + + virtual std::string describe() const override { + std::ostringstream ss; + ss << std::boolalpha << "Expected exception message: " << m_expectedMessage << " Expected IsWarningOnly: " << m_expectedWarningOnly; + return ss.str(); + } + + private: + std::string m_expectedMessage; + bool m_expectedWarningOnly; + }; + + ManifestValidateOption GetTestManifestValidateOption( + bool schemaValidationOnly = false, + bool errorOnVerifiedPublisher = false) + { + ManifestValidateOption validateOption; + validateOption.FullValidation = true; + validateOption.ThrowOnWarning = true; + validateOption.SchemaValidationOnly = schemaValidationOnly; + validateOption.ErrorOnVerifiedPublisherFields = errorOnVerifiedPublisher; + return validateOption; } - return true; -} + void TestManifest( + const std::filesystem::path& manifestPath, + const std::string& expectedMessage = {}, + bool expectedWarningOnly = false, + ManifestValidateOption validateOption = GetTestManifestValidateOption()) + { + INFO(manifestPath.u8string()); -void ValidateError( - const ValidationError& error, - ValidationError::Level level, - AppInstaller::StringResource::StringId message, - std::string field, - std::string value) -{ - REQUIRE(level == error.ErrorLevel); - REQUIRE(message == error.Message); - REQUIRE(field == error.Context); - REQUIRE(value == error.Value); -} + if (expectedMessage.empty()) + { + CHECK_NOTHROW(YamlParser::CreateFromPath(TestDataFile(manifestPath), validateOption)); + } + else + { + CHECK_THROWS_MATCHES(YamlParser::CreateFromPath(TestDataFile(manifestPath), validateOption), ManifestException, ManifestExceptionMatcher(expectedMessage, expectedWarningOnly)); + } + } -void ValidateError(const ValidationError& error, ValidationError::Level level, AppInstaller::StringResource::StringId message) -{ - ValidateError(error, level, message, std::string(), std::string()); + struct ManifestTestCase + { + std::string TestFile; + std::string ExpectedMessage = {}; + bool IsWarningOnly = false; + ManifestValidateOption ValidateOption = GetTestManifestValidateOption(); + }; + + void CopyTestDataFilesToFolder(const std::vector<std::string>& testDataFiles, const std::filesystem::path& dest) + { + for (const auto& fileName : testDataFiles) + { + std::filesystem::copy(TestDataFile(fileName), dest); + } + } + + void VerifyV1ManifestContent(const Manifest& manifest, bool isSingleton, ManifestVer manifestVer = { s_ManifestVersionV1 }, bool isExported = false) + { + REQUIRE(manifest.Id == "microsoft.msixsdk"); + REQUIRE(manifest.Version == "1.7.32"); + REQUIRE(manifest.DefaultLocalization.Locale == "en-US"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Publisher>() == "Microsoft"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PublisherUrl>() == "https://www.microsoft.com"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PublisherSupportUrl>() == "https://www.microsoft.com/support"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PrivacyUrl>() == "https://www.microsoft.com/privacy"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Author>() == "Microsoft"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PackageName>() == "MSIX SDK"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PackageUrl>() == "https://www.microsoft.com/msixsdk/home"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::License>() == "MIT License"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::LicenseUrl>() == "https://www.microsoft.com/msixsdk/license"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Copyright>() == "Copyright Microsoft Corporation"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::CopyrightUrl>() == "https://www.microsoft.com/msixsdk/copyright"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::ShortDescription>() == "This is MSIX SDK"); + 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"); + } + + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_2 }) + { + REQUIRE(manifest.DefaultLocalization.Get<Localization::PurchaseUrl>() == "https://DefaultPurchaseUrl.com"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::InstallationNotes>() == "Default installation notes"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Documentations>().size() == 1); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Documentations>().at(0).DocumentLabel == "Default document label"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Documentations>().at(0).DocumentUrl == "https://DefaultDocumentUrl.com"); + } + + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_5 }) + { + REQUIRE(manifest.DefaultLocalization.Get<Localization::Icons>().size() == 1); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Icons>().at(0).Url == "https://testIcon-en-US"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Icons>().at(0).FileType == IconFileTypeEnum::Ico); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Icons>().at(0).Resolution == IconResolutionEnum::Custom); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Icons>().at(0).Theme == IconThemeEnum::Default); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Icons>().at(0).Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8123")); + } + + if (!isExported) + { + 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"); + REQUIRE(manifest.DefaultInstallerInfo.BaseInstallerType == InstallerTypeEnum::Exe); + REQUIRE(manifest.DefaultInstallerInfo.Scope == ScopeEnum::Machine); + REQUIRE(manifest.DefaultInstallerInfo.InstallModes == std::vector<InstallModeEnum>{ InstallModeEnum::Interactive, InstallModeEnum::Silent, InstallModeEnum::SilentWithProgress }); + + auto defaultSwitches = manifest.DefaultInstallerInfo.Switches; + REQUIRE(defaultSwitches.at(InstallerSwitchType::Custom) == "/custom"); + REQUIRE(defaultSwitches.at(InstallerSwitchType::SilentWithProgress) == "/silentwithprogress"); + REQUIRE(defaultSwitches.at(InstallerSwitchType::Silent) == "/silence"); + REQUIRE(defaultSwitches.at(InstallerSwitchType::Interactive) == "/interactive"); + REQUIRE(defaultSwitches.at(InstallerSwitchType::Log) == "/log=<LOGPATH>"); + REQUIRE(defaultSwitches.at(InstallerSwitchType::InstallLocation) == "/dir=<INSTALLPATH>"); + REQUIRE(defaultSwitches.at(InstallerSwitchType::Update) == "/upgrade"); + + REQUIRE(manifest.DefaultInstallerInfo.InstallerSuccessCodes == std::vector<DWORD>{ 1, static_cast<DWORD>(0x80070005) }); + REQUIRE(manifest.DefaultInstallerInfo.UpdateBehavior == UpdateBehaviorEnum::UninstallPrevious); + REQUIRE(manifest.DefaultInstallerInfo.Commands == MultiValue{ "makemsix", "makeappx" }); + REQUIRE(manifest.DefaultInstallerInfo.Protocols == MultiValue{ "protocol1", "protocol2" }); + REQUIRE(manifest.DefaultInstallerInfo.FileExtensions == MultiValue{ "appx", "msix", "appxbundle", "msixbundle" }); + + auto dependencies = manifest.DefaultInstallerInfo.Dependencies; + REQUIRE(dependencies.HasExactDependency(DependencyType::WindowsFeature, "IIS")); + REQUIRE(dependencies.HasExactDependency(DependencyType::WindowsLibrary, "VC Runtime")); + REQUIRE(dependencies.HasExactDependency(DependencyType::Package, "Microsoft.MsixSdkDep", "1.0.0")); + REQUIRE(dependencies.HasExactDependency(DependencyType::External, "Outside dependencies")); + REQUIRE(dependencies.Size() == 4); + + REQUIRE(manifest.DefaultInstallerInfo.Capabilities == MultiValue{ "internetClient" }); + REQUIRE(manifest.DefaultInstallerInfo.RestrictedCapabilities == MultiValue{ "runFullTrust" }); + 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"); + REQUIRE(manifest.DefaultInstallerInfo.ExpectedReturnCodes.size() == 1); + REQUIRE(manifest.DefaultInstallerInfo.ExpectedReturnCodes.at(10).ReturnResponseEnum == ExpectedReturnCodeEnum::PackageInUse); + } + + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_2 }) + { + REQUIRE(manifest.DefaultInstallerInfo.DisplayInstallWarnings); + REQUIRE(manifest.DefaultInstallerInfo.UnsupportedArguments.size() == 1); + REQUIRE(manifest.DefaultInstallerInfo.UnsupportedArguments.at(0) == UnsupportedArgumentEnum::Log); + } + + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_4 }) + { + REQUIRE(manifest.DefaultInstallerInfo.NestedInstallerType == InstallerTypeEnum::Msi); + REQUIRE(manifest.DefaultInstallerInfo.NestedInstallerFiles.size() == 1); + REQUIRE(manifest.DefaultInstallerInfo.NestedInstallerFiles.at(0).RelativeFilePath == "RelativeFilePath"); + REQUIRE(manifest.DefaultInstallerInfo.NestedInstallerFiles.at(0).PortableCommandAlias == "PortableCommandAlias"); + REQUIRE(manifest.DefaultInstallerInfo.InstallationMetadata.DefaultInstallLocation == "%ProgramFiles%\\TestApp"); + REQUIRE(manifest.DefaultInstallerInfo.InstallationMetadata.Files.size() == 1); + REQUIRE(manifest.DefaultInstallerInfo.InstallationMetadata.Files.at(0).RelativeFilePath == "main.exe"); + REQUIRE(manifest.DefaultInstallerInfo.InstallationMetadata.Files.at(0).FileType == InstalledFileTypeEnum::Launch); + REQUIRE(manifest.DefaultInstallerInfo.InstallationMetadata.Files.at(0).FileSha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); + REQUIRE(manifest.DefaultInstallerInfo.InstallationMetadata.Files.at(0).InvocationParameter == "/arg"); + } + + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_6 }) + { + REQUIRE(manifest.DefaultInstallerInfo.DownloadCommandProhibited); + } + + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_7 }) + { + REQUIRE(defaultSwitches.at(InstallerSwitchType::Repair) == "/repair"); + REQUIRE(manifest.DefaultInstallerInfo.RepairBehavior == RepairBehaviorEnum::Modify); + } + } + + if (isSingleton || isExported) + { + REQUIRE(manifest.Installers.size() == 1); + } + else + { + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_7 }) + { + REQUIRE(manifest.Installers.size() == 5); + } + else if (manifestVer >= ManifestVer{ s_ManifestVersionV1_4 }) + { + REQUIRE(manifest.Installers.size() == 4); + } + else if (manifestVer == ManifestVer{ s_ManifestVersionV1_2 }) + { + REQUIRE(manifest.Installers.size() == 3); + } + else + { + REQUIRE(manifest.Installers.size() == 2); + } + } + + ManifestInstaller installer1 = manifest.Installers.at(0); + REQUIRE(installer1.Arch == Architecture::X86); + REQUIRE(installer1.Locale == "en-GB"); + REQUIRE(installer1.Platform == std::vector<PlatformEnum>{ PlatformEnum::Desktop }); + REQUIRE(installer1.MinOSVersion == "10.0.1.0"); + REQUIRE(installer1.BaseInstallerType == InstallerTypeEnum::Msix); + REQUIRE(installer1.Url == "https://www.microsoft.com/msixsdk/msixsdkx86.msix"); + REQUIRE(installer1.Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); + REQUIRE(installer1.SignatureSha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); + REQUIRE(installer1.Scope == ScopeEnum::User); + REQUIRE(installer1.InstallModes == std::vector<InstallModeEnum>{ InstallModeEnum::Interactive }); + + auto installer1Switches = installer1.Switches; + REQUIRE(installer1Switches.at(InstallerSwitchType::Custom) == "/c"); + REQUIRE(installer1Switches.at(InstallerSwitchType::SilentWithProgress) == "/sp"); + REQUIRE(installer1Switches.at(InstallerSwitchType::Silent) == "/s"); + REQUIRE(installer1Switches.at(InstallerSwitchType::Interactive) == "/i"); + REQUIRE(installer1Switches.at(InstallerSwitchType::Log) == "/l=<LOGPATH>"); + REQUIRE(installer1Switches.at(InstallerSwitchType::InstallLocation) == "/d=<INSTALLPATH>"); + REQUIRE(installer1Switches.at(InstallerSwitchType::Update) == "/u"); + + REQUIRE(installer1.UpdateBehavior == UpdateBehaviorEnum::Install); + REQUIRE(installer1.Commands == MultiValue{ "makemsixPreview", "makeappxPreview" }); + REQUIRE(installer1.Protocols == MultiValue{ "protocol1preview", "protocol2preview" }); + REQUIRE(installer1.FileExtensions == MultiValue{ "appxbundle", "msixbundle", "appx", "msix" }); + + auto installer1Dependencies = installer1.Dependencies; + REQUIRE(installer1Dependencies.HasExactDependency(DependencyType::WindowsFeature, "PreviewIIS")); + REQUIRE(installer1Dependencies.HasExactDependency(DependencyType::WindowsLibrary, "Preview VC Runtime")); + REQUIRE(installer1Dependencies.HasExactDependency(DependencyType::Package, "Microsoft.MsixSdkDepPreview", "1.0.0")); + REQUIRE(installer1Dependencies.HasExactDependency(DependencyType::External, "Preview Outside dependencies")); + REQUIRE(installer1Dependencies.Size() == 4); + + REQUIRE(installer1.Capabilities == MultiValue{ "internetClientPreview" }); + 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"); + REQUIRE(installer1.ExpectedReturnCodes.at(2).ReturnResponseEnum == ExpectedReturnCodeEnum::ContactSupport); + } + + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_2 }) + { + REQUIRE_FALSE(installer1.DisplayInstallWarnings); + REQUIRE(installer1.ExpectedReturnCodes.at(3).ReturnResponseEnum == ExpectedReturnCodeEnum::Custom); + REQUIRE(installer1.ExpectedReturnCodes.at(3).ReturnResponseUrl == "https://defaultReturnResponseUrl.com"); + REQUIRE(installer1.UnsupportedArguments.size() == 1); + REQUIRE(installer1.UnsupportedArguments.at(0) == UnsupportedArgumentEnum::Location); + } + + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_4 }) + { + // NestedInstaller metadata should not be populated unless the InstallerType is zip. + REQUIRE(installer1.NestedInstallerType == InstallerTypeEnum::Unknown); + REQUIRE(installer1.NestedInstallerFiles.size() == 0); + + REQUIRE(installer1.InstallationMetadata.DefaultInstallLocation == "%ProgramFiles%\\TestApp"); + REQUIRE(installer1.InstallationMetadata.Files.size() == 1); + REQUIRE(installer1.InstallationMetadata.Files.at(0).RelativeFilePath == "main.exe"); + REQUIRE(installer1.InstallationMetadata.Files.at(0).FileType == InstalledFileTypeEnum::Launch); + REQUIRE(installer1.InstallationMetadata.Files.at(0).FileSha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); + REQUIRE(installer1.InstallationMetadata.Files.at(0).InvocationParameter == "/arg"); + REQUIRE(installer1.InstallationMetadata.Files.at(0).DisplayName == "DisplayName"); + } + + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_6 }) + { + REQUIRE_FALSE(installer1.DownloadCommandProhibited); + } + + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_7 }) + { + REQUIRE(installer1.Switches.at(InstallerSwitchType::Repair) == "/r"); + REQUIRE(installer1.RepairBehavior == RepairBehaviorEnum::Modify); + } + + if (!isSingleton) + { + if (!isExported) + { + ManifestInstaller installer2 = manifest.Installers.at(1); + REQUIRE(installer2.BaseInstallerType == InstallerTypeEnum::Exe); + REQUIRE(installer2.Arch == Architecture::X64); + REQUIRE(installer2.Url == "https://www.microsoft.com/msixsdk/msixsdkx64.exe"); + 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"); + REQUIRE(installer2.ExpectedReturnCodes.size() == 1); + REQUIRE(installer2.ExpectedReturnCodes.at(10).ReturnResponseEnum == ExpectedReturnCodeEnum::PackageInUse); + } + + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_2 }) + { + ManifestInstaller installer3 = manifest.Installers.at(2); + REQUIRE(installer3.BaseInstallerType == InstallerTypeEnum::Portable); + REQUIRE(installer3.Arch == Architecture::X86); + REQUIRE(installer3.Url == "https://www.microsoft.com/msixsdk/msixsdkx86.exe"); + REQUIRE(installer3.Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); + REQUIRE(installer3.Commands == MultiValue{ "standalone" }); + REQUIRE(installer3.ExpectedReturnCodes.size() == 1); + REQUIRE(installer3.ExpectedReturnCodes.at(11).ReturnResponseEnum == ExpectedReturnCodeEnum::Custom); + REQUIRE(installer3.ExpectedReturnCodes.at(11).ReturnResponseUrl == "https://defaultReturnResponseUrl.com"); + REQUIRE_FALSE(installer3.DisplayInstallWarnings); + REQUIRE(installer3.UnsupportedArguments.size() == 1); + REQUIRE(installer3.UnsupportedArguments.at(0) == UnsupportedArgumentEnum::Log); + } + + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_4 }) + { + ManifestInstaller installer4 = manifest.Installers.at(3); + REQUIRE(installer4.BaseInstallerType == InstallerTypeEnum::Zip); + REQUIRE(installer4.Arch == Architecture::X64); + REQUIRE(installer4.Url == "https://www.microsoft.com/msixsdk/msixsdkx64.exe"); + REQUIRE(installer4.Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); + REQUIRE(installer4.ProductCode == "{Foo}"); + REQUIRE(installer4.NestedInstallerType == InstallerTypeEnum::Portable); + REQUIRE(installer4.NestedInstallerFiles.size() == 2); + REQUIRE(installer4.NestedInstallerFiles.at(0).RelativeFilePath == "relativeFilePath1"); + REQUIRE(installer4.NestedInstallerFiles.at(0).PortableCommandAlias == "portableAlias1"); + REQUIRE(installer4.NestedInstallerFiles.at(1).RelativeFilePath == "relativeFilePath2"); + REQUIRE(installer4.NestedInstallerFiles.at(1).PortableCommandAlias == "portableAlias2"); + REQUIRE(installer4.InstallationMetadata.DefaultInstallLocation == "%ProgramFiles%\\TestApp2"); + REQUIRE(installer4.InstallationMetadata.Files.size() == 1); + REQUIRE(installer4.InstallationMetadata.Files.at(0).RelativeFilePath == "main2.exe"); + REQUIRE(installer4.InstallationMetadata.Files.at(0).FileType == InstalledFileTypeEnum::Other); + REQUIRE(installer4.InstallationMetadata.Files.at(0).FileSha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); + REQUIRE(installer4.InstallationMetadata.Files.at(0).InvocationParameter == "/arg2"); + REQUIRE(installer4.InstallationMetadata.Files.at(0).DisplayName == "DisplayName2"); + } + + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_6 }) + { + REQUIRE(installer2.DownloadCommandProhibited); + REQUIRE(installer2.UpdateBehavior == UpdateBehaviorEnum::Deny); + } + + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_7 }) + { + REQUIRE(installer2.RepairBehavior == RepairBehaviorEnum::Uninstaller); + REQUIRE(installer2.Switches.at(InstallerSwitchType::Repair) == "/r"); + + ManifestInstaller installer5 = manifest.Installers.at(4); + REQUIRE(installer5.BaseInstallerType == InstallerTypeEnum::Burn); + REQUIRE(installer5.Arch == Architecture::X64); + REQUIRE(installer5.Url == "https://www.microsoft.com/msixsdk/msixsdkx64.exe"); + REQUIRE(installer5.Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); + REQUIRE(installer5.ProductCode == "{Bar}"); + REQUIRE(installer5.Switches.at(InstallerSwitchType::Repair) == "/repair"); + REQUIRE(installer5.RepairBehavior == RepairBehaviorEnum::Modify); + } + } + + // Localization + REQUIRE(manifest.Localizations.size() == 1); + ManifestLocalization localization1 = manifest.Localizations.at(0); + REQUIRE(localization1.Locale == "en-GB"); + REQUIRE(localization1.Get<Localization::Publisher>() == "Microsoft UK"); + REQUIRE(localization1.Get<Localization::PublisherUrl>() == "https://www.microsoft.com/UK"); + REQUIRE(localization1.Get<Localization::PublisherSupportUrl>() == "https://www.microsoft.com/support/UK"); + REQUIRE(localization1.Get<Localization::PrivacyUrl>() == "https://www.microsoft.com/privacy/UK"); + REQUIRE(localization1.Get<Localization::Author>() == "Microsoft UK"); + REQUIRE(localization1.Get<Localization::PackageName>() == "MSIX SDK UK"); + REQUIRE(localization1.Get<Localization::PackageUrl>() == "https://www.microsoft.com/msixsdk/home/UK"); + REQUIRE(localization1.Get<Localization::License>() == "MIT License UK"); + REQUIRE(localization1.Get<Localization::LicenseUrl>() == "https://www.microsoft.com/msixsdk/license/UK"); + REQUIRE(localization1.Get<Localization::Copyright>() == "Copyright Microsoft Corporation UK"); + REQUIRE(localization1.Get<Localization::CopyrightUrl>() == "https://www.microsoft.com/msixsdk/copyright/UK"); + REQUIRE(localization1.Get<Localization::ShortDescription>() == "This is MSIX SDK UK"); + REQUIRE(localization1.Get<Localization::Description>() == "The MSIX SDK project is an effort to enable developers UK"); + REQUIRE(localization1.Get<Localization::Tags>() == MultiValue{ "appxsdkUK", "msixsdkUK" }); + + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_1 }) + { + REQUIRE(localization1.Get<Localization::ReleaseNotes>() == "Release notes"); + REQUIRE(localization1.Get<Localization::ReleaseNotesUrl>() == "https://ReleaseNotes.net"); + REQUIRE(localization1.Get<Localization::Agreements>().size() == 1); + REQUIRE(localization1.Get<Localization::Agreements>().at(0).Label == "Label"); + REQUIRE(localization1.Get<Localization::Agreements>().at(0).AgreementText == "Text"); + REQUIRE(localization1.Get<Localization::Agreements>().at(0).AgreementUrl == "https://AgreementUrl.net"); + } + + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_2 }) + { + REQUIRE(localization1.Get<Localization::PurchaseUrl>() == "https://DefaultPurchaseUrl.com"); + REQUIRE(localization1.Get<Localization::InstallationNotes>() == "Default installation notes"); + REQUIRE(localization1.Get<Localization::Documentations>().size() == 1); + REQUIRE(localization1.Get<Localization::Documentations>().at(0).DocumentLabel == "Default document label"); + REQUIRE(localization1.Get<Localization::Documentations>().at(0).DocumentUrl == "https://DefaultDocumentUrl.com"); + } + + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_5 }) + { + REQUIRE(localization1.Get<Localization::Icons>().size() == 1); + REQUIRE(localization1.Get<Localization::Icons>().at(0).Url == "https://localeTestIcon-en-GB"); + REQUIRE(localization1.Get<Localization::Icons>().at(0).FileType == IconFileTypeEnum::Png); + REQUIRE(localization1.Get<Localization::Icons>().at(0).Resolution == IconResolutionEnum::Square32); + REQUIRE(localization1.Get<Localization::Icons>().at(0).Theme == IconThemeEnum::Light); + REQUIRE(localization1.Get<Localization::Icons>().at(0).Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8321")); + } + } + } + + struct ManifestShadowTestInfo + { + bool shadowDefaultLocale; + bool shadowEnGbLocale; + }; + + void VerifyV1ManifestContentCreatedWithShadow(const Manifest& manifest, ManifestShadowTestInfo shadowInfo, ManifestVer manifestVer = { s_ManifestVersionV1_5 }) + { + REQUIRE(manifest.Id == "microsoft.msixsdk"); + REQUIRE(manifest.Version == "1.7.32"); + REQUIRE(manifest.Installers.size() == 1); + + // Default localization + REQUIRE(manifest.DefaultLocalization.Locale == "en-US"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Publisher>() == "Microsoft"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PackageName>() == "MSIX SDK"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::License>() == "MIT License"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Description>() == "The MSIX SDK project is an effort to enable developers"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::ShortDescription>() == "This is MSIX SDK"); + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_5 }) + { + REQUIRE(manifest.DefaultLocalization.Get<Localization::Icons>().size() == 1); + + if (shadowInfo.shadowDefaultLocale) + { + REQUIRE(manifest.DefaultLocalization.Get<Localization::Icons>().at(0).Url == "https://shadowIcon-default"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Icons>().at(0).FileType == IconFileTypeEnum::Ico); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Icons>().at(0).Resolution == IconResolutionEnum::Custom); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Icons>().at(0).Theme == IconThemeEnum::Default); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Icons>().at(0).Sha256 == SHA256::ConvertToBytes("1111111111111111111111111111111111111111111111111111111111111111")); + } + else + { + REQUIRE(manifest.DefaultLocalization.Get<Localization::Icons>().size() == 1); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Icons>().at(0).Url == "https://testIcon-en-US"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Icons>().at(0).FileType == IconFileTypeEnum::Ico); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Icons>().at(0).Resolution == IconResolutionEnum::Custom); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Icons>().at(0).Theme == IconThemeEnum::Default); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Icons>().at(0).Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8123")); + } + } + + // Localization + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_5 }) + { + REQUIRE(manifest.Localizations.size() == 3); + + bool foundEnGbLocale = false; + bool foundfrFrLocale = false; + for (auto const& localization : manifest.Localizations) + { + if (localization.Locale == "en-GB") + { + REQUIRE(localization.Get<Localization::Description>() == "The MSIX SDK project is an effort to enable developers UK"); + if (shadowInfo.shadowEnGbLocale) + { + REQUIRE(localization.Get<Localization::Icons>().size() == 1); + REQUIRE(localization.Get<Localization::Icons>().at(0).Url == "https://shadowIcon-en-GB"); + REQUIRE(localization.Get<Localization::Icons>().at(0).FileType == IconFileTypeEnum::Png); + REQUIRE(localization.Get<Localization::Icons>().at(0).Resolution == IconResolutionEnum::Square32); + REQUIRE(localization.Get<Localization::Icons>().at(0).Theme == IconThemeEnum::Light); + REQUIRE(localization.Get<Localization::Icons>().at(0).Sha256 == SHA256::ConvertToBytes("2222222222222222222222222222222222222222222222222222222222222222")); + } + else + { + REQUIRE(localization.Get<Localization::Icons>().size() == 1); + REQUIRE(localization.Get<Localization::Icons>().at(0).Url == "https://localeTestIcon-en-GB"); + REQUIRE(localization.Get<Localization::Icons>().at(0).FileType == IconFileTypeEnum::Png); + REQUIRE(localization.Get<Localization::Icons>().at(0).Resolution == IconResolutionEnum::Square32); + REQUIRE(localization.Get<Localization::Icons>().at(0).Theme == IconThemeEnum::Light); + REQUIRE(localization.Get<Localization::Icons>().at(0).Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8321")); + } + + foundEnGbLocale = true; + } + else if (localization.Locale == "fr-FR") + { + REQUIRE(localization.Get<Localization::Icons>().size() == 1); + REQUIRE(localization.Get<Localization::Icons>().at(0).Url == "https://shadowIcon-fr-FR"); + REQUIRE(localization.Get<Localization::Icons>().at(0).FileType == IconFileTypeEnum::Jpeg); + REQUIRE(localization.Get<Localization::Icons>().at(0).Resolution == IconResolutionEnum::Square20); + REQUIRE(localization.Get<Localization::Icons>().at(0).Theme == IconThemeEnum::Dark); + REQUIRE(localization.Get<Localization::Icons>().at(0).Sha256 == SHA256::ConvertToBytes("3333333333333333333333333333333333333333333333333333333333333333")); + foundfrFrLocale = true; + } + else + { + REQUIRE(localization.Locale == "es-MX"); + REQUIRE(localization.Get<Localization::Description>() == "The MSIX SDK project is an effort to enable developers MX"); + REQUIRE(localization.Get<Localization::Icons>().size() == 1); + REQUIRE(localization.Get<Localization::Icons>().at(0).Url == "https://localeTestIcon-es-MX"); + REQUIRE(localization.Get<Localization::Icons>().at(0).FileType == IconFileTypeEnum::Png); + REQUIRE(localization.Get<Localization::Icons>().at(0).Resolution == IconResolutionEnum::Square32); + REQUIRE(localization.Get<Localization::Icons>().at(0).Theme == IconThemeEnum::Light); + REQUIRE(localization.Get<Localization::Icons>().at(0).Sha256 == SHA256::ConvertToBytes("4444444444444444444444444444444444444444444444444444444444444444")); + } + } + + REQUIRE(foundEnGbLocale); + REQUIRE(foundfrFrLocale); + } + } } TEST_CASE("ReadPreviewGoodManifestAndVerifyContents", "[ManifestValidation]") @@ -165,67 +737,6 @@ TEST_CASE("ReadGoodManifestWithSpaces", "[ManifestValidation]") REQUIRE(manifest.DefaultInstallerInfo.FileExtensions == MultiValue{ "appx", "appxbundle", "msix", "msixbundle" }); } -struct ManifestExceptionMatcher : public Catch::MatcherBase<ManifestException> -{ - ManifestExceptionMatcher(std::string expectedMessage, bool expectedWarningOnly = false) : - m_expectedMessage(expectedMessage), m_expectedWarningOnly(expectedWarningOnly) {} - - // Performs the test for this matcher - bool match(ManifestException const& e) const override - { - return e.GetManifestErrorMessage().find(m_expectedMessage) != std::string::npos && - e.IsWarningOnly() == m_expectedWarningOnly; - } - - virtual std::string describe() const override { - std::ostringstream ss; - ss << std::boolalpha << "Expected exception message: " << m_expectedMessage << " Expected IsWarningOnly: " << m_expectedWarningOnly; - return ss.str(); - } - -private: - std::string m_expectedMessage; - bool m_expectedWarningOnly; -}; - -ManifestValidateOption GetTestManifestValidateOption( - bool schemaValidationOnly = false, - bool errorOnVerifiedPublisher = false) -{ - ManifestValidateOption validateOption; - validateOption.FullValidation = true; - validateOption.ThrowOnWarning = true; - validateOption.SchemaValidationOnly = schemaValidationOnly; - validateOption.ErrorOnVerifiedPublisherFields = errorOnVerifiedPublisher; - return validateOption; -} - -void TestManifest( - const std::filesystem::path& manifestPath, - const std::string& expectedMessage = {}, - bool expectedWarningOnly = false, - ManifestValidateOption validateOption = GetTestManifestValidateOption()) -{ - INFO(manifestPath.u8string()); - - if (expectedMessage.empty()) - { - CHECK_NOTHROW(YamlParser::CreateFromPath(TestDataFile(manifestPath), validateOption)); - } - else - { - CHECK_THROWS_MATCHES(YamlParser::CreateFromPath(TestDataFile(manifestPath), validateOption), ManifestException, ManifestExceptionMatcher(expectedMessage, expectedWarningOnly)); - } -} - -struct ManifestTestCase -{ - std::string TestFile; - std::string ExpectedMessage = {}; - bool IsWarningOnly = false; - ManifestValidateOption ValidateOption = GetTestManifestValidateOption(); -}; - TEST_CASE("ReadGoodManifests", "[ManifestValidation]") { ManifestTestCase TestCases[] = @@ -386,411 +897,6 @@ TEST_CASE("ManifestVersionExtensions", "[ManifestValidation]") REQUIRE(ManifestVer("1.0.0-msstore.2-other"sv).HasExtension("msstore")); } -void CopyTestDataFilesToFolder(const std::vector<std::string>& testDataFiles, const std::filesystem::path& dest) -{ - for (const auto& fileName : testDataFiles) - { - std::filesystem::copy(TestDataFile(fileName), dest); - } -} - -void VerifyV1ManifestContent(const Manifest& manifest, bool isSingleton, ManifestVer manifestVer = { s_ManifestVersionV1 }, bool isExported = false) -{ - REQUIRE(manifest.Id == "microsoft.msixsdk"); - REQUIRE(manifest.Version == "1.7.32"); - REQUIRE(manifest.DefaultLocalization.Locale == "en-US"); - REQUIRE(manifest.DefaultLocalization.Get<Localization::Publisher>() == "Microsoft"); - REQUIRE(manifest.DefaultLocalization.Get<Localization::PublisherUrl>() == "https://www.microsoft.com"); - REQUIRE(manifest.DefaultLocalization.Get<Localization::PublisherSupportUrl>() == "https://www.microsoft.com/support"); - REQUIRE(manifest.DefaultLocalization.Get<Localization::PrivacyUrl>() == "https://www.microsoft.com/privacy"); - REQUIRE(manifest.DefaultLocalization.Get<Localization::Author>() == "Microsoft"); - REQUIRE(manifest.DefaultLocalization.Get<Localization::PackageName>() == "MSIX SDK"); - REQUIRE(manifest.DefaultLocalization.Get<Localization::PackageUrl>() == "https://www.microsoft.com/msixsdk/home"); - REQUIRE(manifest.DefaultLocalization.Get<Localization::License>() == "MIT License"); - REQUIRE(manifest.DefaultLocalization.Get<Localization::LicenseUrl>() == "https://www.microsoft.com/msixsdk/license"); - REQUIRE(manifest.DefaultLocalization.Get<Localization::Copyright>() == "Copyright Microsoft Corporation"); - REQUIRE(manifest.DefaultLocalization.Get<Localization::CopyrightUrl>() == "https://www.microsoft.com/msixsdk/copyright"); - REQUIRE(manifest.DefaultLocalization.Get<Localization::ShortDescription>() == "This is MSIX SDK"); - 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"); - } - - if (manifestVer >= ManifestVer{ s_ManifestVersionV1_2 }) - { - REQUIRE(manifest.DefaultLocalization.Get<Localization::PurchaseUrl>() == "https://DefaultPurchaseUrl.com"); - REQUIRE(manifest.DefaultLocalization.Get<Localization::InstallationNotes>() == "Default installation notes"); - REQUIRE(manifest.DefaultLocalization.Get<Localization::Documentations>().size() == 1); - REQUIRE(manifest.DefaultLocalization.Get<Localization::Documentations>().at(0).DocumentLabel == "Default document label"); - REQUIRE(manifest.DefaultLocalization.Get<Localization::Documentations>().at(0).DocumentUrl == "https://DefaultDocumentUrl.com"); - } - - if (manifestVer >= ManifestVer{ s_ManifestVersionV1_5 }) - { - REQUIRE(manifest.DefaultLocalization.Get<Localization::Icons>().size() == 1); - REQUIRE(manifest.DefaultLocalization.Get<Localization::Icons>().at(0).Url == "https://testIcon"); - REQUIRE(manifest.DefaultLocalization.Get<Localization::Icons>().at(0).FileType == IconFileTypeEnum::Ico); - REQUIRE(manifest.DefaultLocalization.Get<Localization::Icons>().at(0).Resolution == IconResolutionEnum::Custom); - REQUIRE(manifest.DefaultLocalization.Get<Localization::Icons>().at(0).Theme == IconThemeEnum::Default); - REQUIRE(manifest.DefaultLocalization.Get<Localization::Icons>().at(0).Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8123")); - } - - if (!isExported) - { - 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"); - REQUIRE(manifest.DefaultInstallerInfo.BaseInstallerType == InstallerTypeEnum::Exe); - REQUIRE(manifest.DefaultInstallerInfo.Scope == ScopeEnum::Machine); - REQUIRE(manifest.DefaultInstallerInfo.InstallModes == std::vector<InstallModeEnum>{ InstallModeEnum::Interactive, InstallModeEnum::Silent, InstallModeEnum::SilentWithProgress }); - - auto defaultSwitches = manifest.DefaultInstallerInfo.Switches; - REQUIRE(defaultSwitches.at(InstallerSwitchType::Custom) == "/custom"); - REQUIRE(defaultSwitches.at(InstallerSwitchType::SilentWithProgress) == "/silentwithprogress"); - REQUIRE(defaultSwitches.at(InstallerSwitchType::Silent) == "/silence"); - REQUIRE(defaultSwitches.at(InstallerSwitchType::Interactive) == "/interactive"); - REQUIRE(defaultSwitches.at(InstallerSwitchType::Log) == "/log=<LOGPATH>"); - REQUIRE(defaultSwitches.at(InstallerSwitchType::InstallLocation) == "/dir=<INSTALLPATH>"); - REQUIRE(defaultSwitches.at(InstallerSwitchType::Update) == "/upgrade"); - - REQUIRE(manifest.DefaultInstallerInfo.InstallerSuccessCodes == std::vector<DWORD>{ 1, static_cast<DWORD>(0x80070005) }); - REQUIRE(manifest.DefaultInstallerInfo.UpdateBehavior == UpdateBehaviorEnum::UninstallPrevious); - REQUIRE(manifest.DefaultInstallerInfo.Commands == MultiValue{ "makemsix", "makeappx" }); - REQUIRE(manifest.DefaultInstallerInfo.Protocols == MultiValue{ "protocol1", "protocol2" }); - REQUIRE(manifest.DefaultInstallerInfo.FileExtensions == MultiValue{ "appx", "msix", "appxbundle", "msixbundle" }); - - auto dependencies = manifest.DefaultInstallerInfo.Dependencies; - REQUIRE(dependencies.HasExactDependency(DependencyType::WindowsFeature, "IIS")); - REQUIRE(dependencies.HasExactDependency(DependencyType::WindowsLibrary, "VC Runtime")); - REQUIRE(dependencies.HasExactDependency(DependencyType::Package, "Microsoft.MsixSdkDep", "1.0.0")); - REQUIRE(dependencies.HasExactDependency(DependencyType::External, "Outside dependencies")); - REQUIRE(dependencies.Size() == 4); - - REQUIRE(manifest.DefaultInstallerInfo.Capabilities == MultiValue{ "internetClient" }); - REQUIRE(manifest.DefaultInstallerInfo.RestrictedCapabilities == MultiValue{ "runFullTrust" }); - 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"); - REQUIRE(manifest.DefaultInstallerInfo.ExpectedReturnCodes.size() == 1); - REQUIRE(manifest.DefaultInstallerInfo.ExpectedReturnCodes.at(10).ReturnResponseEnum == ExpectedReturnCodeEnum::PackageInUse); - } - - if (manifestVer >= ManifestVer{ s_ManifestVersionV1_2 }) - { - REQUIRE(manifest.DefaultInstallerInfo.DisplayInstallWarnings); - REQUIRE(manifest.DefaultInstallerInfo.UnsupportedArguments.size() == 1); - REQUIRE(manifest.DefaultInstallerInfo.UnsupportedArguments.at(0) == UnsupportedArgumentEnum::Log); - } - - if (manifestVer >= ManifestVer{ s_ManifestVersionV1_4 }) - { - REQUIRE(manifest.DefaultInstallerInfo.NestedInstallerType == InstallerTypeEnum::Msi); - REQUIRE(manifest.DefaultInstallerInfo.NestedInstallerFiles.size() == 1); - REQUIRE(manifest.DefaultInstallerInfo.NestedInstallerFiles.at(0).RelativeFilePath == "RelativeFilePath"); - REQUIRE(manifest.DefaultInstallerInfo.NestedInstallerFiles.at(0).PortableCommandAlias == "PortableCommandAlias"); - REQUIRE(manifest.DefaultInstallerInfo.InstallationMetadata.DefaultInstallLocation == "%ProgramFiles%\\TestApp"); - REQUIRE(manifest.DefaultInstallerInfo.InstallationMetadata.Files.size() == 1); - REQUIRE(manifest.DefaultInstallerInfo.InstallationMetadata.Files.at(0).RelativeFilePath == "main.exe"); - REQUIRE(manifest.DefaultInstallerInfo.InstallationMetadata.Files.at(0).FileType == InstalledFileTypeEnum::Launch); - REQUIRE(manifest.DefaultInstallerInfo.InstallationMetadata.Files.at(0).FileSha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); - REQUIRE(manifest.DefaultInstallerInfo.InstallationMetadata.Files.at(0).InvocationParameter == "/arg"); - } - - if (manifestVer >= ManifestVer{ s_ManifestVersionV1_6 }) - { - REQUIRE(manifest.DefaultInstallerInfo.DownloadCommandProhibited); - } - - if (manifestVer >= ManifestVer{ s_ManifestVersionV1_7 }) - { - REQUIRE(defaultSwitches.at(InstallerSwitchType::Repair) == "/repair"); - REQUIRE(manifest.DefaultInstallerInfo.RepairBehavior == RepairBehaviorEnum::Modify); - } - } - - if (isSingleton || isExported) - { - REQUIRE(manifest.Installers.size() == 1); - } - else - { - if (manifestVer >= ManifestVer{ s_ManifestVersionV1_7 }) - { - REQUIRE(manifest.Installers.size() == 5); - } - else if (manifestVer >= ManifestVer{ s_ManifestVersionV1_4 }) - { - REQUIRE(manifest.Installers.size() == 4); - } - else if (manifestVer == ManifestVer{ s_ManifestVersionV1_2 }) - { - REQUIRE(manifest.Installers.size() == 3); - } - else - { - REQUIRE(manifest.Installers.size() == 2); - } - } - - ManifestInstaller installer1 = manifest.Installers.at(0); - REQUIRE(installer1.Arch == Architecture::X86); - REQUIRE(installer1.Locale == "en-GB"); - REQUIRE(installer1.Platform == std::vector<PlatformEnum>{ PlatformEnum::Desktop }); - REQUIRE(installer1.MinOSVersion == "10.0.1.0"); - REQUIRE(installer1.BaseInstallerType == InstallerTypeEnum::Msix); - REQUIRE(installer1.Url == "https://www.microsoft.com/msixsdk/msixsdkx86.msix"); - REQUIRE(installer1.Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); - REQUIRE(installer1.SignatureSha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); - REQUIRE(installer1.Scope == ScopeEnum::User); - REQUIRE(installer1.InstallModes == std::vector<InstallModeEnum>{ InstallModeEnum::Interactive }); - - auto installer1Switches = installer1.Switches; - REQUIRE(installer1Switches.at(InstallerSwitchType::Custom) == "/c"); - REQUIRE(installer1Switches.at(InstallerSwitchType::SilentWithProgress) == "/sp"); - REQUIRE(installer1Switches.at(InstallerSwitchType::Silent) == "/s"); - REQUIRE(installer1Switches.at(InstallerSwitchType::Interactive) == "/i"); - REQUIRE(installer1Switches.at(InstallerSwitchType::Log) == "/l=<LOGPATH>"); - REQUIRE(installer1Switches.at(InstallerSwitchType::InstallLocation) == "/d=<INSTALLPATH>"); - REQUIRE(installer1Switches.at(InstallerSwitchType::Update) == "/u"); - - REQUIRE(installer1.UpdateBehavior == UpdateBehaviorEnum::Install); - REQUIRE(installer1.Commands == MultiValue{ "makemsixPreview", "makeappxPreview" }); - REQUIRE(installer1.Protocols == MultiValue{ "protocol1preview", "protocol2preview" }); - REQUIRE(installer1.FileExtensions == MultiValue{ "appxbundle", "msixbundle", "appx", "msix" }); - - auto installer1Dependencies = installer1.Dependencies; - REQUIRE(installer1Dependencies.HasExactDependency(DependencyType::WindowsFeature, "PreviewIIS")); - REQUIRE(installer1Dependencies.HasExactDependency(DependencyType::WindowsLibrary, "Preview VC Runtime")); - REQUIRE(installer1Dependencies.HasExactDependency(DependencyType::Package, "Microsoft.MsixSdkDepPreview", "1.0.0")); - REQUIRE(installer1Dependencies.HasExactDependency(DependencyType::External, "Preview Outside dependencies")); - REQUIRE(installer1Dependencies.Size() == 4); - - REQUIRE(installer1.Capabilities == MultiValue{ "internetClientPreview" }); - 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"); - REQUIRE(installer1.ExpectedReturnCodes.at(2).ReturnResponseEnum == ExpectedReturnCodeEnum::ContactSupport); - } - - if (manifestVer >= ManifestVer{ s_ManifestVersionV1_2 }) - { - REQUIRE_FALSE(installer1.DisplayInstallWarnings); - REQUIRE(installer1.ExpectedReturnCodes.at(3).ReturnResponseEnum == ExpectedReturnCodeEnum::Custom); - REQUIRE(installer1.ExpectedReturnCodes.at(3).ReturnResponseUrl == "https://defaultReturnResponseUrl.com"); - REQUIRE(installer1.UnsupportedArguments.size() == 1); - REQUIRE(installer1.UnsupportedArguments.at(0) == UnsupportedArgumentEnum::Location); - } - - if (manifestVer >= ManifestVer{ s_ManifestVersionV1_4 }) - { - // NestedInstaller metadata should not be populated unless the InstallerType is zip. - REQUIRE(installer1.NestedInstallerType == InstallerTypeEnum::Unknown); - REQUIRE(installer1.NestedInstallerFiles.size() == 0); - - REQUIRE(installer1.InstallationMetadata.DefaultInstallLocation == "%ProgramFiles%\\TestApp"); - REQUIRE(installer1.InstallationMetadata.Files.size() == 1); - REQUIRE(installer1.InstallationMetadata.Files.at(0).RelativeFilePath == "main.exe"); - REQUIRE(installer1.InstallationMetadata.Files.at(0).FileType == InstalledFileTypeEnum::Launch); - REQUIRE(installer1.InstallationMetadata.Files.at(0).FileSha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); - REQUIRE(installer1.InstallationMetadata.Files.at(0).InvocationParameter == "/arg"); - REQUIRE(installer1.InstallationMetadata.Files.at(0).DisplayName == "DisplayName"); - } - - if (manifestVer >= ManifestVer{ s_ManifestVersionV1_6 }) - { - REQUIRE_FALSE(installer1.DownloadCommandProhibited); - } - - if (manifestVer >= ManifestVer { s_ManifestVersionV1_7}) - { - REQUIRE(installer1.Switches.at(InstallerSwitchType::Repair) == "/r"); - REQUIRE(installer1.RepairBehavior == RepairBehaviorEnum::Modify); - } - - if (!isSingleton) - { - if (!isExported) - { - ManifestInstaller installer2 = manifest.Installers.at(1); - REQUIRE(installer2.BaseInstallerType == InstallerTypeEnum::Exe); - REQUIRE(installer2.Arch == Architecture::X64); - REQUIRE(installer2.Url == "https://www.microsoft.com/msixsdk/msixsdkx64.exe"); - 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"); - REQUIRE(installer2.ExpectedReturnCodes.size() == 1); - REQUIRE(installer2.ExpectedReturnCodes.at(10).ReturnResponseEnum == ExpectedReturnCodeEnum::PackageInUse); - } - - if (manifestVer >= ManifestVer{ s_ManifestVersionV1_2 }) - { - ManifestInstaller installer3 = manifest.Installers.at(2); - REQUIRE(installer3.BaseInstallerType == InstallerTypeEnum::Portable); - REQUIRE(installer3.Arch == Architecture::X86); - REQUIRE(installer3.Url == "https://www.microsoft.com/msixsdk/msixsdkx86.exe"); - REQUIRE(installer3.Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); - REQUIRE(installer3.Commands == MultiValue{ "standalone" }); - REQUIRE(installer3.ExpectedReturnCodes.size() == 1); - REQUIRE(installer3.ExpectedReturnCodes.at(11).ReturnResponseEnum == ExpectedReturnCodeEnum::Custom); - REQUIRE(installer3.ExpectedReturnCodes.at(11).ReturnResponseUrl == "https://defaultReturnResponseUrl.com"); - REQUIRE_FALSE(installer3.DisplayInstallWarnings); - REQUIRE(installer3.UnsupportedArguments.size() == 1); - REQUIRE(installer3.UnsupportedArguments.at(0) == UnsupportedArgumentEnum::Log); - } - - if (manifestVer >= ManifestVer{ s_ManifestVersionV1_4 }) - { - ManifestInstaller installer4 = manifest.Installers.at(3); - REQUIRE(installer4.BaseInstallerType == InstallerTypeEnum::Zip); - REQUIRE(installer4.Arch == Architecture::X64); - REQUIRE(installer4.Url == "https://www.microsoft.com/msixsdk/msixsdkx64.exe"); - REQUIRE(installer4.Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); - REQUIRE(installer4.ProductCode == "{Foo}"); - REQUIRE(installer4.NestedInstallerType == InstallerTypeEnum::Portable); - REQUIRE(installer4.NestedInstallerFiles.size() == 2); - REQUIRE(installer4.NestedInstallerFiles.at(0).RelativeFilePath == "relativeFilePath1"); - REQUIRE(installer4.NestedInstallerFiles.at(0).PortableCommandAlias == "portableAlias1"); - REQUIRE(installer4.NestedInstallerFiles.at(1).RelativeFilePath == "relativeFilePath2"); - REQUIRE(installer4.NestedInstallerFiles.at(1).PortableCommandAlias == "portableAlias2"); - REQUIRE(installer4.InstallationMetadata.DefaultInstallLocation == "%ProgramFiles%\\TestApp2"); - REQUIRE(installer4.InstallationMetadata.Files.size() == 1); - REQUIRE(installer4.InstallationMetadata.Files.at(0).RelativeFilePath == "main2.exe"); - REQUIRE(installer4.InstallationMetadata.Files.at(0).FileType == InstalledFileTypeEnum::Other); - REQUIRE(installer4.InstallationMetadata.Files.at(0).FileSha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); - REQUIRE(installer4.InstallationMetadata.Files.at(0).InvocationParameter == "/arg2"); - REQUIRE(installer4.InstallationMetadata.Files.at(0).DisplayName == "DisplayName2"); - } - - if (manifestVer >= ManifestVer{ s_ManifestVersionV1_6 }) - { - REQUIRE(installer2.DownloadCommandProhibited); - REQUIRE(installer2.UpdateBehavior == UpdateBehaviorEnum::Deny); - } - - if (manifestVer >= ManifestVer{ s_ManifestVersionV1_7 }) - { - REQUIRE(installer2.RepairBehavior == RepairBehaviorEnum::Uninstaller); - REQUIRE(installer2.Switches.at(InstallerSwitchType::Repair) == "/r"); - - ManifestInstaller installer5 = manifest.Installers.at(4); - REQUIRE(installer5.BaseInstallerType == InstallerTypeEnum::Burn); - REQUIRE(installer5.Arch == Architecture::X64); - REQUIRE(installer5.Url == "https://www.microsoft.com/msixsdk/msixsdkx64.exe"); - REQUIRE(installer5.Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); - REQUIRE(installer5.ProductCode == "{Bar}"); - REQUIRE(installer5.Switches.at(InstallerSwitchType::Repair) == "/repair"); - REQUIRE(installer5.RepairBehavior == RepairBehaviorEnum::Modify); - } - } - - // Localization - REQUIRE(manifest.Localizations.size() == 1); - ManifestLocalization localization1 = manifest.Localizations.at(0); - REQUIRE(localization1.Locale == "en-GB"); - REQUIRE(localization1.Get<Localization::Publisher>() == "Microsoft UK"); - REQUIRE(localization1.Get<Localization::PublisherUrl>() == "https://www.microsoft.com/UK"); - REQUIRE(localization1.Get<Localization::PublisherSupportUrl>() == "https://www.microsoft.com/support/UK"); - REQUIRE(localization1.Get<Localization::PrivacyUrl>() == "https://www.microsoft.com/privacy/UK"); - REQUIRE(localization1.Get<Localization::Author>() == "Microsoft UK"); - REQUIRE(localization1.Get<Localization::PackageName>() == "MSIX SDK UK"); - REQUIRE(localization1.Get<Localization::PackageUrl>() == "https://www.microsoft.com/msixsdk/home/UK"); - REQUIRE(localization1.Get<Localization::License>() == "MIT License UK"); - REQUIRE(localization1.Get<Localization::LicenseUrl>() == "https://www.microsoft.com/msixsdk/license/UK"); - REQUIRE(localization1.Get<Localization::Copyright>() == "Copyright Microsoft Corporation UK"); - REQUIRE(localization1.Get<Localization::CopyrightUrl>() == "https://www.microsoft.com/msixsdk/copyright/UK"); - REQUIRE(localization1.Get<Localization::ShortDescription>() == "This is MSIX SDK UK"); - REQUIRE(localization1.Get<Localization::Description>() == "The MSIX SDK project is an effort to enable developers UK"); - REQUIRE(localization1.Get<Localization::Tags>() == MultiValue{ "appxsdkUK", "msixsdkUK" }); - - if (manifestVer >= ManifestVer{ s_ManifestVersionV1_1 }) - { - REQUIRE(localization1.Get<Localization::ReleaseNotes>() == "Release notes"); - REQUIRE(localization1.Get<Localization::ReleaseNotesUrl>() == "https://ReleaseNotes.net"); - REQUIRE(localization1.Get<Localization::Agreements>().size() == 1); - REQUIRE(localization1.Get<Localization::Agreements>().at(0).Label == "Label"); - REQUIRE(localization1.Get<Localization::Agreements>().at(0).AgreementText == "Text"); - REQUIRE(localization1.Get<Localization::Agreements>().at(0).AgreementUrl == "https://AgreementUrl.net"); - } - - if (manifestVer >= ManifestVer{ s_ManifestVersionV1_2 }) - { - REQUIRE(localization1.Get<Localization::PurchaseUrl>() == "https://DefaultPurchaseUrl.com"); - REQUIRE(localization1.Get<Localization::InstallationNotes>() == "Default installation notes"); - REQUIRE(localization1.Get<Localization::Documentations>().size() == 1); - REQUIRE(localization1.Get<Localization::Documentations>().at(0).DocumentLabel == "Default document label"); - REQUIRE(localization1.Get<Localization::Documentations>().at(0).DocumentUrl == "https://DefaultDocumentUrl.com"); - } - - if (manifestVer >= ManifestVer{ s_ManifestVersionV1_5 }) - { - REQUIRE(localization1.Get<Localization::Icons>().size() == 1); - REQUIRE(localization1.Get<Localization::Icons>().at(0).Url == "https://localeTestIcon"); - REQUIRE(localization1.Get<Localization::Icons>().at(0).FileType == IconFileTypeEnum::Png); - REQUIRE(localization1.Get<Localization::Icons>().at(0).Resolution == IconResolutionEnum::Square32); - REQUIRE(localization1.Get<Localization::Icons>().at(0).Theme == IconThemeEnum::Light); - REQUIRE(localization1.Get<Localization::Icons>().at(0).Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8321")); - } - } -} - TEST_CASE("ValidateV1GoodManifestAndVerifyContents", "[ManifestValidation]") { ManifestValidateOption validateOption; @@ -1543,6 +1649,147 @@ TEST_CASE("ManifestArpVersionRange", "[ManifestValidation]") REQUIRE(arpRangeMultiArp.GetMaxVersion().ToString() == "13.0"); } +TEST_CASE("ShadowManifest", "[ShadowManifest]") +{ + ManifestValidateOption validateOption; + validateOption.FullValidation = true; + validateOption.AllowShadowManifest = true; + + TempDirectory multiFileDirectory{ "MultiFileManifest" }; + CopyTestDataFilesToFolder({ + "ManifestV1_5-MultiFile-Version.yaml", + "ManifestV1_5-Shadow-Installer.yaml", + "ManifestV1_5-Shadow-DefaultLocale.yaml", + "ManifestV1_5-Shadow-Locale.yaml", + "ManifestV1_5-Shadow-Locale2.yaml", + "ManifestV1_5-Shadow-Shadow.yaml" }, multiFileDirectory); + + auto shadowInfo = ManifestShadowTestInfo{}; + shadowInfo.shadowDefaultLocale = true; + shadowInfo.shadowEnGbLocale = true; + + TempFile mergedManifestFile{ "merged.yaml" }; + Manifest multiFileManifest = YamlParser::CreateFromPath(multiFileDirectory, validateOption, mergedManifestFile); + VerifyV1ManifestContentCreatedWithShadow(multiFileManifest, shadowInfo); + + // Read from merged manifest should have the same content as multi file manifest + Manifest mergedManifest = YamlParser::CreateFromPath(mergedManifestFile); + VerifyV1ManifestContentCreatedWithShadow(mergedManifest, shadowInfo); +} + +TEST_CASE("ShadowManifest_SkipShadowDefaultLocale", "[ShadowManifest]") +{ + ManifestValidateOption validateOption; + validateOption.FullValidation = true; + validateOption.AllowShadowManifest = true; + + TempDirectory multiFileDirectory{ "MultiFileManifest" }; + CopyTestDataFilesToFolder({ + "ManifestV1_5-MultiFile-Version.yaml", + "ManifestV1_5-Shadow-Installer.yaml", + "ManifestV1_5-MultiFile-DefaultLocale.yaml", + "ManifestV1_5-Shadow-Locale.yaml", + "ManifestV1_5-Shadow-Locale2.yaml", + "ManifestV1_5-Shadow-Shadow.yaml" }, multiFileDirectory); + + auto shadowInfo = ManifestShadowTestInfo{}; + shadowInfo.shadowDefaultLocale = false; + shadowInfo.shadowEnGbLocale = true; + + TempFile mergedManifestFile{ "merged.yaml" }; + Manifest multiFileManifest = YamlParser::CreateFromPath(multiFileDirectory, validateOption, mergedManifestFile); + VerifyV1ManifestContentCreatedWithShadow(multiFileManifest, shadowInfo); + + // Read from merged manifest should have the same content as multi file manifest + Manifest mergedManifest = YamlParser::CreateFromPath(mergedManifestFile); + VerifyV1ManifestContentCreatedWithShadow(mergedManifest, shadowInfo); +} + +TEST_CASE("ShadowManifest_SkipShadowLocalizationLocale", "[ShadowManifest]") +{ + ManifestValidateOption validateOption; + validateOption.FullValidation = true; + validateOption.AllowShadowManifest = true; + + TempDirectory multiFileDirectory{ "MultiFileManifest" }; + CopyTestDataFilesToFolder({ + "ManifestV1_5-MultiFile-Version.yaml", + "ManifestV1_5-Shadow-Installer.yaml", + "ManifestV1_5-Shadow-DefaultLocale.yaml", + "ManifestV1_5-MultiFile-Locale.yaml", + "ManifestV1_5-Shadow-Locale2.yaml", + "ManifestV1_5-Shadow-Shadow.yaml" }, multiFileDirectory); + + auto shadowInfo = ManifestShadowTestInfo{}; + shadowInfo.shadowDefaultLocale = true; + shadowInfo.shadowEnGbLocale = false; + + TempFile mergedManifestFile{ "merged.yaml" }; + Manifest multiFileManifest = YamlParser::CreateFromPath(multiFileDirectory, validateOption, mergedManifestFile); + VerifyV1ManifestContentCreatedWithShadow(multiFileManifest, shadowInfo); + + // Read from merged manifest should have the same content as multi file manifest + Manifest mergedManifest = YamlParser::CreateFromPath(mergedManifestFile); + VerifyV1ManifestContentCreatedWithShadow(mergedManifest, shadowInfo); +} + +TEST_CASE("ShadowManifest_ShadowNotAllowed", "[ShadowManifest]") +{ + ManifestValidateOption validateOption; + validateOption.FullValidation = true; + validateOption.AllowShadowManifest = false; + + TempDirectory multiFileDirectory{ "MultiFileManifest" }; + CopyTestDataFilesToFolder({ + "ManifestV1_5-MultiFile-Version.yaml", + "ManifestV1_5-Shadow-Installer.yaml", + "ManifestV1_5-Shadow-DefaultLocale.yaml", + "ManifestV1_5-Shadow-Locale.yaml", + "ManifestV1_5-Shadow-Locale2.yaml", + "ManifestV1_5-Shadow-Shadow.yaml" }, multiFileDirectory); + + TempFile mergedManifestFile{ "merged.yaml" }; + REQUIRE_THROWS_MATCHES(YamlParser::CreateFromPath(multiFileDirectory, validateOption, mergedManifestFile), ManifestException, ManifestExceptionMatcher("Shadow manifest is not allowed. [ManifestType] Value: shadow File: ManifestV1_5-Shadow-Shadow.yaml")); +} + +TEST_CASE("ShadowManifest_TwoShadowFiles", "[ShadowManifest]") +{ + ManifestValidateOption validateOption; + validateOption.FullValidation = true; + validateOption.AllowShadowManifest = true; + + TempDirectory multiFileDirectory{ "MultiFileManifest" }; + CopyTestDataFilesToFolder({ + "ManifestV1_5-MultiFile-Version.yaml", + "ManifestV1_5-Shadow-Installer.yaml", + "ManifestV1_5-Shadow-DefaultLocale.yaml", + "ManifestV1_5-Shadow-Shadow.yaml", + "ManifestV1_5-Shadow-Shadow2.yaml" }, multiFileDirectory); + + TempFile mergedManifestFile{ "merged.yaml" }; + REQUIRE_THROWS_MATCHES(YamlParser::CreateFromPath(multiFileDirectory, validateOption, mergedManifestFile), ManifestException, ManifestExceptionMatcher("The multi file manifest should contain only one file with the particular ManifestType. [ManifestType] Value: shadow File: ManifestV1_5-Shadow-Shadow2.yaml")); +} + +TEST_CASE("ShadowManifest_NotVerifiedPublisher", "[ShadowManifest]") +{ + ManifestValidateOption validateOption; + validateOption.FullValidation = true; + validateOption.AllowShadowManifest = true; + validateOption.ErrorOnVerifiedPublisherFields = true; + + TempDirectory multiFileDirectory{ "MultiFileManifest" }; + CopyTestDataFilesToFolder({ + "ManifestV1_5-MultiFile-Version.yaml", + "ManifestV1_5-Shadow-Installer.yaml", + "ManifestV1_5-Shadow-DefaultLocale.yaml", + "ManifestV1_5-Shadow-Locale.yaml", + "ManifestV1_5-Shadow-Locale2.yaml", + "ManifestV1_5-Shadow-Shadow.yaml" }, multiFileDirectory); + + TempFile mergedManifestFile{ "merged.yaml" }; + REQUIRE_THROWS_MATCHES(YamlParser::CreateFromPath(multiFileDirectory, validateOption, mergedManifestFile), ManifestException, ManifestExceptionMatcher("Field usage requires verified publishers. [Icons]")); +} + TEST_CASE("YamlParserTypes", "[YAML]") { auto document = AppInstaller::YAML::Load(TestDataFile("Node-Types.yaml")); @@ -1571,3 +1818,133 @@ TEST_CASE("YamlParserTypes", "[YAML]") auto localTag = document["LocalTag"]; CHECK(localTag.GetTagType() == Node::TagType::Unknown); } + +TEST_CASE("YamlMergeMappingNode", "[YAML]") +{ + auto document = Load(TestDataFile("Node-Mapping.yaml")); + + auto mergeNode = document["MergeNode"]; + auto mergeNode2 = document["MergeNode2"]; + + REQUIRE(3 == mergeNode.size()); + REQUIRE(2 == mergeNode2.size()); + + mergeNode.MergeMappingNode(mergeNode2); + + REQUIRE(5 == mergeNode.size()); +} + +TEST_CASE("YamlMergeMappingNode_CaseInsensitive", "[YAML]") +{ + auto document = Load(TestDataFile("Node-Mapping.yaml")); + + auto mergeNode = document["MergeNode"]; + auto mergeNode2 = document["MergeNode2"]; + + REQUIRE(3 == mergeNode.size()); + REQUIRE(2 == mergeNode2.size()); + + mergeNode.MergeMappingNode(mergeNode2, true); + + REQUIRE(4 == mergeNode.size()); +} + +TEST_CASE("YamlMergeSequenceNode", "[YAML]") +{ + auto document = Load(TestDataFile("Node-Merge.yaml")); + auto document2 = Load(TestDataFile("Node-Merge2.yaml")); + + REQUIRE(3 == document["StrawHats"].size()); + REQUIRE(2 == document2["StrawHats"].size()); + + // Internally will call MergeMappingNode. + document["StrawHats"].MergeSequenceNode(document2["StrawHats"], "Bounty"); + REQUIRE(5 == document["StrawHats"].size()); +} + +TEST_CASE("YamlMergeSequenceNode_CaseInsensitive", "[YAML]") +{ + auto document = Load(TestDataFile("Node-Merge.yaml")); + auto document2 = Load(TestDataFile("Node-Merge2.yaml")); + + REQUIRE(3 == document["StrawHats"].size()); + REQUIRE(2 == document2["StrawHats"].size()); + + // Internally will call MergeMappingNode. + document["StrawHats"].MergeSequenceNode(document2["StrawHats"], "Name", true); + REQUIRE(4 == document["StrawHats"].size()); + + auto luffy = std::find_if( + document["StrawHats"].Sequence().begin(), + document["StrawHats"].Sequence().end(), + [](auto const& n) { return n["Name"].as<std::string>() == "Monkey D Luffy"; }); + REQUIRE(luffy != document["StrawHats"].Sequence().end()); + + // From original node + REQUIRE((*luffy)["Bounty"].as<std::string>() == "3,000,000,000"); + + // From merged node + REQUIRE((*luffy)["Fruit"].as<std::string>() == "Gomu Gomu no Mi"); +} + +TEST_CASE("YamlMergeNode_MergeSequenceNoKey", "[YAML]") +{ + auto document = Load(TestDataFile("Node-Merge.yaml")); + auto document2 = Load(TestDataFile("Node-Merge2.yaml")); + + REQUIRE_THROWS_HR(document["StrawHats"].MergeSequenceNode(document2["StrawHats"], "Power"), APPINSTALLER_CLI_ERROR_YAML_INVALID_DATA); +} + +TEST_CASE("YamlMappingNode", "[YAML]") +{ + auto document = Load(TestDataFile("Node-Mapping.yaml")); + + auto node = document["key"]; + REQUIRE(node.as<std::string>() == "value"); + + auto node2 = document.GetChildNode("KEY"); + REQUIRE(node2.as<std::string>() == "value"); + + auto node3 = document.GetChildNode("key"); + REQUIRE(node3.as<std::string>() == "value"); + + auto node4 = document.GetChildNode("kEy"); + REQUIRE(node4.as<std::string>() == "value"); + + auto node5 = document.GetChildNode("fake"); + REQUIRE(node5.IsNull()); + + auto node6 = document["repeatedkey"]; + REQUIRE(node6.as<std::string>() == "repeated value"); + REQUIRE_THROWS_HR(document.GetChildNode("repeatedkey"), APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); + + REQUIRE_THROWS_HR(document.GetChildNode("RepeatedKey"), APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); + REQUIRE_THROWS_HR(document["RepeatedKey"], APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); +} + +TEST_CASE("YamlMappingNode_const", "[YAML]") +{ + const auto document = Load(TestDataFile("Node-Mapping.yaml")); + + auto node = document["key"]; + REQUIRE(node.as<std::string>() == "value"); + + auto node2 = document.GetChildNode("KEY"); + REQUIRE(node2.as<std::string>() == "value"); + + auto node3 = document.GetChildNode("key"); + REQUIRE(node3.as<std::string>() == "value"); + + auto node4 = document.GetChildNode("kEy"); + REQUIRE(node4.as<std::string>() == "value"); + + auto node5 = document.GetChildNode("fake"); + REQUIRE(node5.IsNull()); + + auto node6 = document["repeatedkey"]; + REQUIRE(node6.as<std::string>() == "repeated value"); + REQUIRE_THROWS_HR(document.GetChildNode("repeatedkey"), APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); + + REQUIRE_THROWS_HR(document.GetChildNode("RepeatedKey"), APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); + REQUIRE_THROWS_HR(document["RepeatedKey"], APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY); +} diff --git a/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp @@ -300,6 +300,10 @@ namespace AppInstaller::Manifest { return ManifestTypeEnum::Merged; } + else if (in == "shadow") + { + return ManifestTypeEnum::Shadow; + } else { THROW_HR_MSG(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), "Unsupported ManifestType: %hs", in.c_str()); diff --git a/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp @@ -203,6 +203,12 @@ namespace AppInstaller::Manifest::YamlParser for (const auto& entry : manifestList) { + if (entry.ManifestType == ManifestTypeEnum::Shadow) + { + // There's no schema for a shadow manifest. + continue; + } + if (schemaList.find(entry.ManifestType) == schemaList.end()) { // Copy constructor of valijson::Schema was private diff --git a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp @@ -62,7 +62,8 @@ namespace AppInstaller::Manifest { AppInstaller::Manifest::ManifestError::RelativeFilePathEscapesDirectory, "Relative file path must not point to a location outside of archive directory."sv }, { AppInstaller::Manifest::ManifestError::ArpValidationError, "Arp Validation Error."sv }, { AppInstaller::Manifest::ManifestError::SchemaError, "Schema Error."sv }, - { AppInstaller::Manifest::ManifestError::MsixSignatureHashFailed, "Failed to calculate MSIX signature hash.Please verify that the input file is a valid, signed MSIX."sv } + { AppInstaller::Manifest::ManifestError::MsixSignatureHashFailed, "Failed to calculate MSIX signature hash.Please verify that the input file is a valid, signed MSIX."sv }, + { AppInstaller::Manifest::ManifestError::ShadowManifestNotAllowed, "Shadow manifest is not allowed."} }; return ErrorIdToMessageMap; diff --git a/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp b/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp @@ -168,7 +168,7 @@ namespace AppInstaller::Manifest } } - std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetRootFieldProcessInfo(const ManifestVer& manifestVersion) + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetRootFieldProcessInfo() { // Common fields across versions std::vector<FieldProcessInfo> result = @@ -176,12 +176,11 @@ namespace AppInstaller::Manifest { "ManifestVersion", [](const YAML::Node&, const VariantManifestPtr&)->ValidationErrors { /* ManifestVersion already populated. Field listed here for duplicate and PascalCase check */ return {}; } }, { "Installers", [this](const YAML::Node& value, const VariantManifestPtr&)->ValidationErrors { m_p_installersNode = &value; return {}; } }, { "Localization", [this](const YAML::Node& value, const VariantManifestPtr&)->ValidationErrors { m_p_localizationsNode = &value; return {}; } }, - { "Channel", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { variant_ptr<Manifest>(v)->Channel = Utility::Trim(value.as<std::string>()); return {}; } - }, + { "Channel", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { variant_ptr<Manifest>(v)->Channel = Utility::Trim(value.as<std::string>()); return {}; } }, }; // Additional version specific fields - if (manifestVersion.Major() == 0) + if (m_manifestVersion.get().Major() == 0) { std::vector<FieldProcessInfo> previewRootFields { @@ -190,12 +189,13 @@ namespace AppInstaller::Manifest { "AppMoniker", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { variant_ptr<Manifest>(v)->Moniker = Utility::Trim(value.as<std::string>()); return {}; } }, }; + std::move(previewRootFields.begin(), previewRootFields.end(), std::inserter(result, result.end())); } - else if (manifestVersion.Major() == 1) + else if (m_manifestVersion.get().Major() == 1) { // Starting v1, we should be only adding new fields for each minor version increase - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1 }) + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1 }) { std::vector<FieldProcessInfo> v1RootFields { @@ -210,16 +210,16 @@ namespace AppInstaller::Manifest } // Root fields mapped as Installer and Localization values - auto rootInstallerFields = GetInstallerFieldProcessInfo(manifestVersion, true); + auto rootInstallerFields = GetInstallerFieldProcessInfo(true); std::move(rootInstallerFields.begin(), rootInstallerFields.end(), std::inserter(result, result.end())); - auto rootLocalizationFields = GetLocalizationFieldProcessInfo(manifestVersion, true); + auto rootLocalizationFields = GetLocalizationFieldProcessInfo(true); std::move(rootLocalizationFields.begin(), rootLocalizationFields.end(), std::inserter(result, result.end())); return result; } - std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetInstallerFieldProcessInfo(const ManifestVer& manifestVersion, bool forRootFields) + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetInstallerFieldProcessInfo(bool forRootFields) { // Common fields across versions std::vector<FieldProcessInfo> result = @@ -230,7 +230,7 @@ namespace AppInstaller::Manifest }; // Additional version specific fields - if (manifestVersion.Major() == 0) + if (m_manifestVersion.get().Major() == 0) { // Root level and Localization node level std::vector<FieldProcessInfo> previewCommonFields = @@ -254,7 +254,7 @@ namespace AppInstaller::Manifest { "Scope", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { variant_ptr<ManifestInstaller>(v)->Scope = ConvertToScopeEnum(value.as<std::string>()); return {}; } }, }; - if (manifestVersion.HasExtension(s_MSStoreExtension)) + if (m_manifestVersion.get().HasExtension(s_MSStoreExtension)) { installerOnlyFields.emplace_back("ProductId", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { variant_ptr<ManifestInstaller>(v)->ProductId = value.as<std::string>(); return {}; }); } @@ -275,10 +275,10 @@ namespace AppInstaller::Manifest std::move(rootOnlyFields.begin(), rootOnlyFields.end(), std::inserter(result, result.end())); } } - else if (manifestVersion.Major() == 1) + else if (m_manifestVersion.get().Major() == 1) { // Starting v1, we should be only adding new fields for each minor version increase - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1 }) + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1 }) { // Root level and Installer node level std::vector<FieldProcessInfo> v1CommonFields = @@ -316,7 +316,7 @@ namespace AppInstaller::Manifest } } - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_1 }) + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_1 }) { std::vector<FieldProcessInfo> fields_v1_1 = { @@ -334,7 +334,7 @@ namespace AppInstaller::Manifest std::move(fields_v1_1.begin(), fields_v1_1.end(), std::inserter(result, result.end())); } - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_2 }) + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_2 }) { std::vector<FieldProcessInfo> fields_v1_2 = { @@ -345,7 +345,7 @@ namespace AppInstaller::Manifest std::move(fields_v1_2.begin(), fields_v1_2.end(), std::inserter(result, result.end())); } - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_4 }) + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_4 }) { std::vector<FieldProcessInfo> fields_v1_4 = { @@ -357,7 +357,7 @@ namespace AppInstaller::Manifest std::move(fields_v1_4.begin(), fields_v1_4.end(), std::inserter(result, result.end())); } - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_6 }) + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_6 }) { std::vector<FieldProcessInfo> fields_v1_6 = { @@ -367,7 +367,7 @@ namespace AppInstaller::Manifest std::move(fields_v1_6.begin(), fields_v1_6.end(), std::inserter(result, result.end())); } - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_7 }) + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_7 }) { std::vector<FieldProcessInfo> fields_v1_7 = { @@ -381,7 +381,7 @@ namespace AppInstaller::Manifest return result; } - std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetSwitchesFieldProcessInfo(const ManifestVer& manifestVersion) + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetSwitchesFieldProcessInfo() { // Common fields across versions std::vector<FieldProcessInfo> result = @@ -395,17 +395,17 @@ namespace AppInstaller::Manifest }; // Additional version specific fields - if (manifestVersion.Major() == 0) + if (m_manifestVersion.get().Major() == 0) { // Language only exists in preview manifests. Though we don't use it in our code yet, keep it here to be consistent with schema. result.emplace_back("Language", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { (*variant_ptr<std::map<InstallerSwitchType, Utility::NormalizedString>>(v))[InstallerSwitchType::Language] = value.as<std::string>(); return{}; }); result.emplace_back("Update", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { (*variant_ptr<std::map<InstallerSwitchType, Utility::NormalizedString>>(v))[InstallerSwitchType::Update] = value.as<std::string>(); return{}; }); } - else if (manifestVersion.Major() == 1) + else if (m_manifestVersion.get().Major() == 1) { result.emplace_back("Upgrade", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { (*variant_ptr<std::map<InstallerSwitchType, Utility::NormalizedString>>(v))[InstallerSwitchType::Update] = value.as<std::string>(); return{}; }); - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_7 }) + if (m_manifestVersion.get() >= ManifestVer{s_ManifestVersionV1_7}) { result.emplace_back("Repair", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { (*variant_ptr<std::map<InstallerSwitchType, Utility::NormalizedString>>(v))[InstallerSwitchType::Repair] = value.as<std::string>(); return{}; }); }; @@ -414,17 +414,17 @@ namespace AppInstaller::Manifest return result; } - std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetExpectedReturnCodesFieldProcessInfo(const ManifestVer& manifestVersion) + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetExpectedReturnCodesFieldProcessInfo() { std::vector<FieldProcessInfo> result = {}; - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_1 }) + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_1 }) { result.emplace_back("InstallerReturnCode", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { variant_ptr<ExpectedReturnCode>(v)->InstallerReturnCode = static_cast<int>(value.as<int>()); return {}; }); result.emplace_back("ReturnResponse", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { variant_ptr<ExpectedReturnCode>(v)->ReturnResponse = ConvertToExpectedReturnCodeEnum(value.as<std::string>()); return {}; }); } - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_2 }) + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_2 }) { result.emplace_back("ReturnResponseUrl", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { variant_ptr<ExpectedReturnCode>(v)->ReturnResponseUrl = value.as<std::string>(); return {}; }); } @@ -432,7 +432,7 @@ namespace AppInstaller::Manifest return result; } - std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetLocalizationFieldProcessInfo(const ManifestVer& manifestVersion, bool forRootFields) + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetLocalizationFieldProcessInfo(bool forRootFields) { // Common fields across versions std::vector<FieldProcessInfo> result = @@ -442,7 +442,7 @@ namespace AppInstaller::Manifest }; // Additional version specific fields - if (manifestVersion.Major() == 0) + if (m_manifestVersion.get().Major() == 0) { // Root level and Localization node level result.emplace_back("Homepage", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { GetManifestLocalizationPtr(v)->Add<Localization::PackageUrl>(value.as<std::string>()); return {}; }); @@ -467,10 +467,10 @@ namespace AppInstaller::Manifest std::move(rootOnlyFields.begin(), rootOnlyFields.end(), std::inserter(result, result.end())); } } - else if (manifestVersion.Major() == 1) + else if (m_manifestVersion.get().Major() == 1) { // Starting v1, we should be only adding new fields for each minor version increase - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1 }) + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1 }) { // Root level and Localization node level std::vector<FieldProcessInfo> v1CommonFields = @@ -485,7 +485,7 @@ namespace AppInstaller::Manifest { "PackageUrl", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { GetManifestLocalizationPtr(v)->Add<Localization::PackageUrl>(value.as<std::string>()); return {}; } }, { "License", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { GetManifestLocalizationPtr(v)->Add<Localization::License>(value.as<std::string>()); return {}; } }, { "Copyright", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { GetManifestLocalizationPtr(v)->Add<Localization::Copyright>(value.as<std::string>()); return {}; } }, - { "CopyrightUrl", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors{ GetManifestLocalizationPtr(v)->Add<Localization::CopyrightUrl>(value.as<std::string>()); return {}; } }, + { "CopyrightUrl", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { GetManifestLocalizationPtr(v)->Add<Localization::CopyrightUrl>(value.as<std::string>()); return {}; } }, { "ShortDescription", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { GetManifestLocalizationPtr(v)->Add<Localization::ShortDescription>(Utility::Trim(value.as<std::string>())); return {}; } }, { "Tags", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { GetManifestLocalizationPtr(v)->Add<Localization::Tags>(ProcessStringSequenceNode(value)); return {}; } }, }; @@ -493,7 +493,7 @@ namespace AppInstaller::Manifest std::move(v1CommonFields.begin(), v1CommonFields.end(), std::inserter(result, result.end())); } - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_1 }) + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_1 }) { std::vector<FieldProcessInfo> fields_v1_1 = { @@ -505,7 +505,7 @@ namespace AppInstaller::Manifest std::move(fields_v1_1.begin(), fields_v1_1.end(), std::inserter(result, result.end())); } - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_2 }) + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_2 }) { std::vector<FieldProcessInfo> fields_v1_2 = { @@ -517,7 +517,7 @@ namespace AppInstaller::Manifest std::move(fields_v1_2.begin(), fields_v1_2.end(), std::inserter(result, result.end())); } - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_5 }) + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_5 }) { std::vector<FieldProcessInfo> fields_v1_5 = { @@ -531,11 +531,11 @@ namespace AppInstaller::Manifest return result; } - std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetDependenciesFieldProcessInfo(const ManifestVer& manifestVersion) + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetDependenciesFieldProcessInfo() { std::vector<FieldProcessInfo> result = {}; - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1 }) + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1 }) { result = { @@ -549,11 +549,11 @@ namespace AppInstaller::Manifest return result; } - std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetPackageDependenciesFieldProcessInfo(const ManifestVer& manifestVersion) + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetPackageDependenciesFieldProcessInfo() { std::vector<FieldProcessInfo> result = {}; - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1 }) + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1 }) { result = { @@ -565,11 +565,11 @@ namespace AppInstaller::Manifest return result; } - std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetAgreementFieldProcessInfo(const ManifestVer& manifestVersion) + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetAgreementFieldProcessInfo() { std::vector<FieldProcessInfo> result = {}; - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_1 }) + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_1 }) { result = { @@ -582,11 +582,11 @@ namespace AppInstaller::Manifest return result; } - std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetMarketsFieldProcessInfo(const ManifestVer& manifestVersion) + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetMarketsFieldProcessInfo() { std::vector<FieldProcessInfo> result = {}; - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_1 }) + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_1 }) { result = { @@ -598,11 +598,11 @@ namespace AppInstaller::Manifest return result; } - std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetAppsAndFeaturesEntryFieldProcessInfo(const ManifestVer& manifestVersion) + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetAppsAndFeaturesEntryFieldProcessInfo() { std::vector<FieldProcessInfo> result = {}; - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_1 }) + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_1 }) { result = { @@ -618,11 +618,11 @@ namespace AppInstaller::Manifest return result; } - std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetDocumentationFieldProcessInfo(const ManifestVer& manifestVersion) + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetDocumentationFieldProcessInfo() { std::vector<FieldProcessInfo> result = {}; - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_2 }) + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_2 }) { result = { @@ -634,11 +634,11 @@ namespace AppInstaller::Manifest return result; } - std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetIconFieldProcessInfo(const ManifestVer& manifestVersion) + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetIconFieldProcessInfo() { std::vector<FieldProcessInfo> result = {}; - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_5 }) + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_5 }) { result = { @@ -653,11 +653,11 @@ namespace AppInstaller::Manifest return result; } - std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetNestedInstallerFileFieldProcessInfo(const ManifestVer& manifestVersion) + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetNestedInstallerFileFieldProcessInfo() { std::vector<FieldProcessInfo> result = {}; - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_4 }) + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_4 }) { result = { @@ -669,11 +669,11 @@ namespace AppInstaller::Manifest return result; } - std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetInstallationMetadataFieldProcessInfo(const ManifestVer& manifestVersion) + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetInstallationMetadataFieldProcessInfo() { std::vector<FieldProcessInfo> result = {}; - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_4 }) + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_4 }) { result = { @@ -685,11 +685,11 @@ namespace AppInstaller::Manifest return result; } - std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetInstallationMetadataFilesFieldProcessInfo(const ManifestVer& manifestVersion) + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetInstallationMetadataFilesFieldProcessInfo() { std::vector<FieldProcessInfo> result = {}; - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_4 }) + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_4 }) { result = { @@ -704,6 +704,57 @@ namespace AppInstaller::Manifest return result; } + + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetShadowRootFieldProcessInfo() + { + std::vector<FieldProcessInfo> result; + + if (m_manifestVersion.get().Major() == 1) + { + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_5 }) + { + std::vector<FieldProcessInfo> fields_v1_5 = + { + { + { "Localization", [this](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { return ProcessShadowLocalizationNode(value, variant_ptr<Manifest>(v)); } }, + { "ManifestType", [](const YAML::Node&, const VariantManifestPtr&)->ValidationErrors { return {}; } }, + { "PackageIdentifier", [](const YAML::Node&, const VariantManifestPtr&)->ValidationErrors { return {}; } }, + { "PackageVersion", [](const YAML::Node&, const VariantManifestPtr&)->ValidationErrors { return {}; } }, + { "ManifestVersion", [](const YAML::Node&, const VariantManifestPtr&)->ValidationErrors { return {}; } }, + }, + }; + + std::move(fields_v1_5.begin(), fields_v1_5.end(), std::inserter(result, result.end())); + } + } + + auto rootLocalizationFields = GetShadowLocalizationFieldProcessInfo(); + std::move(rootLocalizationFields.begin(), rootLocalizationFields.end(), std::inserter(result, result.end())); + + return result; + } + + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetShadowLocalizationFieldProcessInfo() + { + std::vector<FieldProcessInfo> result; + + if (m_manifestVersion.get().Major() == 1) + { + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_5 }) + { + std::vector<FieldProcessInfo> fields_v1_5 = + { + { "PackageLocale", [this](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { GetManifestLocalizationPtr(v)->Locale = value.as<std::string>(); return {}; } }, + { "Icons", [this](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { return ProcessIconsNode(value, GetManifestLocalizationPtr(v)); } }, + }; + + std::move(fields_v1_5.begin(), fields_v1_5.end(), std::inserter(result, result.end())); + } + } + + return result; + } + ValidationErrors ManifestYamlPopulator::ValidateAndProcessFields( const YAML::Node& rootNode, const std::vector<FieldProcessInfo>& fieldInfos, @@ -889,7 +940,7 @@ namespace AppInstaller::Manifest { localization->Add<Localization::Documentations>(std::move(documentations)); } - + return resultErrors; } @@ -962,36 +1013,54 @@ namespace AppInstaller::Manifest return resultErrors; } - ValidationErrors ManifestYamlPopulator::PopulateManifestInternal( - const YAML::Node& rootNode, - Manifest& manifest, - const ManifestVer& manifestVersion, - ManifestValidateOption validateOption) + std::vector<ValidationError> ManifestYamlPopulator::ProcessShadowLocalizationNode(const YAML::Node& localizationNode, Manifest* manifest) { - m_validateOption = validateOption; - m_isMergedManifest = !rootNode["ManifestType"sv].IsNull() && rootNode["ManifestType"sv].as<std::string>() == "merged"; + THROW_HR_IF(E_INVALIDARG, !localizationNode.IsSequence()); ValidationErrors resultErrors; - manifest.ManifestVersion = manifestVersion; + auto shadowLocalizationFields = GetShadowLocalizationFieldProcessInfo(); + + for (auto const& entry : localizationNode.Sequence()) + { + ManifestLocalization localization; + auto errors = ValidateAndProcessFields(entry, shadowLocalizationFields, VariantManifestPtr(&localization)); + std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); + manifest->Localizations.emplace_back(std::move(std::move(localization))); + } + + return resultErrors; + } + + ManifestYamlPopulator::ManifestYamlPopulator(YAML::Node& rootNode, Manifest& manifest, const ManifestVer& manifestVersion, ManifestValidateOption validateOption) : + m_rootNode(rootNode), m_manifest(manifest), m_manifestVersion(manifestVersion), m_validateOption(validateOption) + { + m_isMergedManifest = !m_rootNode.get()["ManifestType"sv].IsNull() && m_rootNode.get()["ManifestType"sv].as<std::string>() == "merged"; + m_manifest.get().ManifestVersion = m_manifestVersion; + } + + ValidationErrors ManifestYamlPopulator::PopulateManifestInternal() + { + const YAML::Node& rootNode = m_rootNode; + ValidationErrors resultErrors; // Prepare field infos - RootFieldInfos = GetRootFieldProcessInfo(manifestVersion); - InstallerFieldInfos = GetInstallerFieldProcessInfo(manifestVersion); - SwitchesFieldInfos = GetSwitchesFieldProcessInfo(manifestVersion); - ExpectedReturnCodesFieldInfos = GetExpectedReturnCodesFieldProcessInfo(manifestVersion); - DependenciesFieldInfos = GetDependenciesFieldProcessInfo(manifestVersion); - PackageDependenciesFieldInfos = GetPackageDependenciesFieldProcessInfo(manifestVersion); - LocalizationFieldInfos = GetLocalizationFieldProcessInfo(manifestVersion); - AgreementFieldInfos = GetAgreementFieldProcessInfo(manifestVersion); - MarketsFieldInfos = GetMarketsFieldProcessInfo(manifestVersion); - AppsAndFeaturesEntryFieldInfos = GetAppsAndFeaturesEntryFieldProcessInfo(manifestVersion); - DocumentationFieldInfos = GetDocumentationFieldProcessInfo(manifestVersion); - IconFieldInfos = GetIconFieldProcessInfo(manifestVersion); - NestedInstallerFileFieldInfos = GetNestedInstallerFileFieldProcessInfo(manifestVersion); - InstallationMetadataFieldInfos = GetInstallationMetadataFieldProcessInfo(manifestVersion); - InstallationMetadataFilesFieldInfos = GetInstallationMetadataFilesFieldProcessInfo(manifestVersion); - - resultErrors = ValidateAndProcessFields(rootNode, RootFieldInfos, VariantManifestPtr(&manifest)); + RootFieldInfos = GetRootFieldProcessInfo(); + InstallerFieldInfos = GetInstallerFieldProcessInfo(); + SwitchesFieldInfos = GetSwitchesFieldProcessInfo(); + ExpectedReturnCodesFieldInfos = GetExpectedReturnCodesFieldProcessInfo(); + DependenciesFieldInfos = GetDependenciesFieldProcessInfo(); + PackageDependenciesFieldInfos = GetPackageDependenciesFieldProcessInfo(); + LocalizationFieldInfos = GetLocalizationFieldProcessInfo(); + AgreementFieldInfos = GetAgreementFieldProcessInfo(); + MarketsFieldInfos = GetMarketsFieldProcessInfo(); + AppsAndFeaturesEntryFieldInfos = GetAppsAndFeaturesEntryFieldProcessInfo(); + DocumentationFieldInfos = GetDocumentationFieldProcessInfo(); + IconFieldInfos = GetIconFieldProcessInfo(); + NestedInstallerFileFieldInfos = GetNestedInstallerFileFieldProcessInfo(); + InstallationMetadataFieldInfos = GetInstallationMetadataFieldProcessInfo(); + InstallationMetadataFilesFieldInfos = GetInstallationMetadataFilesFieldProcessInfo(); + + resultErrors = ValidateAndProcessFields(rootNode, RootFieldInfos, VariantManifestPtr(&(m_manifest.get()))); if (!m_p_installersNode) { @@ -1001,7 +1070,7 @@ namespace AppInstaller::Manifest // Populate installers for (auto const& entry : m_p_installersNode->Sequence()) { - ManifestInstaller installer = manifest.DefaultInstallerInfo; + ManifestInstaller installer = m_manifest.get().DefaultInstallerInfo; // Clear these defaults as PackageFamilyName, ProductCode, AppsAndFeaturesEntries need to be copied based on InstallerType installer.PackageFamilyName.clear(); @@ -1019,36 +1088,36 @@ namespace AppInstaller::Manifest // Copy in system reference strings from the root if not set in the installer and appropriate if (installer.PackageFamilyName.empty() && DoesInstallerTypeUsePackageFamilyName(installer.EffectiveInstallerType())) { - installer.PackageFamilyName = manifest.DefaultInstallerInfo.PackageFamilyName; + installer.PackageFamilyName = m_manifest.get().DefaultInstallerInfo.PackageFamilyName; } if (installer.ProductCode.empty() && DoesInstallerTypeUseProductCode(installer.EffectiveInstallerType())) { - installer.ProductCode = manifest.DefaultInstallerInfo.ProductCode; + installer.ProductCode = m_manifest.get().DefaultInstallerInfo.ProductCode; } if (installer.AppsAndFeaturesEntries.empty() && DoesInstallerTypeWriteAppsAndFeaturesEntry(installer.EffectiveInstallerType())) { - installer.AppsAndFeaturesEntries = manifest.DefaultInstallerInfo.AppsAndFeaturesEntries; + installer.AppsAndFeaturesEntries = m_manifest.get().DefaultInstallerInfo.AppsAndFeaturesEntries; } if (IsArchiveType(installer.BaseInstallerType)) { if (installer.NestedInstallerFiles.empty()) { - installer.NestedInstallerFiles = manifest.DefaultInstallerInfo.NestedInstallerFiles; + installer.NestedInstallerFiles = m_manifest.get().DefaultInstallerInfo.NestedInstallerFiles; } if (installer.NestedInstallerType == InstallerTypeEnum::Unknown) { - installer.NestedInstallerType = manifest.DefaultInstallerInfo.NestedInstallerType; + installer.NestedInstallerType = m_manifest.get().DefaultInstallerInfo.NestedInstallerType; } } // If there are no dependencies on installer use default ones if (!installer.Dependencies.HasAny()) { - installer.Dependencies = manifest.DefaultInstallerInfo.Dependencies; + installer.Dependencies = m_manifest.get().DefaultInstallerInfo.Dependencies; } // Populate installer default switches if not exists @@ -1072,7 +1141,7 @@ namespace AppInstaller::Manifest } } - manifest.Installers.emplace_back(std::move(installer)); + m_manifest.get().Installers.emplace_back(std::move(installer)); } // Populate additional localizations @@ -1083,7 +1152,73 @@ namespace AppInstaller::Manifest ManifestLocalization localization; auto errors = ValidateAndProcessFields(entry, LocalizationFieldInfos, VariantManifestPtr(&localization)); std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); - manifest.Localizations.emplace_back(std::move(std::move(localization))); + m_manifest.get().Localizations.emplace_back(std::move(std::move(localization))); + } + } + + return resultErrors; + } + + ValidationErrors ManifestYamlPopulator::InsertShadow(const YAML::Node& shadowNode) + { + Manifest shadowManifest; + + // Process shadow node. + auto resultErrors = ValidateAndProcessFields(shadowNode, GetShadowRootFieldProcessInfo(), VariantManifestPtr(&shadowManifest)); + + // Merge. + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_5 }) + { + // Default localization + if (Utility::ICUCaseInsensitiveEquals(m_manifest.get().DefaultLocalization.Locale, shadowManifest.DefaultLocalization.Locale)) + { + // Icons + if (!m_manifest.get().DefaultLocalization.Contains(Localization::Icons) && + shadowManifest.DefaultLocalization.Contains(Localization::Icons)) + { + m_manifest.get().DefaultLocalization.Add<Localization::Icons>(std::move(shadowManifest.DefaultLocalization.Get<Localization::Icons>())); + + YAML::Node key{ YAML::Node::Type::Scalar, "", YAML::Mark() }; + key.SetScalar("Icons"); + YAML::Node value = shadowNode.GetChildNode("Icons"); + m_rootNode.get().AddMappingNode(std::move(key), std::move(value)); + } + } + + // Localizations + if (!shadowManifest.Localizations.empty()) + { + // Merge manifest object + for (auto const& shadowLocalization : shadowManifest.Localizations) + { + // Manifest + if (auto iter = std::find_if(m_manifest.get().Localizations.begin(), m_manifest.get().Localizations.end(), [&](auto const& l) { return Utility::ICUCaseInsensitiveEquals(l.Locale, shadowLocalization.Locale); }); iter != m_manifest.get().Localizations.end()) + { + if (!(*iter).Contains(Localization::Icons) && + shadowLocalization.Contains(Localization::Icons)) + { + (*iter).Add<Localization::Icons>(std::move(shadowLocalization.Get<Localization::Icons>())); + } + } + else + { + ManifestLocalization localization = shadowLocalization; + m_manifest.get().Localizations.emplace_back(std::move(std::move(localization))); + } + } + + // Merge yaml + auto shadowLocalizationsNode = shadowNode.GetChildNode("Localization"); + if (m_p_localizationsNode) + { + m_rootNode.get().GetChildNode("Localization").MergeSequenceNode(shadowLocalizationsNode, "PackageLocale", true); + } + else + { + YAML::Node key{ YAML::Node::Type::Scalar, "", YAML::Mark() }; + key.SetScalar("Localization"); + m_rootNode.get().AddMappingNode(std::move(key), std::move(shadowLocalizationsNode)); + } } } @@ -1091,12 +1226,21 @@ namespace AppInstaller::Manifest } ValidationErrors ManifestYamlPopulator::PopulateManifest( - const YAML::Node& rootNode, + YAML::Node& rootNode, Manifest& manifest, const ManifestVer& manifestVersion, - ManifestValidateOption validateOption) + ManifestValidateOption validateOption, + const std::optional<YAML::Node>& shadowNode) { - ManifestYamlPopulator manifestPopulator; - return manifestPopulator.PopulateManifestInternal(rootNode, manifest, manifestVersion, validateOption); + ManifestYamlPopulator manifestPopulator(rootNode, manifest, manifestVersion, validateOption); + auto errors = manifestPopulator.PopulateManifestInternal(); + + if (shadowNode.has_value()) + { + auto shadowErrors = manifestPopulator.InsertShadow(shadowNode.value()); + std::move(shadowErrors.begin(), shadowErrors.end(), std::inserter(errors, errors.end())); + } + + return errors; } } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Manifest/YamlParser.cpp b/src/AppInstallerCommonCore/Manifest/YamlParser.cpp @@ -150,6 +150,7 @@ namespace AppInstaller::Manifest::YamlParser bool isVersionManifestFound = false; bool isInstallerManifestFound = false; bool isDefaultLocaleManifestFound = false; + bool isShadowManifestFound = false; std::string defaultLocaleFromVersionManifest; std::string defaultLocaleFromDefaultLocaleManifest; @@ -240,8 +241,26 @@ namespace AppInstaller::Manifest::YamlParser { localesSet.insert(packageLocale); } + break; + } + case ManifestTypeEnum::Shadow: + { + if (!validateOption.AllowShadowManifest) + { + errors.emplace_back(ValidationError::MessageContextValueWithFile( + ManifestError::ShadowManifestNotAllowed, "ManifestType", manifestTypeStr, entry.FileName)); + } + else if (isShadowManifestFound) + { + errors.emplace_back(ValidationError::MessageContextValueWithFile( + ManifestError::DuplicateMultiFileManifestType, "ManifestType", manifestTypeStr, entry.FileName)); + } + else + { + isShadowManifestFound = true; + } + break; } - break; default: errors.emplace_back(ValidationError::MessageContextValueWithFile( ManifestError::UnsupportedMultiFileManifestType, "ManifestType", manifestTypeStr, entry.FileName)); @@ -299,6 +318,22 @@ namespace AppInstaller::Manifest::YamlParser return iter->Root; } + std::optional<YAML::Node> FindUniqueOptionalDocFromMultiFileManifest(std::vector<YamlManifestInfo>& input, ManifestTypeEnum manifestType) + { + auto iter = std::find_if(input.begin(), input.end(), + [=](auto const& s) + { + return s.ManifestType == manifestType; + }); + + if (iter != input.end()) + { + return iter->Root; + } + + return {}; + } + // Merge one manifest file to the final merged manifest, basically copying the mapping but excluding certain common fields void MergeOneManifestToMultiFileManifest(const YAML::Node& input, YAML::Node& destination) { @@ -427,9 +462,16 @@ namespace AppInstaller::Manifest::YamlParser } // Merge manifests in multi file manifest case - const YAML::Node& manifestDoc = (input.size() > 1) ? MergeMultiFileManifest(input) : input[0].Root; + bool isMultiFile = input.size() > 1; + YAML::Node& manifestDoc = input[0].Root; + if (isMultiFile) + { + manifestDoc = MergeMultiFileManifest(input); + } + + auto shadowNode = isMultiFile ? FindUniqueOptionalDocFromMultiFileManifest(input, ManifestTypeEnum::Shadow) : std::optional<YAML::Node>{}; - auto errors = ManifestYamlPopulator::PopulateManifest(manifestDoc, manifest, manifestVersion, validateOption); + auto errors = ManifestYamlPopulator::PopulateManifest(manifestDoc, manifest, manifestVersion, validateOption, shadowNode); std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); // Extra semantic validations after basic validation and field population diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h @@ -55,6 +55,7 @@ namespace AppInstaller::Manifest // Options not exposed in winget util bool FullValidation = false; bool ThrowOnWarning = false; + bool AllowShadowManifest = false; }; // ManifestVer is inherited from Utility::Version and is a more restricted version. @@ -201,6 +202,7 @@ namespace AppInstaller::Manifest Locale, Merged, Preview, + Shadow, }; enum class DependencyType diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h b/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h @@ -63,6 +63,7 @@ namespace AppInstaller::Manifest WINGET_DEFINE_RESOURCE_STRINGID(RequiredFieldMissing); WINGET_DEFINE_RESOURCE_STRINGID(SchemaError); WINGET_DEFINE_RESOURCE_STRINGID(ScopeNotSupported); + WINGET_DEFINE_RESOURCE_STRINGID(ShadowManifestNotAllowed); WINGET_DEFINE_RESOURCE_STRINGID(SingleManifestPackageHasDependencies); WINGET_DEFINE_RESOURCE_STRINGID(UnsupportedMultiFileManifestType); } diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestYamlPopulator.h b/src/AppInstallerCommonCore/Public/winget/ManifestYamlPopulator.h @@ -13,13 +13,19 @@ namespace AppInstaller::Manifest struct ManifestYamlPopulator { static std::vector<ValidationError> PopulateManifest( - const YAML::Node& rootNode, + YAML::Node& rootNode, Manifest& manifest, const ManifestVer& manifestVersion, - ManifestValidateOption validateOption); + ManifestValidateOption validateOption, + const std::optional<YAML::Node>& shadowNode); private: + ManifestYamlPopulator(YAML::Node& rootNode, Manifest& manifest, const ManifestVer& manifestVersion, ManifestValidateOption validateOption); + + std::reference_wrapper<YAML::Node> m_rootNode; + std::reference_wrapper<Manifest> m_manifest; + std::reference_wrapper<const ManifestVer> m_manifestVersion; bool m_isMergedManifest = false; ManifestValidateOption m_validateOption; @@ -50,26 +56,29 @@ namespace AppInstaller::Manifest std::vector<FieldProcessInfo> InstallationMetadataFieldInfos; std::vector<FieldProcessInfo> InstallationMetadataFilesFieldInfos; - // Cache of Installers node and Localization node YAML::Node const* m_p_installersNode = nullptr; YAML::Node const* m_p_localizationsNode = nullptr; - std::vector<FieldProcessInfo> GetRootFieldProcessInfo(const ManifestVer& manifestVersion); - std::vector<FieldProcessInfo> GetInstallerFieldProcessInfo(const ManifestVer& manifestVersion, bool forRootFields = false); - std::vector<FieldProcessInfo> GetSwitchesFieldProcessInfo(const ManifestVer& manifestVersion); - std::vector<FieldProcessInfo> GetExpectedReturnCodesFieldProcessInfo(const ManifestVer& manifestVersion); - std::vector<FieldProcessInfo> GetDependenciesFieldProcessInfo(const ManifestVer& manifestVersion); - 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); - std::vector<FieldProcessInfo> GetDocumentationFieldProcessInfo(const ManifestVer& manifestVersion); - std::vector<FieldProcessInfo> GetIconFieldProcessInfo(const ManifestVer& manifestVersion); - std::vector<FieldProcessInfo> GetNestedInstallerFileFieldProcessInfo(const ManifestVer& manifestVersion); - std::vector<FieldProcessInfo> GetInstallationMetadataFieldProcessInfo(const ManifestVer& manifestVersion); - std::vector<FieldProcessInfo> GetInstallationMetadataFilesFieldProcessInfo(const ManifestVer& manifestVersion); + std::vector<FieldProcessInfo> GetRootFieldProcessInfo(); + std::vector<FieldProcessInfo> GetInstallerFieldProcessInfo(bool forRootFields = false); + std::vector<FieldProcessInfo> GetSwitchesFieldProcessInfo(); + std::vector<FieldProcessInfo> GetExpectedReturnCodesFieldProcessInfo(); + std::vector<FieldProcessInfo> GetDependenciesFieldProcessInfo(); + std::vector<FieldProcessInfo> GetPackageDependenciesFieldProcessInfo(); + std::vector<FieldProcessInfo> GetLocalizationFieldProcessInfo(bool forRootFields = false); + std::vector<FieldProcessInfo> GetAgreementFieldProcessInfo(); + std::vector<FieldProcessInfo> GetMarketsFieldProcessInfo(); + std::vector<FieldProcessInfo> GetAppsAndFeaturesEntryFieldProcessInfo(); + std::vector<FieldProcessInfo> GetDocumentationFieldProcessInfo(); + std::vector<FieldProcessInfo> GetIconFieldProcessInfo(); + std::vector<FieldProcessInfo> GetNestedInstallerFileFieldProcessInfo(); + std::vector<FieldProcessInfo> GetInstallationMetadataFieldProcessInfo(); + std::vector<FieldProcessInfo> GetInstallationMetadataFilesFieldProcessInfo(); + + // Shadow + std::vector<FieldProcessInfo> GetShadowRootFieldProcessInfo(); + std::vector<FieldProcessInfo> GetShadowLocalizationFieldProcessInfo(); // 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, @@ -89,11 +98,9 @@ namespace AppInstaller::Manifest std::vector<ValidationError> ProcessIconsNode(const YAML::Node& iconsNode, ManifestLocalization* localization); std::vector<ValidationError> ProcessNestedInstallerFilesNode(const YAML::Node& nestedInstallerFilesNode, AppInstaller::Manifest::ManifestInstaller* installer); std::vector<ValidationError> ProcessInstallationMetadataFilesNode(const YAML::Node& installedFilesNode, InstallationMetadataInfo* installationMetadata); + std::vector<ValidationError> ProcessShadowLocalizationNode(const YAML::Node& localizationNode, Manifest* manifest); - std::vector<ValidationError> PopulateManifestInternal( - const YAML::Node& rootNode, - Manifest& manifest, - const ManifestVer& manifestVersion, - ManifestValidateOption validateOption); + std::vector<ValidationError> PopulateManifestInternal(); + std::vector<ValidationError> InsertShadow(const YAML::Node& shadowNode); }; } diff --git a/src/AppInstallerSharedLib/Public/winget/Yaml.h b/src/AppInstallerSharedLib/Public/winget/Yaml.h @@ -104,6 +104,10 @@ namespace AppInstaller::YAML return m_sequence->emplace_back(std::forward<Args>(args)...); } + // Merges sequence nodes. If both sequence have the specified key with the same value + // they will get merged together. All elements in sequence must have the key. + void MergeSequenceNode(Node other, std::string_view key, bool caseInsensitive = false); + // Adds a child node to the mapping. template <typename... Args> Node& AddMappingNode(Node&& key, Args&&... args) @@ -112,6 +116,9 @@ namespace AppInstaller::YAML return m_mapping->emplace(std::move(key), Node(std::forward<Args>(args)...))->second; } + // Merge mapping node. If both contain a node with the same key preserve this. + void MergeMappingNode(Node other, bool caseInsensitive = false); + bool IsDefined() const { return m_type != Type::Invalid; } bool IsNull() const { return m_type == Type::Invalid || m_type == Type::None || (m_type == Type::Scalar && m_scalar.empty()); } bool IsScalar() const { return m_type == Type::Scalar; } @@ -149,6 +156,10 @@ namespace AppInstaller::YAML Node& operator[](std::string_view key); const Node& operator[](std::string_view key) const; + // Gets a child node from the mapping by its name case insensitive. + Node& GetChildNode(std::string_view key); + const Node& GetChildNode(std::string_view key) const; + // Gets a child node from the sequence by its index. Node& operator[](size_t index); const Node& operator[](size_t index) const; diff --git a/src/AppInstallerSharedLib/Yaml.cpp b/src/AppInstallerSharedLib/Yaml.cpp @@ -269,6 +269,71 @@ namespace AppInstaller::YAML return result; } + // Gets a child node from the mapping by its name. + Node& Node::GetChildNode(std::string_view key) + { + Require(Type::Mapping); + + auto itr = m_mapping->begin(); + for (; itr != m_mapping->end(); itr++) + { + if (Utility::CaseInsensitiveEquals(itr->first.m_scalar, key)) + { + break; + } + } + + if (itr == m_mapping->end()) + { + return s_globalInvalidNode; + } + + auto firstFound = itr; + for (++itr; itr != m_mapping->end(); itr++) + { + if (Utility::CaseInsensitiveEquals(itr->first.m_scalar, key)) + { + break; + } + } + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY, itr != m_mapping->end()); + Node& result = firstFound->second; + return result; + } + + const Node& Node::GetChildNode(std::string_view key) const + { + Require(Type::Mapping); + + auto itr = m_mapping->begin(); + for (; itr != m_mapping->end(); itr++) + { + if (Utility::CaseInsensitiveEquals(itr->first.m_scalar, key)) + { + break; + } + } + + if (itr == m_mapping->end()) + { + return s_globalInvalidNode; + } + + auto firstFound = itr; + for (++itr; itr != m_mapping->end(); itr++) + { + if (Utility::CaseInsensitiveEquals(itr->first.m_scalar, key)) + { + break; + } + } + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_YAML_DUPLICATE_MAPPING_KEY, itr != m_mapping->end()); + const Node& result = firstFound->second; + return result; + } + Node& Node::operator[](size_t index) { Require(Type::Sequence); @@ -398,6 +463,82 @@ namespace AppInstaller::YAML return {}; } + void Node::MergeSequenceNode(Node other, std::string_view key, bool caseInsensitive) + { + Require(Type::Sequence); + other.Require(Type::Sequence); + + auto getKeyValue = [&](const YAML::Node& node) { + auto keyNode = caseInsensitive ? node.GetChildNode(key) : node[key]; + if (keyNode.IsNull()) + { + THROW_HR(APPINSTALLER_CLI_ERROR_YAML_INVALID_DATA); + } + + auto keyValue = keyNode.as<std::string>(); + return caseInsensitive ? std::string{ Utility::FoldCase(std::string_view{keyValue}) } : keyValue; + }; + + std::map<std::string, Node> newSequenceMap; + for (Node& node : m_sequence.value()) + { + node.Require(Type::Mapping); + auto keyValue = getKeyValue(node); + newSequenceMap.emplace(std::move(keyValue), std::move(node)); + } + + for (Node& node : other.m_sequence.value()) + { + node.Require(Type::Mapping); + auto keyValue = getKeyValue(node); + if (newSequenceMap.find(keyValue) == newSequenceMap.end()) + { + newSequenceMap.emplace(std::move(keyValue), std::move(node)); + } + else + { + newSequenceMap[keyValue].MergeMappingNode(node, caseInsensitive); + } + } + + m_sequence.reset(); + std::vector<Node> newSequence; + for (const auto& keyValuePair : newSequenceMap) + { + newSequence.push_back(keyValuePair.second); + } + + m_sequence = std::move(newSequence); + } + + void Node::MergeMappingNode(Node other, bool caseInsensitive) + { + Require(Type::Mapping); + other.Require(Type::Mapping); + + std::multimap<Node, Node> uniques; + for (auto& keyValuePair : other.m_mapping.value()) + { + if (caseInsensitive) + { + auto node = GetChildNode(keyValuePair.first.as<std::string>()); + if (node.IsNull()) + { + uniques.emplace(std::move(keyValuePair)); + } + } + else + { + if (m_mapping->count(keyValuePair.first) == 0) + { + uniques.emplace(std::move(keyValuePair)); + } + } + } + + m_mapping->merge(uniques); + } + Node Load(std::string_view input) { Wrapper::Parser parser(input); diff --git a/src/WinGetUtil/Exports.cpp b/src/WinGetUtil/Exports.cpp @@ -291,6 +291,11 @@ extern "C" validateOption.ErrorOnVerifiedPublisherFields = WI_IsFlagSet(option, WinGetCreateManifestOption::ReturnErrorOnVerifiedPublisherFields); } + if (WI_IsFlagSet(option, WinGetCreateManifestOption::AllowShadowManifest)) + { + validateOption.AllowShadowManifest = true; + } + std::unique_ptr<Manifest> result = std::make_unique<Manifest>(YamlParser::CreateFromPath(inputPath, validateOption, mergedManifestPath ? mergedManifestPath : L"")); *manifest = static_cast<WINGET_MANIFEST_HANDLE>(result.release()); @@ -301,7 +306,13 @@ extern "C" *succeeded = e.IsWarningOnly(); if (*succeeded) { - std::unique_ptr<Manifest> result = std::make_unique<Manifest>(YamlParser::CreateFromPath(inputPath)); + ManifestValidateOption validateOption; + if (WI_IsFlagSet(option, WinGetCreateManifestOption::AllowShadowManifest)) + { + validateOption.AllowShadowManifest = true; + } + + std::unique_ptr<Manifest> result = std::make_unique<Manifest>(YamlParser::CreateFromPath(inputPath, validateOption)); *manifest = static_cast<WINGET_MANIFEST_HANDLE>(result.release()); } if (message) diff --git a/src/WinGetUtil/WinGetUtil.h b/src/WinGetUtil/WinGetUtil.h @@ -38,6 +38,8 @@ extern "C" SchemaValidation = 0x1, // Validate against schema and also perform semantic validation SchemaAndSemanticValidation = 0x2, + // Use shadow manifest + AllowShadowManifest = 0x4, /// Below options are additional validation behaviors if needed diff --git a/src/WinGetUtilInterop.UnitTests/APIUnitTests/ManifestUnitTests.cs b/src/WinGetUtilInterop.UnitTests/APIUnitTests/ManifestUnitTests.cs @@ -0,0 +1,160 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ManifestUnitTests.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace WinGetUtilInterop.UnitTests.APIUnitTests +{ + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Reflection; + using Microsoft.WinGetUtil.Api; + using Microsoft.WinGetUtil.Common; + using Microsoft.WinGetUtil.Manifest.V1; + using Microsoft.WinGetUtil.Models.V1; + using WinGetUtilInterop.UnitTests.Common; + using Xunit; + using Xunit.Abstractions; + + /// <summary> + /// API manifests tests. + /// </summary> + public class ManifestUnitTests + { + private static string testCollateralDir = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "TestCollateral"); + private readonly ITestOutputHelper log; + + /// <summary> + /// Initializes a new instance of the <see cref="ManifestUnitTests"/> class. + /// </summary> + /// <param name="log">Output Helper.</param> + public ManifestUnitTests(ITestOutputHelper log) + { + this.log = log; + } + + /// <summary> + /// Test creating a manifest with shadow. + /// </summary> + [FactSkipx64CI] + public void CreateShadowManifest() + { + var input = Path.Combine(testCollateralDir, "Shadow"); + var mergedManifestPath = Path.GetTempFileName(); + var logFile = Path.GetTempFileName(); + + var factory = new WinGetFactory(); + using var log = factory.LoggingInit(logFile); + using var result = factory.CreateManifest( + input, + mergedManifestPath, + WinGetCreateManifestOption.SchemaAndSemanticValidation | WinGetCreateManifestOption.AllowShadowManifest); + + Assert.NotNull(result); + Assert.True(result.IsValid); + Assert.NotNull(result.ManifestHandle); + + var manifest = Manifest.CreateManifestFromPath(mergedManifestPath); + + Assert.Equal("microsoft.msixsdk", manifest.Id); + Assert.Equal("1.7.32", manifest.Version); + Assert.Single(manifest.Installers); + Assert.Equal("en-US", manifest.PackageLocale); + Assert.Equal("Microsoft", manifest.Publisher); + Assert.Equal("MSIX SDK", manifest.PackageName); + Assert.Equal("MIT License", manifest.License); + Assert.Equal("The MSIX SDK project is an effort to enable developers", manifest.Description); + Assert.Equal("This is MSIX SDK", manifest.ShortDescription); + + Assert.Single(manifest.Icons); + Assert.Equal("https://shadowIcon-default", manifest.Icons[0].IconUrl); + Assert.Equal("ico", manifest.Icons[0].IconFileType); + Assert.Equal("custom", manifest.Icons[0].IconResolution); + Assert.Equal("default", manifest.Icons[0].IconTheme); + Assert.Equal("1111111111111111111111111111111111111111111111111111111111111111", manifest.Icons[0].IconSha256); + + Assert.Equal(2, manifest.Localization.Count); + + var enGBLocale = manifest.Localization.Where(l => l.PackageLocale == "en-gb").FirstOrDefault(); + Assert.NotNull(enGBLocale); + Assert.Single(enGBLocale.Icons); + Assert.Equal("https://shadowIcon-en-GB", enGBLocale.Icons[0].IconUrl); + Assert.Equal("png", enGBLocale.Icons[0].IconFileType); + Assert.Equal("32x32", enGBLocale.Icons[0].IconResolution); + Assert.Equal("light", enGBLocale.Icons[0].IconTheme); + Assert.Equal("2222222222222222222222222222222222222222222222222222222222222222", enGBLocale.Icons[0].IconSha256); + + var frFRLocale = manifest.Localization.Where(l => l.PackageLocale == "fr-FR").FirstOrDefault(); + Assert.NotNull(frFRLocale); + Assert.Single(frFRLocale.Icons); + Assert.Equal("https://shadowIcon-fr-FR", frFRLocale.Icons[0].IconUrl); + Assert.Equal("jpeg", frFRLocale.Icons[0].IconFileType); + Assert.Equal("20x20", frFRLocale.Icons[0].IconResolution); + Assert.Equal("dark", frFRLocale.Icons[0].IconTheme); + Assert.Equal("3333333333333333333333333333333333333333333333333333333333333333", frFRLocale.Icons[0].IconSha256); + } + + /// <summary> + /// Test serializing the shadow manifest. + /// </summary> + [Fact] + public void SerializeShadowManifest() + { + var shadowManifest = ManifestShadow.CreateManifest(); + shadowManifest.Id = "Package.package"; + shadowManifest.Version = "1.0"; + shadowManifest.PackageLocale = "en-US"; + shadowManifest.ManifestVersion = "1.5"; + shadowManifest.Icons = new List<ManifestIcon> + { + new ManifestIcon() + { + IconUrl = "iconUrl", + IconFileType = "fileType", + IconResolution = "iconResolution", + IconTheme = "iconTheme", + IconSha256 = "iconSha256", + }, + }; + shadowManifest.Localization = new List<ManifestShadowLocalization> + { + new ManifestShadowLocalization() + { + PackageLocale = "es-MX", + Icons = new List<ManifestIcon>() + { + new ManifestIcon() + { + IconUrl = "iconUrl-esMX", + IconFileType = "fileType-esMX", + IconResolution = "iconResolution-esMX", + IconTheme = "iconTheme-esMX", + IconSha256 = "iconSha256-esMX", + }, + }, + }, + new ManifestShadowLocalization() + { + PackageLocale = "de-DE", + Icons = new List<ManifestIcon>() + { + new ManifestIcon() + { + IconUrl = "iconUrl-de-DE", + IconFileType = "fileType-de-DE", + IconResolution = "iconResolution-de-DE", + IconTheme = "iconTheme-de-DE", + IconSha256 = "iconSha256-de-DE", + }, + }, + }, + }; + + var serialized = shadowManifest.Serialize(); + Assert.Equal(File.ReadAllText(Path.Combine(testCollateralDir, "ExpectedShadowManifest.yaml")), serialized); + this.log.WriteLine(serialized); + } + } +} diff --git a/src/WinGetUtilInterop.UnitTests/Common/FactSkipx64CI.cs b/src/WinGetUtilInterop.UnitTests/Common/FactSkipx64CI.cs @@ -0,0 +1,28 @@ +// ----------------------------------------------------------------------------- +// <copyright file="FactSkipx64CI.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace WinGetUtilInterop.UnitTests.Common +{ + using System; + using Xunit; + + /// <summary> + /// Skip fact tests if running in CI x64 builds. + /// </summary> + public class FactSkipx64CI : FactAttribute + { + /// <summary> + /// Initializes a new instance of the <see cref="FactSkipx64CI"/> class. + /// </summary> + public FactSkipx64CI() + { + if (Environment.Is64BitProcess && Environment.GetEnvironmentVariable("BUILD_BUILDNUMBER") is not null) + { + this.Skip = "Skip test for x64 CI builds"; + } + } + } +} diff --git a/src/WinGetUtilInterop.UnitTests/ManifestUnitTest/ManifestEqualityUnitTests.cs b/src/WinGetUtilInterop.UnitTests/ManifestUnitTest/ManifestEqualityUnitTests.cs @@ -4,7 +4,7 @@ // </copyright> // ----------------------------------------------------------------------------- -namespace Microsoft.WinGetUtil.UnitTests.ManifestUnitTest +namespace WinGetUtilInterop.UnitTests.ManifestUnitTest { using System.IO; using System.Reflection; diff --git a/src/WinGetUtilInterop.UnitTests/ManifestUnitTest/V1ManifestReadTest.cs b/src/WinGetUtilInterop.UnitTests/ManifestUnitTest/V1ManifestReadTest.cs @@ -4,7 +4,7 @@ // </copyright> // ----------------------------------------------------------------------------- -namespace Microsoft.WinGetUtil.UnitTests.ManifestUnitTest +namespace WinGetUtilInterop.UnitTests.ManifestUnitTest { using System.IO; using System.Reflection; diff --git a/src/WinGetUtilInterop.UnitTests/TestCollateral/ExpectedShadowManifest.yaml b/src/WinGetUtilInterop.UnitTests/TestCollateral/ExpectedShadowManifest.yaml @@ -0,0 +1,26 @@ +PackageIdentifier: Package.package +PackageVersion: 1.0 +ManifestType: shadow +ManifestVersion: 1.5 +PackageLocale: en-US +Icons: +- IconUrl: iconUrl + IconFileType: fileType + IconResolution: iconResolution + IconTheme: iconTheme + IconSha256: iconSha256 +Localization: +- PackageLocale: es-MX + Icons: + - IconUrl: iconUrl-esMX + IconFileType: fileType-esMX + IconResolution: iconResolution-esMX + IconTheme: iconTheme-esMX + IconSha256: iconSha256-esMX +- PackageLocale: de-DE + Icons: + - IconUrl: iconUrl-de-DE + IconFileType: fileType-de-DE + IconResolution: iconResolution-de-DE + IconTheme: iconTheme-de-DE + IconSha256: iconSha256-de-DE diff --git a/src/WinGetUtilInterop.UnitTests/WinGetUtilInterop.UnitTests.csproj b/src/WinGetUtilInterop.UnitTests/WinGetUtilInterop.UnitTests.csproj @@ -26,7 +26,7 @@ <ItemGroup> <ProjectReference Include="..\WinGetUtilInterop\WinGetUtilInterop.csproj" /> - <ProjectReference Include="..\WinGetUtil\WinGetUtil.vcxproj" > + <ProjectReference Include="..\WinGetUtil\WinGetUtil.vcxproj"> <OutputItemType>Content</OutputItemType> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <ReferenceOutputAssembly>True</ReferenceOutputAssembly> @@ -43,6 +43,9 @@ <None Update="TestCollateral\DifferentId.yaml"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </None> + <None Update="TestCollateral\ExpectedShadowManifest.yaml"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> <None Update="TestCollateral\PackageTestNewName.yaml"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </None> @@ -92,5 +95,24 @@ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> </ItemGroup> + + <ItemGroup> + <Folder Include="TestCollateral\Shadow\" /> + </ItemGroup> + + <ItemGroup> + <None Include="..\AppInstallerCLITests\TestData\MultiFileManifestV1_5\ManifestV1_5-MultiFile-Version.yaml" Link="TestCollateral\Shadow\ManifestV1_5-MultiFile-Version.yaml"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + <None Include="..\AppInstallerCLITests\TestData\Shadow\V1_5\ManifestV1_5-Shadow-DefaultLocale.yaml" Link="TestCollateral\Shadow\ManifestV1_5-Shadow-DefaultLocale.yaml"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + <None Include="..\AppInstallerCLITests\TestData\Shadow\V1_5\ManifestV1_5-Shadow-Installer.yaml" Link="TestCollateral\Shadow\ManifestV1_5-Shadow-Installer.yaml"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + <None Include="..\AppInstallerCLITests\TestData\Shadow\V1_5\ManifestV1_5-Shadow-Shadow.yaml" Link="TestCollateral\Shadow\ManifestV1_5-Shadow-Shadow.yaml"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + </ItemGroup> </Project> diff --git a/src/WinGetUtilInterop/Common/Enums.cs b/src/WinGetUtilInterop/Common/Enums.cs @@ -31,6 +31,11 @@ namespace Microsoft.WinGetUtil.Common /// </summary> SchemaAndSemanticValidation = 0x2, + /// <summary> + /// Allow shadow manifest + /// </summary> + AllowShadowManifest = 0x4, + // Below options are additional validation behaviors if needed /// <summary> diff --git a/src/WinGetUtilInterop/Common/Helpers.cs b/src/WinGetUtilInterop/Common/Helpers.cs @@ -0,0 +1,40 @@ +// ----------------------------------------------------------------------------- +// <copyright file="Helpers.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGetUtil.Common +{ + using YamlDotNet.Serialization; + using YamlDotNet.Serialization.NamingConventions; + + /// <summary> + /// Helpers. + /// </summary> + internal static class Helpers + { + /// <summary> + /// Helper to deserialize the manifest. + /// </summary> + /// <returns>IDeserializer object.</returns> + public static IDeserializer CreateDeserializer() + { + return new DeserializerBuilder() + .WithNamingConvention(PascalCaseNamingConvention.Instance) + .IgnoreUnmatchedProperties() + .Build(); + } + + /// <summary> + /// Helper to serialize the manifest. + /// </summary> + /// <returns>ISerializer object.</returns> + public static ISerializer CreateSerializer() + { + return new SerializerBuilder() + .WithNamingConvention(PascalCaseNamingConvention.Instance) + .Build(); + } + } +} diff --git a/src/WinGetUtilInterop/Manifest/V1/Manifest.cs b/src/WinGetUtilInterop/Manifest/V1/Manifest.cs @@ -8,8 +8,8 @@ namespace Microsoft.WinGetUtil.Models.V1 { using System.Collections.Generic; using System.IO; + using Microsoft.WinGetUtil.Common; using YamlDotNet.Serialization; - using YamlDotNet.Serialization.NamingConventions; /// <summary> /// Class that defines the structure of the manifest. Uses YamlDotNet @@ -369,7 +369,7 @@ namespace Microsoft.WinGetUtil.Models.V1 public static Manifest CreateManifestFromStreamReader(StreamReader streamReader) { streamReader.BaseStream.Seek(0, SeekOrigin.Begin); - var deserializer = CreateDeserializer(); + var deserializer = Helpers.CreateDeserializer(); return deserializer.Deserialize<Manifest>(streamReader); } @@ -380,7 +380,7 @@ namespace Microsoft.WinGetUtil.Models.V1 /// <returns>Manifest object populated and validated.</returns> public static Manifest CreateManifestFromString(string value) { - var deserializer = CreateDeserializer(); + var deserializer = Helpers.CreateDeserializer(); return deserializer.Deserialize<Manifest>(value); } @@ -450,6 +450,28 @@ namespace Microsoft.WinGetUtil.Models.V1 } } + if (this.Documentations != null) + { + foreach (var docs in this.Documentations) + { + if (!string.IsNullOrEmpty(docs.DocumentUrl)) + { + uris.Add(docs.DocumentUrl); + } + } + } + + if (this.Icons != null) + { + foreach (var icon in this.Icons) + { + if (!string.IsNullOrEmpty(icon.IconUrl)) + { + uris.Add(icon.IconUrl); + } + } + } + return uris; } @@ -488,18 +510,6 @@ namespace Microsoft.WinGetUtil.Models.V1 this.CompareInstallers(other.Installers); } - /// <summary> - /// Helper to deserialize the manifest. - /// </summary> - /// <returns>IDeserializer object.</returns> - private static IDeserializer CreateDeserializer() - { - var deserializer = new DeserializerBuilder(). - WithNamingConvention(PascalCaseNamingConvention.Instance). - IgnoreUnmatchedProperties(); - return deserializer.Build(); - } - private bool CompareInstallers(List<ManifestInstaller> installers) { ISet<ManifestInstaller> first = diff --git a/src/WinGetUtilInterop/Manifest/V1/ManifestLocalization.cs b/src/WinGetUtilInterop/Manifest/V1/ManifestLocalization.cs @@ -104,6 +104,26 @@ namespace Microsoft.WinGetUtil.Models.V1 public string ReleaseNotesUrl { get; set; } /// <summary> + /// Gets or sets the purchase url of the package. + /// </summary> + public string PurchaseUrl { get; set; } + + /// <summary> + /// Gets or sets the installation notes. + /// </summary> + public string InstallationNotes { get; set; } + + /// <summary> + /// Gets or sets the manifest documentation. + /// </summary> + public List<ManifestDocumentation> Documentations { get; set; } + + /// <summary> + /// Gets or sets the manifest icons information. + /// </summary> + public List<ManifestIcon> Icons { get; set; } + + /// <summary> /// Returns a List of strings containing the URIs contained within this localization. /// </summary> /// <returns>List of strings.</returns> @@ -156,6 +176,28 @@ namespace Microsoft.WinGetUtil.Models.V1 } } + if (this.Documentations != null) + { + foreach (var docs in this.Documentations) + { + if (!string.IsNullOrEmpty(docs.DocumentUrl)) + { + uris.Add(docs.DocumentUrl); + } + } + } + + if (this.Icons != null) + { + foreach (var icon in this.Icons) + { + if (!string.IsNullOrEmpty(icon.IconUrl)) + { + uris.Add(icon.IconUrl); + } + } + } + return uris; } } diff --git a/src/WinGetUtilInterop/Manifest/V1/ManifestShadow.cs b/src/WinGetUtilInterop/Manifest/V1/ManifestShadow.cs @@ -0,0 +1,159 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ManifestShadow.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGetUtil.Manifest.V1 +{ + using System; + using System.Collections.Generic; + using System.IO; + using Microsoft.WinGetUtil.Common; + using Microsoft.WinGetUtil.Exceptions; + using Microsoft.WinGetUtil.Models.V1; + using YamlDotNet.Serialization; + + /// <summary> + /// Model for manifest type shadow. + /// </summary> + public sealed class ManifestShadow + { + /// <summary> + /// Gets or sets the Id of the package. + /// </summary> + [YamlMember(Alias = "PackageIdentifier")] + public string Id { get; set; } + + /// <summary> + /// Gets or sets the version of the package. + /// </summary> + [YamlMember(Alias = "PackageVersion")] + public string Version { get; set; } + + /// <summary> + /// Gets or sets the manifest type. + /// </summary> + public string ManifestType { get; set; } + + /// <summary> + /// Gets or sets the manifest version. + /// </summary> + public string ManifestVersion { get; set; } + + /// <summary> + /// Gets or sets the default locale. + /// </summary> + public string PackageLocale { get; set; } + + /// <summary> + /// Gets or sets the manifest icons information. + /// </summary> + public List<ManifestIcon> Icons { get; set; } + + /// <summary> + /// Gets or sets collection of additional ManifestShadowLocalization. + /// </summary> + public List<ManifestShadowLocalization> Localization { get; set; } + + /// <summary> + /// Creates a shadow manifest. + /// Assigns the correct ManifestType. + /// </summary> + /// <returns>A shadow manifest.</returns> + public static ManifestShadow CreateManifest() + { + return new ManifestShadow() + { + ManifestType = "shadow", + }; + } + + /// <summary> + /// Deserialize a stream reader into a ManifestShadow object. + /// </summary> + /// <param name="filePath">file path.</param> + /// <returns>ManifestShadow object populated and validated.</returns> + public static ManifestShadow CreateManifestFromPath(string filePath) + { + using (StreamReader streamReader = new StreamReader(filePath)) + { + return CreateManifestFromStreamReader(streamReader); + } + } + + /// <summary> + /// Deserialize a stream into a ManifestShadow object. + /// </summary> + /// <param name="stream">Manifest stream.</param> + /// <returns>ManifestShadow object populated and validated.</returns> + public static ManifestShadow CreateManifestFromStream(Stream stream) + { + using (StreamReader streamReader = new StreamReader(stream)) + { + return CreateManifestFromStreamReader(streamReader); + } + } + + /// <summary> + /// Deserialize a stream reader into a ManifestShadow object. + /// </summary> + /// <param name="streamReader">stream reader.</param> + /// <returns>Manifest object populated and validated.</returns> + public static ManifestShadow CreateManifestFromStreamReader(StreamReader streamReader) + { + streamReader.BaseStream.Seek(0, SeekOrigin.Begin); + var deserializer = Helpers.CreateDeserializer(); + var shadow = deserializer.Deserialize<ManifestShadow>(streamReader); + shadow.Validate(); + return shadow; + } + + /// <summary> + /// Deserialize a string into a ManifestShadow object. + /// </summary> + /// <param name="value">Manifest in string value.</param> + /// <returns>ManifestShadow object populated and validated.</returns> + public static ManifestShadow CreateManifestFromString(string value) + { + var deserializer = Helpers.CreateDeserializer(); + var shadow = deserializer.Deserialize<ManifestShadow>(value); + shadow.Validate(); + return shadow; + } + + /// <summary> + /// Serializes the shadow manifest. + /// </summary> + /// <returns>Serialized shadow manifest as string.</returns> + public string Serialize() + { + this.Validate(); + var serializer = Helpers.CreateSerializer(); + return serializer.Serialize(this); + } + + private void Validate() + { + if (this.ManifestType != "shadow") + { + throw new WinGetManifestException("Invalid shadow manifest"); + } + + if (string.IsNullOrEmpty(this.Id)) + { + throw new ArgumentNullException(nameof(this.Id)); + } + + if (string.IsNullOrEmpty(this.Version)) + { + throw new ArgumentNullException(nameof(this.Version)); + } + + if (string.IsNullOrEmpty(this.ManifestVersion)) + { + throw new ArgumentNullException(nameof(this.ManifestVersion)); + } + } + } +} diff --git a/src/WinGetUtilInterop/Manifest/V1/ManifestShadowLocalization.cs b/src/WinGetUtilInterop/Manifest/V1/ManifestShadowLocalization.cs @@ -0,0 +1,48 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ManifestShadowLocalization.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGetUtil.Models.V1 +{ + using System.Collections.Generic; + + /// <summary> + /// Localization properties for shadow manifest. + /// </summary> + public class ManifestShadowLocalization + { + /// <summary> + /// Gets or sets the locale. + /// </summary> + public string PackageLocale { get; set; } + + /// <summary> + /// Gets or sets the manifest icons information. + /// </summary> + public List<ManifestIcon> Icons { get; set; } + + /// <summary> + /// Returns a List of strings containing the URIs contained within this localization. + /// </summary> + /// <returns>List of strings.</returns> + public List<string> GetURIs() + { + List<string> uris = new List<string>(); + + if (this.Icons != null) + { + foreach (var icon in this.Icons) + { + if (!string.IsNullOrEmpty(icon.IconUrl)) + { + uris.Add(icon.IconUrl); + } + } + } + + return uris; + } + } +}