commit cca8545f18f51bb094fbd096e1d7700875174754 parent 009e6849d839ddb1599c65aec9ac076ead4dcfa9 Author: yao-msft <50888816+yao-msft@users.noreply.github.com> Date: Wed, 17 Feb 2021 13:32:46 -0800 Implement V1 manifest and schema validation (#740) Diffstat:
82 files changed, 5064 insertions(+), 1254 deletions(-)
diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt @@ -13,6 +13,7 @@ apps appx appxbundle appxmanifest +appxsdk APSTUDIO argc args @@ -68,6 +69,7 @@ dbconn DBId declspec decltype +defaultlocale delstore Demitrius denelon @@ -128,14 +130,18 @@ getline github githubusercontent hfile +HGLOBAL hinternet HKEY hmac +HMODULE +homepage Homepage hostname hpp HRESULT hresult +HRSRC hstring html http @@ -146,11 +152,13 @@ IAsync IBuffer icu IDisposable +IDX ifdef ifndef ifstream IInput IInspectable +IIS ILogger impl Inet @@ -188,7 +196,9 @@ LPVOID mailto MAJORVERSION makeappx +MAKEINTRESOURCE makemsix +MANIFESTSCHEMA MANIFESTVERSION mday metadata @@ -210,6 +220,7 @@ msixsdk msixsdkx msixtest msrc +Multifile Multimatch mutex namespace @@ -246,12 +257,14 @@ ostringstream OSVERSIONINFOEXW outfile OUTOFMEMORY +OWC Params parentidx pathpart Pathto PBYTE pch +PCWSTR pdb PEVENT pfp @@ -276,6 +289,7 @@ ptr publiccontainer PUCHAR PVOID +pwa QCol RAII rdbuf @@ -436,6 +450,8 @@ usersources utext utf uuidof +validator +valijson vcxitems vcxproj vdproj diff --git a/.github/actions/spelling/patterns.txt b/.github/actions/spelling/patterns.txt @@ -30,3 +30,6 @@ El proyecto .* diferentes # URLs -- Added here instead of allow.txt to facilitate wildcarding them as more are added http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer + +# schema regex +"pattern": .*$+ \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml @@ -203,13 +203,13 @@ jobs: name: AppInstallerTest displayName: 'Download Source Package Certificate' inputs: - secureFile: 'AppInstallerTest.pfx' + secureFile: 'AppInstallerTest.pfx' - task: DownloadSecureFile@1 name: HTTPSDevCert displayName: 'Download Kestrel Certificate' inputs: - secureFile: 'HTTPSDevCert.pfx' + secureFile: 'HTTPSDevCert.pfx' - task: MSBuild@1 displayName: Build MSIX Test Installer File diff --git a/schemas/JSON/manifests/preview/manifest.0.1.0.json b/schemas/JSON/manifests/preview/manifest.0.1.0.json @@ -0,0 +1,315 @@ +{ + "$id": "https://aka.ms/winget-manifest.0.1.0.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "A single-file manifest representing a package in winget community repo. v0.1.0 Preview", + "definitions": { + "InstallerType": { + "type": [ "string", "null" ], + "pattern": "^(([Ee][Xx][Ee])|([Mm][Ss][Ii])|([Mm][Ss][Ii][Xx])|([Ii][Nn][Nn][Oo])|([Ww][Ii][Xx])|([Nn][Uu][Ll][Ll][Ss][Oo][Ff][Tt])|([Aa][Pp][Pp][Xx])|([Zz][Ii][Pp])|([Bb][Uu][Rr][Nn]))$", + "description": "InstallerType is required under Installer node if it's not defined in root" + }, + "UpdateBehavior": { + "type": [ "string", "null" ], + "pattern": "^(([Ii][Nn][Ss][Tt][Aa][Ll][Ll])|([Uu][Nn][Ii][Nn][Ss][Tt][Aa][Ll][Ll][Pp][Rr][Ee][Vv][Ii][Oo][Uu][Ss]))$", + "description": "UpdateBehavior is used to specify desired action during package upgrade" + }, + "PackageFamilyName": { + "type": [ "string", "null" ], + "pattern": "^[A-Za-z0-9][-\\.A-Za-z0-9]+_[A-Za-z0-9]{13}$", + "maxLength": 255, + "description": "PackageFamilyName for appx or msix installer. Could be used for correlation of packages across sources" + }, + "ProductCode": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 255, + "description": "ProductCode could be used for correlation of packages across sources" + }, + "Description": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 10000, + "description": "Description of the package" + }, + "Url": { + "type": [ "string", "null" ], + "format": "uri", + "pattern": "^([Hh][Tt][Tt][Pp][Ss]?)://", + "maxLength": 2000 + }, + "Homepage": { + "$ref": "#/definitions/Url", + "description": "Homepage is a Url where the user can find more information about the package" + }, + "LicenseUrl": { + "$ref": "#/definitions/Url", + "description": "LicenseUrl provides a link to the license for the user to read" + }, + "InstallerSwitches": { + "type": [ "object", "null" ], + "properties": { + "Custom": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 1000, + "description": "Custom switches will be passed directly to the installer by winget" + }, + "Silent": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 100, + "description": "Silent is the value that should be passed to the installer when user chooses a silent or quiet install" + }, + "SilentWithProgress": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 100, + "description": "SilentWithProgress is the value that should be passed to the installer when user chooses a non-interactive install" + }, + "Interactive": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 100, + "description": "Interactive is the value that should be passed to the installer when user chooses an interactive install" + }, + "Language": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 100, + "description": "Some installers include all localized resources. By specifying a Language switch, winget will pass the value of Language to the installer. This is not yet supported in Preview releases" + }, + "Log": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 100, + "description": "Log is the value passed to the installer for custom log file path. <LOGPATH> token can be included in the switch value so that winget will replace the token with user provided path" + }, + "InstallLocation": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 100, + "description": "InstallLocation is the value passed to the installer for custom install location. <INSTALLPATH> token can be included in the switch value so that winget will replace the token with user provided path" + }, + "Update": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 100, + "description": "Update is the value that should be passed to the installer when user chooses an upgrade" + } + } + }, + "Installer": { + "type": "object", + "properties": { + "Arch": { + "type": "string", + "pattern": "^(([Aa][Rr][Mm])|([Xx]86)|([Xx]64)|([Aa][Rr][Mm]64)|([Nn][Ee][Uu][Tt][Rr][Aa][Ll]))$", + "description": "Arch is required. The installer architecture" + }, + "Url": { + "type": "string", + "format": "uri", + "pattern": "^([Hh][Tt][Tt][Pp][Ss]?)://", + "maxLength": 2000, + "description": "Url is required. The path to the installer." + }, + "Sha256": { + "type": "string", + "pattern": "^[A-Fa-f0-9]{64}$", + "description": "Sha256 is required. Sha256 of the installer" + }, + "SignatureSha256": { + "type": [ "string", "null" ], + "pattern": "^[A-Fa-f0-9]{64}$", + "description": "SignatureSha256 is recommended for appx or msix. It is the sha256 of signature file inside appx or msix. Could be used during streaming install if applicable" + }, + "Language": { + "type": [ "string", "null" ], + "minLength": 2, + "maxLength": 20, + "description": "Language is the specific language of the installer. Language must follow IETF language tag guidelines" + }, + "Scope": { + "type": [ "string", "null" ], + "pattern": "^(([Uu][Ss][Ee][Rr])|([Mm][Aa][Cc][Hh][Ii][Nn][Ee]))$", + "description": "Scope indicates if the installer is per user or per machine. Scope is not yet supported in Preview releases" + }, + "InstallerType": { + "$ref": "#/definitions/InstallerType" + }, + "UpdateBehavior": { + "$ref": "#/definitions/UpdateBehavior" + }, + "PackageFamilyName": { + "$ref": "#/definitions/PackageFamilyName" + }, + "ProductCode": { + "$ref": "#/definitions/ProductCode" + }, + "Switches": { + "$ref": "#/definitions/InstallerSwitches" + } + }, + "required": [ + "Arch", + "Url", + "Sha256" + ] + }, + "ManifestLocalization": { + "type": "object", + "properties": { + "Language": { + "type": "string", + "minLength": 2, + "maxLength": 20, + "description": "Language is the specific language of the localization. Language must follow IETF language tag guidelines" + }, + "Description": { + "$ref": "#/definitions/Description" + }, + "Homepage": { + "$ref": "#/definitions/Homepage" + }, + "LicenseUrl": { + "$ref": "#/definitions/LicenseUrl" + } + }, + "required": [ + "Language" + ] + } + }, + "type": "object", + "properties": { + "ManifestVersion": { + "type": "string", + "default": "0.1.0", + "pattern": "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){2}$", + "description": "The manifest syntax version" + }, + "Id": { + "type": "string", + "pattern": "^[^\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+\\.[^\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$", + "maxLength": 255, + "description": "Id is a required field. It MUST include the publisher name and package name separated by a period. For example: Publisher.Package" + }, + "Name": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "Name is a required field. The name of the package" + }, + "Version": { + "type": "string", + "pattern": "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$", + "minLength": 1, + "description": "Version is a required field. The version of the package" + }, + "Publisher": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "Publisher is a required field. The legal publisher name" + }, + "AppMoniker": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 100, + "description": "AppMoniker is the common name someone may use to search for the package" + }, + "Channel": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 40, + "description": "Channel a string representing the flight ring. For example: stable, beta, canary" + }, + "Author": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 100, + "description": "The person or company responsible for authoring the package" + }, + "License": { + "type": "string", + "minLength": 1, + "maxLength": 1000, + "description": "License is a required field. License provides the type of license the package is provided under" + }, + "MinOSVersion": { + "type": [ "string", "null" ], + "pattern": "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){0,3}$", + "description": "MinOSVersion uses the Windows version to limit installations on unsupported platforms" + }, + "Tags": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 1000, + "description": "Tags is a comma separated list. They represent strings that user may use to search for the package" + }, + "Commands": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 1000, + "description": "Commands is a comma separated list. They are the common executable or alias that user might type trying to run the package" + }, + "Protocols": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 1000, + "description": "Protocols is a comma separated list. Protocols provides the list of protocols the package provides a handler for" + }, + "FileExtensions": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 1000, + "description": "FileExtensions is a comma separated list. FileExtensions provides the list of extensions the package can support" + }, + "InstallerType": { + "$ref": "#/definitions/InstallerType" + }, + "UpdateBehavior": { + "$ref": "#/definitions/UpdateBehavior" + }, + "PackageFamilyName": { + "$ref": "#/definitions/PackageFamilyName" + }, + "ProductCode": { + "$ref": "#/definitions/ProductCode" + }, + "Description": { + "$ref": "#/definitions/Description" + }, + "Homepage": { + "$ref": "#/definitions/Homepage" + }, + "LicenseUrl": { + "$ref": "#/definitions/LicenseUrl" + }, + "Switches": { + "$ref": "#/definitions/InstallerSwitches" + }, + "Installers": { + "type": "array", + "items": { + "$ref": "#/definitions/Installer" + }, + "minItems": 1, + "uniqueItems": true + }, + "Localization": { + "type": [ "array", "null" ], + "items": { + "$ref": "#/definitions/ManifestLocalization" + } + } + }, + "required": [ + "Id", + "Name", + "Version", + "Publisher", + "License", + "Installers" + ] +}+ \ No newline at end of file diff --git a/schemas/JSON/manifests/v1.0.0/manifest.defaultLocale.1.0.0.json b/schemas/JSON/manifests/v1.0.0/manifest.defaultLocale.1.0.0.json @@ -0,0 +1,144 @@ +{ + "$id": "https://aka.ms/winget-manifest.defaultlocale.1.0.0.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "A representation of a multiple-file manifest representing a default app metadata in the OWC. v1.0.0", + "definitions": { + "Url": { + "type": [ "string", "null" ], + "format": "uri", + "pattern": "^([Hh][Tt][Tt][Pp][Ss]?)://", + "maxLength": 2000, + "description": "Optional Url type" + }, + "Tag": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 32, + "pattern": "^\\S+$", + "description": "Package moniker or tag" + } + }, + "type": "object", + "properties": { + "PackageIdentifier": { + "type": "string", + "pattern": "^[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}(\\.[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}){1,3}$", + "maxLength": 128, + "description": "The package unique identifier" + }, + "PackageVersion": { + "type": "string", + "pattern": "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$", + "maxLength": 128, + "description": "The package version" + }, + "PackageLocale": { + "type": "string", + "pattern": "^([a-zA-Z]{2}|[iI]-[a-zA-Z]+|[xX]-[a-zA-Z]{1,8})(-[a-zA-Z]{1,8})*$", + "maxLength": 20, + "description": "The package meta-data locale" + }, + "Publisher": { + "type": "string", + "minLength": 2, + "maxLength": 64, + "description": "The publisher name" + }, + "PublisherUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher home page" + }, + "PublisherSupportUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher support page" + }, + "PrivacyUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher privacy page or the package privacy page" + }, + "Author": { + "type": [ "string", "null" ], + "minLength": 2, + "maxLength": 256, + "description": "The package author" + }, + "PackageName": { + "type": "string", + "minLength": 2, + "maxLength": 64, + "description": "The package name" + }, + "PackageUrl": { + "$ref": "#/definitions/Url", + "description": "The package home page" + }, + "License": { + "type": "string", + "minLength": 3, + "maxLength": 512, + "description": "The package license" + }, + "LicenseUrl": { + "$ref": "#/definitions/Url", + "description": "The license page" + }, + "Copyright": { + "type": [ "string", "null" ], + "minLength": 3, + "maxLength": 512, + "description": "The package copyright" + }, + "CopyrightUrl": { + "$ref": "#/definitions/Url", + "description": "The package copyright page" + }, + "ShortDescription": { + "type": "string", + "minLength": 3, + "maxLength": 256, + "description": "The short package description" + }, + "Description": { + "type": [ "string", "null" ], + "minLength": 3, + "maxLength": 10000, + "description": "The full package description" + }, + "Moniker": { + "$ref": "#/definitions/Tag", + "description": "The most common package term" + }, + "Tags": { + "type": [ "array", "null" ], + "items": { + "$ref": "#/definitions/Tag" + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of additional package search terms" + }, + "ManifestType": { + "type": "string", + "default": "defaultLocale", + "const": "defaultLocale", + "description": "The manifest type" + }, + "ManifestVersion": { + "type": "string", + "default": "1.0.0", + "pattern": "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){2}$", + "description": "The manifest syntax version" + } + }, + "required": [ + "PackageIdentifier", + "PackageVersion", + "PackageLocale", + "Publisher", + "PackageName", + "License", + "ShortDescription", + "ManifestType", + "ManifestVersion" + ] +}+ \ No newline at end of file diff --git a/schemas/JSON/manifests/v1.0.0/manifest.installer.1.0.0.json b/schemas/JSON/manifests/v1.0.0/manifest.installer.1.0.0.json @@ -0,0 +1,450 @@ +{ + "$id": "https://aka.ms/winget-manifest.installer.1.0.0.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "A representation of a single-file manifest representing an app installers in the OWC. v1.0.0", + "definitions": { + "PackageIdentifier": { + "type": "string", + "pattern": "^[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}(\\.[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}){1,3}$", + "maxLength": 128, + "description": "The package unique identifier" + }, + "PackageVersion": { + "type": "string", + "pattern": "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$", + "maxLength": 128, + "description": "The package version" + }, + "Locale": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 20, + "description": "The package meta-data locale" + }, + "Channel": { + "type": [ "string", "null" ], + "minLength": 2, + "maxLength": 16, + "description": "The distribution channel" + }, + "Platform": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "enum": [ + "Windows.Desktop", + "Windows.Universal" + ] + }, + "maxItems": 2, + "uniqueItems": true, + "description": "The installer supported operating system" + }, + "MinimumOSVersion": { + "type": [ "string", "null" ], + "pattern": "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){0,3}$", + "description": "The installer minimum operating system version" + }, + "InstallerType": { + "type": [ "string", "null" ], + "enum": [ + "msix", + "msi", + "appx", + "exe", + "zip", + "inno", + "nullsoft", + "wix", + "burn", + "pwa" + ], + "description": "Enumeration of supported installer types" + }, + "Scope": { + "type": [ "string", "null" ], + "enum": [ + "user", + "machine" + ], + "description": "Scope indicates if the installer is per user or per machine" + }, + "InstallModes": { + "type": [ "array", "null" ], + "items": { + "$ref": "#/definitions/Scope" + }, + "maxItems": 2, + "uniqueItems": true, + "description": "List of supported installer modes" + }, + "InstallerSwitches": { + "type": "object", + "properties": { + "Silent": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Silent is the value that should be passed to the installer when user chooses a silent or quiet install" + }, + "SilentWithProgress": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "SilentWithProgress is the value that should be passed to the installer when user chooses a non-interactive install" + }, + "Interactive": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Interactive is the value that should be passed to the installer when user chooses an interactive install" + }, + "InstallLocation": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "InstallLocation is the value passed to the installer for custom install location. <INSTALLPATH> token can be included in the switch value so that winget will replace the token with user provided path" + }, + "Log": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Log is the value passed to the installer for custom log file path. <LOGPATH> token can be included in the switch value so that winget will replace the token with user provided path" + }, + "Upgrade": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Upgrade is the value that should be passed to the installer when user chooses an upgrade" + }, + "Custom": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Custom switches will be passed directly to the installer by winget" + } + } + }, + "InstallerSuccessCodes": { + "type": [ "array", "null" ], + "items": { + "type": "integer", + "not": { + "enum": [ 0 ] + } + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of non-zero installer success exit codes" + }, + "UpgradeBehavior": { + "type": [ "string", "null" ], + "enum": [ + "install", + "uninstallPrevious" + ], + "description": "The upgrade method" + }, + "Commands": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of commands or aliases to run the package" + }, + "Protocols": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "pattern": "^[a-z][-a-z0-9\\.\\+]*$", + "maxLength": 39 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of protocols the package provides a handler for" + }, + "FileExtensions": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "pattern": "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$", + "maxLength": 40 + }, + "maxItems": 256, + "uniqueItems": true, + "description": "List of file extensions the package could support" + }, + "Dependencies": { + "type": [ "object", "null" ], + "properties": { + "WindowsFeatures": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of Windows feature dependencies" + }, + "WindowsLibraries": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of Windows library dependencies" + }, + "PackageDependencies": { + "type": [ "array", "null" ], + "items": { + "type": "object", + "properties": { + "PackageIdentifier": { + "$ref": "#/definitions/PackageIdentifier" + }, + "MinimumVersion": { + "$ref": "#/definitions/PackageVersion" + } + }, + "required": [ "PackageIdentifier" ] + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of package dependencies from current source" + }, + "ExternalDependencies": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of external package dependencies" + } + } + }, + "PackageFamilyName": { + "type": [ "string", "null" ], + "pattern": "^[A-Za-z0-9][-\\.A-Za-z0-9]+_[A-Za-z0-9]{13}$", + "maxLength": 255, + "description": "PackageFamilyName for appx or msix installer. Could be used for correlation of packages across sources" + }, + "ProductCode": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 255, + "description": "ProductCode could be used for correlation of packages across sources" + }, + "Capabilities": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "maxItems": 1000, + "uniqueItems": true, + "description": "List of appx or msix installer capabilities" + }, + "RestrictedCapabilities": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "maxItems": 1000, + "uniqueItems": true, + "description": "List of appx or msix installer restricted capabilities" + }, + "Installer": { + "type": "object", + "properties": { + "InstallerLocale": { + "$ref": "#/definitions/Locale" + }, + "Platform": { + "$ref": "#/definitions/Platform" + }, + "MinimumOSVersion": { + "$ref": "#/definitions/MinimumOSVersion" + }, + "Architecture": { + "type": "string", + "enum": [ + "x86", + "x64", + "arm", + "arm64", + "neutral" + ], + "description": "The installer target architecture" + }, + "InstallerType": { + "$ref": "#/definitions/InstallerType" + }, + "Scope": { + "$ref": "#/definitions/Scope" + }, + "InstallerUrl": { + "type": "string", + "format": "uri", + "pattern": "^([Hh][Tt][Tt][Pp][Ss]?)://", + "description": "The installer Url" + }, + "InstallerSha256": { + "type": "string", + "pattern": "^[A-Fa-f0-9]{64}$", + "description": "Sha256 is required. Sha256 of the installer" + }, + "SignatureSha256": { + "type": [ "string", "null" ], + "pattern": "^[A-Fa-f0-9]{64}$", + "description": "SignatureSha256 is recommended for appx or msix. It is the sha256 of signature file inside appx or msix. Could be used during streaming install if applicable" + }, + "InstallModes": { + "$ref": "#/definitions/InstallModes" + }, + "InstallerSwitches": { + "$ref": "#/definitions/InstallerSwitches" + }, + "InstallerSuccessCodes": { + "$ref": "#/definitions/InstallerSuccessCodes" + }, + "UpgradeBehavior": { + "$ref": "#/definitions/UpgradeBehavior" + }, + "Commands": { + "$ref": "#/definitions/Commands" + }, + "Protocols": { + "$ref": "#/definitions/Protocols" + }, + "FileExtensions": { + "$ref": "#/definitions/FileExtensions" + }, + "Dependencies": { + "$ref": "#/definitions/Dependencies" + }, + "PackageFamilyName": { + "$ref": "#/definitions/PackageFamilyName" + }, + "ProductCode": { + "$ref": "#/definitions/ProductCode" + }, + "Capabilities": { + "$ref": "#/definitions/Capabilities" + }, + "RestrictedCapabilities": { + "$ref": "#/definitions/RestrictedCapabilities" + } + }, + "required": [ + "Architecture", + "InstallerUrl", + "InstallerSha256" + ] + } + }, + "type": "object", + "properties": { + "PackageIdentifier": { + "$ref": "#/definitions/PackageIdentifier" + }, + "PackageVersion": { + "$ref": "#/definitions/PackageVersion" + }, + "Channel": { + "$ref": "#/definitions/Channel" + }, + "InstallerLocale": { + "$ref": "#/definitions/Locale" + }, + "Platform": { + "$ref": "#/definitions/Platform" + }, + "MinimumOSVersion": { + "$ref": "#/definitions/MinimumOSVersion" + }, + "InstallerType": { + "$ref": "#/definitions/InstallerType" + }, + "Scope": { + "$ref": "#/definitions/Scope" + }, + "InstallModes": { + "$ref": "#/definitions/InstallModes" + }, + "InstallerSwitches": { + "$ref": "#/definitions/InstallerSwitches" + }, + "InstallerSuccessCodes": { + "$ref": "#/definitions/InstallerSuccessCodes" + }, + "UpgradeBehavior": { + "$ref": "#/definitions/UpgradeBehavior" + }, + "Commands": { + "$ref": "#/definitions/Commands" + }, + "Protocols": { + "$ref": "#/definitions/Protocols" + }, + "FileExtensions": { + "$ref": "#/definitions/FileExtensions" + }, + "Dependencies": { + "$ref": "#/definitions/Dependencies" + }, + "PackageFamilyName": { + "$ref": "#/definitions/PackageFamilyName" + }, + "ProductCode": { + "$ref": "#/definitions/ProductCode" + }, + "Capabilities": { + "$ref": "#/definitions/Capabilities" + }, + "RestrictedCapabilities": { + "$ref": "#/definitions/RestrictedCapabilities" + }, + "Installers": { + "type": "array", + "items": { + "$ref": "#/definitions/Installer" + }, + "minItems": 1, + "maxItems": 128 + }, + "ManifestType": { + "type": "string", + "default": "installer", + "const": "installer", + "description": "The manifest type" + }, + "ManifestVersion": { + "type": "string", + "default": "1.0.0", + "pattern": "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){2}$", + "description": "The manifest syntax version" + } + }, + "required": [ + "PackageIdentifier", + "PackageVersion", + "Installers", + "ManifestType", + "ManifestVersion" + ] +}+ \ No newline at end of file diff --git a/schemas/JSON/manifests/v1.0.0/manifest.locale.1.0.0.json b/schemas/JSON/manifests/v1.0.0/manifest.locale.1.0.0.json @@ -0,0 +1,140 @@ +{ + "$id": "https://aka.ms/winget-manifest.locale.1.0.0.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "A representation of a multiple-file manifest representing app metadata in other locale in the OWC. v1.0.0", + "definitions": { + "Url": { + "type": [ "string", "null" ], + "format": "uri", + "pattern": "^([Hh][Tt][Tt][Pp][Ss]?)://", + "maxLength": 2000, + "description": "Optional Url type" + }, + "Tag": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 32, + "pattern": "^\\S+$", + "description": "Package moniker or tag" + } + }, + "type": "object", + "properties": { + "PackageIdentifier": { + "type": "string", + "pattern": "^[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}(\\.[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}){1,3}$", + "maxLength": 128, + "description": "The package unique identifier" + }, + "PackageVersion": { + "type": "string", + "pattern": "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$", + "maxLength": 128, + "description": "The package version" + }, + "PackageLocale": { + "type": "string", + "pattern": "^([a-zA-Z]{2}|[iI]-[a-zA-Z]+|[xX]-[a-zA-Z]{1,8})(-[a-zA-Z]{1,8})*$", + "maxLength": 20, + "description": "The package meta-data locale" + }, + "Publisher": { + "type": [ "string", "null" ], + "minLength": 2, + "maxLength": 64, + "description": "The publisher name" + }, + "PublisherUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher home page" + }, + "PublisherSupportUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher support page" + }, + "PrivacyUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher privacy page or the package privacy page" + }, + "Author": { + "type": [ "string", "null" ], + "minLength": 2, + "maxLength": 256, + "description": "The package author" + }, + "PackageName": { + "type": [ "string", "null" ], + "minLength": 2, + "maxLength": 64, + "description": "The package name" + }, + "PackageUrl": { + "$ref": "#/definitions/Url", + "description": "The package home page" + }, + "License": { + "type": [ "string", "null" ], + "minLength": 3, + "maxLength": 512, + "description": "The package license" + }, + "LicenseUrl": { + "$ref": "#/definitions/Url", + "description": "The license page" + }, + "Copyright": { + "type": [ "string", "null" ], + "minLength": 3, + "maxLength": 512, + "description": "The package copyright" + }, + "CopyrightUrl": { + "$ref": "#/definitions/Url", + "description": "The package copyright page" + }, + "ShortDescription": { + "type": [ "string", "null" ], + "minLength": 3, + "maxLength": 256, + "description": "The short package description" + }, + "Description": { + "type": [ "string", "null" ], + "minLength": 3, + "maxLength": 10000, + "description": "The full package description" + }, + "Moniker": { + "$ref": "#/definitions/Tag", + "description": "The most common package term" + }, + "Tags": { + "type": [ "array", "null" ], + "items": { + "$ref": "#/definitions/Tag" + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of additional package search terms" + }, + "ManifestType": { + "type": "string", + "default": "locale", + "const": "locale", + "description": "The manifest type" + }, + "ManifestVersion": { + "type": "string", + "default": "1.0.0", + "pattern": "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){2}$", + "description": "The manifest syntax version" + } + }, + "required": [ + "PackageIdentifier", + "PackageVersion", + "PackageLocale", + "ManifestType", + "ManifestVersion" + ] +}+ \ No newline at end of file diff --git a/schemas/JSON/manifests/v1.0.0/manifest.singleton.1.0.0.json b/schemas/JSON/manifests/v1.0.0/manifest.singleton.1.0.0.json @@ -0,0 +1,550 @@ +{ + "$id": "https://aka.ms/winget-manifest.singleton.1.0.0.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "A representation of a single-file manifest representing an app in the OWC. v1.0.0", + "definitions": { + "PackageIdentifier": { + "type": "string", + "pattern": "^[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}(\\.[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}){1,3}$", + "maxLength": 128, + "description": "The package unique identifier" + }, + "PackageVersion": { + "type": "string", + "pattern": "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$", + "maxLength": 128, + "description": "The package version" + }, + "Locale": { + "type": [ "string", "null" ], + "pattern": "^([a-zA-Z]{2}|[iI]-[a-zA-Z]+|[xX]-[a-zA-Z]{1,8})(-[a-zA-Z]{1,8})*$", + "maxLength": 20, + "description": "The package meta-data locale" + }, + "Url": { + "type": [ "string", "null" ], + "format": "uri", + "pattern": "^([Hh][Tt][Tt][Pp][Ss]?)://", + "maxLength": 2000, + "description": "Optional Url type" + }, + "Tag": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 32, + "pattern": "^\\S+$", + "description": "Package moniker or tag" + }, + "Channel": { + "type": [ "string", "null" ], + "minLength": 2, + "maxLength": 16, + "description": "The distribution channel" + }, + "Platform": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "enum": [ + "Windows.Desktop", + "Windows.Universal" + ] + }, + "maxItems": 2, + "uniqueItems": true, + "description": "The installer supported operating system" + }, + "MinimumOSVersion": { + "type": [ "string", "null" ], + "pattern": "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){0,3}$", + "description": "The installer minimum operating system version" + }, + "InstallerType": { + "type": [ "string", "null" ], + "enum": [ + "msix", + "msi", + "appx", + "exe", + "zip", + "inno", + "nullsoft", + "wix", + "burn", + "pwa" + ], + "description": "Enumeration of supported installer types" + }, + "Scope": { + "type": [ "string", "null" ], + "enum": [ + "user", + "machine" + ], + "description": "Scope indicates if the installer is per user or per machine" + }, + "InstallModes": { + "type": [ "array", "null" ], + "items": { + "$ref": "#/definitions/Scope" + }, + "maxItems": 2, + "uniqueItems": true, + "description": "List of supported installer modes" + }, + "InstallerSwitches": { + "type": "object", + "properties": { + "Silent": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Silent is the value that should be passed to the installer when user chooses a silent or quiet install" + }, + "SilentWithProgress": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "SilentWithProgress is the value that should be passed to the installer when user chooses a non-interactive install" + }, + "Interactive": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Interactive is the value that should be passed to the installer when user chooses an interactive install" + }, + "InstallLocation": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "InstallLocation is the value passed to the installer for custom install location. <INSTALLPATH> token can be included in the switch value so that winget will replace the token with user provided path" + }, + "Log": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Log is the value passed to the installer for custom log file path. <LOGPATH> token can be included in the switch value so that winget will replace the token with user provided path" + }, + "Upgrade": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Upgrade is the value that should be passed to the installer when user chooses an upgrade" + }, + "Custom": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Custom switches will be passed directly to the installer by winget" + } + } + }, + "InstallerSuccessCodes": { + "type": [ "array", "null" ], + "items": { + "type": "integer", + "not": { + "enum": [ 0 ] + } + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of non-zero installer success exit codes" + }, + "UpgradeBehavior": { + "type": [ "string", "null" ], + "enum": [ + "install", + "uninstallPrevious" + ], + "description": "The upgrade method" + }, + "Commands": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of commands or aliases to run the package" + }, + "Protocols": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "pattern": "^[a-z][-a-z0-9\\.\\+]*$", + "maxLength": 39 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of protocols the package provides a handler for" + }, + "FileExtensions": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "pattern": "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$", + "maxLength": 40 + }, + "maxItems": 256, + "uniqueItems": true, + "description": "List of file extensions the package could support" + }, + "Dependencies": { + "type": [ "object", "null" ], + "properties": { + "WindowsFeatures": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of Windows feature dependencies" + }, + "WindowsLibraries": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of Windows library dependencies" + }, + "PackageDependencies": { + "type": [ "array", "null" ], + "items": { + "type": "object", + "properties": { + "PackageIdentifier": { + "$ref": "#/definitions/PackageIdentifier" + }, + "MinimumVersion": { + "$ref": "#/definitions/PackageVersion" + } + }, + "required": [ "PackageIdentifier" ] + }, + "maxItems": 16, + "description": "List of package dependencies from current source" + }, + "ExternalDependencies": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of external package dependencies" + } + } + }, + "PackageFamilyName": { + "type": [ "string", "null" ], + "pattern": "^[A-Za-z0-9][-\\.A-Za-z0-9]+_[A-Za-z0-9]{13}$", + "maxLength": 255, + "description": "PackageFamilyName for appx or msix installer. Could be used for correlation of packages across sources" + }, + "ProductCode": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 255, + "description": "ProductCode could be used for correlation of packages across sources" + }, + "Capabilities": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "maxItems": 1000, + "uniqueItems": true, + "description": "List of appx or msix installer capabilities" + }, + "RestrictedCapabilities": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "maxItems": 1000, + "uniqueItems": true, + "description": "List of appx or msix installer restricted capabilities" + }, + "Installer": { + "type": "object", + "properties": { + "InstallerLocale": { + "$ref": "#/definitions/Locale" + }, + "Platform": { + "$ref": "#/definitions/Platform" + }, + "MinimumOSVersion": { + "$ref": "#/definitions/MinimumOSVersion" + }, + "Architecture": { + "type": "string", + "enum": [ + "x86", + "x64", + "arm", + "arm64", + "neutral" + ], + "description": "The installer target architecture" + }, + "InstallerType": { + "$ref": "#/definitions/InstallerType" + }, + "Scope": { + "$ref": "#/definitions/Scope" + }, + "InstallerUrl": { + "type": "string", + "format": "uri", + "pattern": "^([Hh][Tt][Tt][Pp][Ss]?)://", + "description": "The installer Url" + }, + "InstallerSha256": { + "type": "string", + "pattern": "^[A-Fa-f0-9]{64}$", + "description": "Sha256 is required. Sha256 of the installer" + }, + "SignatureSha256": { + "type": [ "string", "null" ], + "pattern": "^[A-Fa-f0-9]{64}$", + "description": "SignatureSha256 is recommended for appx or msix. It is the sha256 of signature file inside appx or msix. Could be used during streaming install if applicable" + }, + "InstallModes": { + "$ref": "#/definitions/InstallModes" + }, + "InstallerSwitches": { + "$ref": "#/definitions/InstallerSwitches" + }, + "InstallerSuccessCodes": { + "$ref": "#/definitions/InstallerSuccessCodes" + }, + "UpgradeBehavior": { + "$ref": "#/definitions/UpgradeBehavior" + }, + "Commands": { + "$ref": "#/definitions/Commands" + }, + "Protocols": { + "$ref": "#/definitions/Protocols" + }, + "FileExtensions": { + "$ref": "#/definitions/FileExtensions" + }, + "Dependencies": { + "$ref": "#/definitions/Dependencies" + }, + "PackageFamilyName": { + "$ref": "#/definitions/PackageFamilyName" + }, + "ProductCode": { + "$ref": "#/definitions/ProductCode" + }, + "Capabilities": { + "$ref": "#/definitions/Capabilities" + }, + "RestrictedCapabilities": { + "$ref": "#/definitions/RestrictedCapabilities" + } + }, + "required": [ + "Architecture", + "InstallerUrl", + "InstallerSha256" + ] + } + }, + "type": "object", + "properties": { + "PackageIdentifier": { + "$ref": "#/definitions/PackageIdentifier" + }, + "PackageVersion": { + "$ref": "#/definitions/PackageVersion" + }, + "PackageLocale": { + "$ref": "#/definitions/Locale", + "description": "The package meta-data locale" + }, + "Publisher": { + "type": "string", + "minLength": 2, + "maxLength": 64, + "description": "The publisher name" + }, + "PublisherUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher home page" + }, + "PublisherSupportUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher support page" + }, + "PrivacyUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher privacy page or the package privacy page" + }, + "Author": { + "type": [ "string", "null" ], + "minLength": 2, + "maxLength": 256, + "description": "The package author" + }, + "PackageName": { + "type": "string", + "minLength": 2, + "maxLength": 64, + "description": "The package name" + }, + "PackageUrl": { + "$ref": "#/definitions/Url", + "description": "The package home page" + }, + "License": { + "type": "string", + "minLength": 3, + "maxLength": 512, + "description": "The package license" + }, + "LicenseUrl": { + "$ref": "#/definitions/Url", + "description": "The license page" + }, + "Copyright": { + "type": [ "string", "null" ], + "minLength": 3, + "maxLength": 512, + "description": "The package copyright" + }, + "CopyrightUrl": { + "$ref": "#/definitions/Url", + "description": "The package copyright page" + }, + "ShortDescription": { + "type": "string", + "minLength": 3, + "maxLength": 256, + "description": "The short package description" + }, + "Description": { + "type": [ "string", "null" ], + "minLength": 3, + "maxLength": 10000, + "description": "The full package description" + }, + "Moniker": { + "$ref": "#/definitions/Tag", + "description": "The most common package term" + }, + "Tags": { + "type": [ "array", "null" ], + "items": { + "$ref": "#/definitions/Tag" + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of additional package search terms" + }, + "Channel": { + "$ref": "#/definitions/Channel" + }, + "InstallerLocale": { + "$ref": "#/definitions/Locale" + }, + "Platform": { + "$ref": "#/definitions/Platform" + }, + "MinimumOSVersion": { + "$ref": "#/definitions/MinimumOSVersion" + }, + "InstallerType": { + "$ref": "#/definitions/InstallerType" + }, + "Scope": { + "$ref": "#/definitions/Scope" + }, + "InstallModes": { + "$ref": "#/definitions/InstallModes" + }, + "InstallerSwitches": { + "$ref": "#/definitions/InstallerSwitches" + }, + "InstallerSuccessCodes": { + "$ref": "#/definitions/InstallerSuccessCodes" + }, + "UpgradeBehavior": { + "$ref": "#/definitions/UpgradeBehavior" + }, + "Commands": { + "$ref": "#/definitions/Commands" + }, + "Protocols": { + "$ref": "#/definitions/Protocols" + }, + "FileExtensions": { + "$ref": "#/definitions/FileExtensions" + }, + "Dependencies": { + "$ref": "#/definitions/Dependencies" + }, + "PackageFamilyName": { + "$ref": "#/definitions/PackageFamilyName" + }, + "ProductCode": { + "$ref": "#/definitions/ProductCode" + }, + "Capabilities": { + "$ref": "#/definitions/Capabilities" + }, + "RestrictedCapabilities": { + "$ref": "#/definitions/RestrictedCapabilities" + }, + "Installers": { + "type": "array", + "items": { + "$ref": "#/definitions/Installer" + }, + "minItems": 1, + "maxItems": 1 + }, + "ManifestType": { + "type": "string", + "default": "singleton", + "const": "singleton", + "description": "The manifest type" + }, + "ManifestVersion": { + "type": "string", + "default": "1.0.0", + "pattern": "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){2}$", + "description": "The manifest syntax version" + } + }, + "required": [ + "PackageIdentifier", + "PackageVersion", + "Publisher", + "PackageName", + "License", + "ShortDescription", + "Installers", + "ManifestType", + "ManifestVersion" + ] +}+ \ No newline at end of file diff --git a/schemas/JSON/manifests/v1.0.0/manifest.version.1.0.0.json b/schemas/JSON/manifests/v1.0.0/manifest.version.1.0.0.json @@ -0,0 +1,45 @@ +{ + "$id": "https://aka.ms/winget-manifest.version.1.0.0.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "A representation of a multi-file manifest representing an app version in the OWC. v1.0.0", + "type": "object", + "properties": { + "PackageIdentifier": { + "type": "string", + "pattern": "^[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}(\\.[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}){1,3}$", + "maxLength": 128, + "description": "The package unique identifier" + }, + "PackageVersion": { + "type": "string", + "pattern": "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$", + "maxLength": 128, + "description": "The package version" + }, + "DefaultLocale": { + "type": "string", + "pattern": "^([a-zA-Z]{2}|[iI]-[a-zA-Z]+|[xX]-[a-zA-Z]{1,8})(-[a-zA-Z]{1,8})*$", + "maxLength": 20, + "description": "The default package meta-data locale" + }, + "ManifestType": { + "type": "string", + "default": "version", + "const": "version", + "description": "The manifest type" + }, + "ManifestVersion": { + "type": "string", + "default": "1.0.0", + "pattern": "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){2}$", + "description": "The manifest syntax version" + } + }, + "required": [ + "PackageIdentifier", + "PackageVersion", + "DefaultLocale", + "ManifestType", + "ManifestVersion" + ] +}+ \ No newline at end of file diff --git a/src/AppInstallerCLI.sln b/src/AppInstallerCLI.sln @@ -64,13 +64,25 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WinGetYamlFuzzing", "WinGet EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalhostWebServer", "LocalhostWebServer\LocalhostWebServer.csproj", "{3BAF989F-7F65-465B-ACE8-BAFE42D1017E}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Valijson", "Valijson\Valijson.vcxitems", "{358BC478-0624-4AD1-A933-0422B5292AF8}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ManifestSchema", "ManifestSchema\ManifestSchema.vcxitems", "{7D05F64D-CE5A-42AA-A2C1-E91458F061CF}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution + ManifestSchema\ManifestSchema.vcxitems*{1622da16-914f-4f57-a259-d5169003cc8c}*SharedItemsImports = 4 + Valijson\Valijson.vcxitems*{358bc478-0624-4ad1-a933-0422b5292af8}*SharedItemsImports = 9 catch2\catch2.vcxitems*{5295e21e-9868-4de2-a177-fbb97b36579b}*SharedItemsImports = 9 + ManifestSchema\ManifestSchema.vcxitems*{5890d6ed-7c3b-40f3-b436-b54f640d9e65}*SharedItemsImports = 4 + Valijson\Valijson.vcxitems*{5890d6ed-7c3b-40f3-b436-b54f640d9e65}*SharedItemsImports = 4 binver\binver.vcxitems*{5b6f90df-fd19-4bae-83d9-24dad128e777}*SharedItemsImports = 4 + ManifestSchema\ManifestSchema.vcxitems*{5b6f90df-fd19-4bae-83d9-24dad128e777}*SharedItemsImports = 4 binver\binver.vcxitems*{6e36ddd7-1602-474e-b1d7-d0a7e1d5ad86}*SharedItemsImports = 9 + ManifestSchema\ManifestSchema.vcxitems*{7d05f64d-ce5a-42aa-a2c1-e91458f061cf}*SharedItemsImports = 9 catch2\catch2.vcxitems*{89b1aab4-2bbc-4b65-9ed7-a01d5cf88230}*SharedItemsImports = 4 + ManifestSchema\ManifestSchema.vcxitems*{89b1aab4-2bbc-4b65-9ed7-a01d5cf88230}*SharedItemsImports = 4 binver\binver.vcxitems*{fb313532-38b0-4676-9303-ab200aa13576}*SharedItemsImports = 4 + ManifestSchema\ManifestSchema.vcxitems*{fb313532-38b0-4676-9303-ab200aa13576}*SharedItemsImports = 4 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM = Debug|ARM @@ -415,6 +427,8 @@ Global {82B39FDA-E86B-4713-A873-9D56DE00247A} = {60618CAC-2995-4DF9-9914-45C6FC02C995} {1622DA16-914F-4F57-A259-D5169003CC8C} = {6D7776A8-42FE-46DD-B0F8-712F35EA0C79} {3BAF989F-7F65-465B-ACE8-BAFE42D1017E} = {EA8CD934-0702-4911-A2C5-A40600E616DE} + {358BC478-0624-4AD1-A933-0422B5292AF8} = {60618CAC-2995-4DF9-9914-45C6FC02C995} + {7D05F64D-CE5A-42AA-A2C1-E91458F061CF} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B6FDB70C-A751-422C-ACD1-E35419495857} diff --git a/src/AppInstallerCLI/AppInstallerCLI.vcxproj b/src/AppInstallerCLI/AppInstallerCLI.vcxproj @@ -70,6 +70,7 @@ </ImportGroup> <ImportGroup Label="Shared"> <Import Project="..\binver\binver.vcxitems" Label="Shared" /> + <Import Project="..\ManifestSchema\ManifestSchema.vcxitems" Label="Shared" /> </ImportGroup> <ImportGroup Label="PropertySheets"> <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> diff --git a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp @@ -155,7 +155,6 @@ namespace AppInstaller::CLI EnsureOneMatchFromSearchResult(true) << GetInstalledPackageVersion << EnsureUpdateVersionApplicable << - EnsureMinOSVersion << SelectInstaller << EnsureApplicableInstaller << ShowInstallationDisclaimer << @@ -181,7 +180,6 @@ namespace AppInstaller::CLI context << GetManifestFromPackage << EnsureUpdateVersionApplicable << - EnsureMinOSVersion << SelectInstaller << EnsureApplicableInstaller; } diff --git a/src/AppInstallerCLICore/Commands/ValidateCommand.cpp b/src/AppInstallerCLICore/Commands/ValidateCommand.cpp @@ -34,7 +34,7 @@ namespace AppInstaller::CLI void ValidateCommand::ExecuteInternal(Execution::Context& context) const { context << - Workflow::VerifyFile(Execution::Args::Type::ValidateManifest) << + Workflow::VerifyPath(Execution::Args::Type::ValidateManifest) << [](Execution::Context& context) { auto inputFile = Utility::ConvertToUTF16(context.Args.GetArg(Execution::Args::Type::ValidateManifest)); diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -243,6 +243,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(VerifyFileFailedIsDirectory); WINGET_DEFINE_RESOURCE_STRINGID(VerifyFileFailedNotExist); WINGET_DEFINE_RESOURCE_STRINGID(VerifyFileSignedMsix); + WINGET_DEFINE_RESOURCE_STRINGID(VerifyPathFailedNotExist); WINGET_DEFINE_RESOURCE_STRINGID(VersionArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(VersionsArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(WordArgumentDescription); diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -18,18 +18,6 @@ namespace AppInstaller::CLI::Workflow using namespace AppInstaller::Manifest; using namespace AppInstaller::Repository; - void EnsureMinOSVersion(Execution::Context& context) - { - const auto& manifest = context.Get<Execution::Data::Manifest>(); - - if (!manifest.MinOSVersion.empty() && - !Runtime::IsCurrentOSVersionGreaterThanOrEqual(Version(manifest.MinOSVersion))) - { - context.Reporter.Error() << Resource::String::InstallationRequiresHigherWindows << ' ' << manifest.MinOSVersion << std::endl; - AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_OLD_WIN_VERSION)); - } - } - void EnsureApplicableInstaller(Execution::Context& context) { const auto& installer = context.Get<Execution::Data::Installer>(); @@ -45,7 +33,7 @@ namespace AppInstaller::CLI::Workflow { auto installerType = context.Get<Execution::Data::Installer>().value().InstallerType; - if (installerType == ManifestInstaller::InstallerTypeEnum::MSStore) + if (installerType == InstallerTypeEnum::MSStore) { context.Reporter.Info() << Resource::String::InstallationDisclaimerMSStore << std::endl; } @@ -63,15 +51,15 @@ namespace AppInstaller::CLI::Workflow switch (installer.InstallerType) { - case ManifestInstaller::InstallerTypeEnum::Exe: - case ManifestInstaller::InstallerTypeEnum::Burn: - case ManifestInstaller::InstallerTypeEnum::Inno: - case ManifestInstaller::InstallerTypeEnum::Msi: - case ManifestInstaller::InstallerTypeEnum::Nullsoft: - case ManifestInstaller::InstallerTypeEnum::Wix: + case InstallerTypeEnum::Exe: + case InstallerTypeEnum::Burn: + case InstallerTypeEnum::Inno: + case InstallerTypeEnum::Msi: + case InstallerTypeEnum::Nullsoft: + case InstallerTypeEnum::Wix: context << DownloadInstallerFile << VerifyInstallerHash << UpdateInstallerFileMotwIfApplicable; break; - case ManifestInstaller::InstallerTypeEnum::Msix: + case InstallerTypeEnum::Msix: if (installer.SignatureSha256.empty()) { context << DownloadInstallerFile << VerifyInstallerHash << UpdateInstallerFileMotwIfApplicable; @@ -82,7 +70,7 @@ namespace AppInstaller::CLI::Workflow context << GetMsixSignatureHash << VerifyInstallerHash << UpdateInstallerFileMotwIfApplicable; } break; - case ManifestInstaller::InstallerTypeEnum::MSStore: + case InstallerTypeEnum::MSStore: // Nothing to do here break; default: @@ -273,13 +261,13 @@ namespace AppInstaller::CLI::Workflow switch (installer.InstallerType) { - case ManifestInstaller::InstallerTypeEnum::Exe: - case ManifestInstaller::InstallerTypeEnum::Burn: - case ManifestInstaller::InstallerTypeEnum::Inno: - case ManifestInstaller::InstallerTypeEnum::Msi: - case ManifestInstaller::InstallerTypeEnum::Nullsoft: - case ManifestInstaller::InstallerTypeEnum::Wix: - if (isUpdate && installer.UpdateBehavior == ManifestInstaller::UpdateBehaviorEnum::UninstallPrevious) + case InstallerTypeEnum::Exe: + case InstallerTypeEnum::Burn: + case InstallerTypeEnum::Inno: + case InstallerTypeEnum::Msi: + case InstallerTypeEnum::Nullsoft: + case InstallerTypeEnum::Wix: + if (isUpdate && installer.UpdateBehavior == UpdateBehaviorEnum::UninstallPrevious) { context << GetUninstallInfo << @@ -288,10 +276,10 @@ namespace AppInstaller::CLI::Workflow } context << ShellExecuteInstall; break; - case ManifestInstaller::InstallerTypeEnum::Msix: + case InstallerTypeEnum::Msix: context << MsixInstall; break; - case ManifestInstaller::InstallerTypeEnum::MSStore: + case InstallerTypeEnum::MSStore: context << EnsureFeatureEnabled(Settings::ExperimentalFeature::Feature::ExperimentalMSStore) << EnsureStorePolicySatisfied << @@ -372,7 +360,6 @@ namespace AppInstaller::CLI::Workflow void InstallPackageVersion(Execution::Context& context) { context << - Workflow::EnsureMinOSVersion << Workflow::SelectInstaller << Workflow::EnsureApplicableInstaller << Workflow::ShowInstallationDisclaimer << diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.h b/src/AppInstallerCLICore/Workflows/InstallFlow.h @@ -11,12 +11,6 @@ namespace AppInstaller::CLI::Workflow static constexpr std::string_view ARG_TOKEN_LOGPATH = "<LOGPATH>"sv; static constexpr std::string_view ARG_TOKEN_INSTALLPATH = "<INSTALLPATH>"sv; - // Ensures that the current OS version is greater than or equal to the one in the manifest. - // Required Args: None - // Inputs: Manifest - // Outputs: None - void EnsureMinOSVersion(Execution::Context& context); - // Ensures that there is an applicable installer. // Required Args: None // Inputs: Installer diff --git a/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp b/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp @@ -12,15 +12,23 @@ namespace AppInstaller::CLI::Workflow namespace { // Determine if the installer is applicable. - bool IsInstallerApplicable(const Manifest::ManifestInstaller& installer, Manifest::ManifestInstaller::InstallerTypeEnum installedType) + // TODO: Implement a mechanism for better error messaging for no applicable installer scenario + bool IsInstallerApplicable(const Manifest::ManifestInstaller& installer, Manifest::InstallerTypeEnum installedType) { + // Check MinOSVersion + if (!installer.MinOSVersion.empty() && + !Runtime::IsCurrentOSVersionGreaterThanOrEqual(Utility::Version(installer.MinOSVersion))) + { + return false; + } + if (Utility::IsApplicableArchitecture(installer.Arch) == Utility::InapplicableArchitecture) { return false; } - if (installedType != Manifest::ManifestInstaller::InstallerTypeEnum::Unknown && - !Manifest::ManifestInstaller::IsInstallerTypeCompatible(installer.InstallerType, installedType)) + if (installedType != Manifest::InstallerTypeEnum::Unknown && + !Manifest::IsInstallerTypeCompatible(installer.InstallerType, installedType)) { return false; } @@ -33,33 +41,21 @@ namespace AppInstaller::CLI::Workflow bool IsInstallerBetterMatch( const Manifest::ManifestInstaller& installer1, const Manifest::ManifestInstaller& installer2, - Manifest::ManifestInstaller::InstallerTypeEnum installedType) + Manifest::InstallerTypeEnum installedType) { - auto arch1 = Utility::IsApplicableArchitecture(installer1.Arch); - auto arch2 = Utility::IsApplicableArchitecture(installer2.Arch); - - // Applicable architecture should always come before inapplicable architecture - if (arch1 != Utility::InapplicableArchitecture && - arch2 == Utility::InapplicableArchitecture) - { - return true; - } - // If there's installation metadata, pick the preferred one or compatible one - if (installedType != Manifest::ManifestInstaller::InstallerTypeEnum::Unknown) + if (installedType != Manifest::InstallerTypeEnum::Unknown) { if (installer1.InstallerType == installedType && installer2.InstallerType != installedType) { return true; } - if (Manifest::ManifestInstaller::IsInstallerTypeCompatible(installer1.InstallerType, installedType) && - !Manifest::ManifestInstaller::IsInstallerTypeCompatible(installer2.InstallerType, installedType)) - { - return true; - } } // Todo: Compare only architecture for now. Need more work and spec. + auto arch1 = Utility::IsApplicableArchitecture(installer1.Arch); + auto arch2 = Utility::IsApplicableArchitecture(installer2.Arch); + if (arch1 > arch2) { return true; @@ -67,28 +63,6 @@ namespace AppInstaller::CLI::Workflow return false; } - - // This is used in sorting the list of available localizations to get the best match. - struct LocalizationComparator - { - bool operator() ( - const Manifest::ManifestLocalization& loc1, - const Manifest::ManifestLocalization& loc2) - { - // Todo: Compare simple language for now. Need more work and spec. - std::string userPreferredLocale = std::locale("").name(); - - auto foundLoc1 = userPreferredLocale.find(loc1.Language); - auto foundLoc2 = userPreferredLocale.find(loc2.Language); - - if (foundLoc1 != std::string::npos && foundLoc2 == std::string::npos) - { - return true; - } - - return false; - } - }; } std::optional<Manifest::ManifestInstaller> ManifestComparator::GetPreferredInstaller(const Manifest::Manifest& manifest) @@ -96,11 +70,11 @@ namespace AppInstaller::CLI::Workflow AICLI_LOG(CLI, Info, << "Starting installer selection."); // Get the currently installed package's type (if present) - Manifest::ManifestInstaller::InstallerTypeEnum installedType = Manifest::ManifestInstaller::InstallerTypeEnum::Unknown; + Manifest::InstallerTypeEnum installedType = Manifest::InstallerTypeEnum::Unknown; auto installerTypeItr = m_installationMetadata.find(Repository::PackageVersionMetadata::InstalledType); if (installerTypeItr != m_installationMetadata.end()) { - installedType = Manifest::ManifestInstaller::ConvertToInstallerTypeEnum(installerTypeItr->second); + installedType = Manifest::ConvertToInstallerTypeEnum(installerTypeItr->second); } const Manifest::ManifestInstaller* result = nullptr; @@ -114,7 +88,7 @@ namespace AppInstaller::CLI::Workflow result = &installer; } } - else if (IsInstallerBetterMatch(installer, *result, installedType)) + else if (IsInstallerApplicable(installer, installedType) && IsInstallerBetterMatch(installer, *result, installedType)) { result = &installer; } @@ -128,39 +102,10 @@ namespace AppInstaller::CLI::Workflow Logging::Telemetry().LogSelectedInstaller( static_cast<int>(result->Arch), result->Url, - Manifest::ManifestInstaller::InstallerTypeToString(result->InstallerType), - Manifest::ManifestInstaller::ScopeToString(result->Scope), - result->Language); + Manifest::InstallerTypeToString(result->InstallerType), + Manifest::ScopeToString(result->Scope), + result->Locale); return *result; } - - Manifest::ManifestLocalization ManifestComparator::GetPreferredLocalization(const Manifest::Manifest& manifest) - { - AICLI_LOG(CLI, Info, << "Starting localization selection."); - - ManifestLocalization selectedLocalization; - - // Sorting the list of available localizations according to rules defined in LocalizationComparator. - if (!manifest.Localization.empty()) - { - auto localization = manifest.Localization; - std::sort(localization.begin(), localization.end(), LocalizationComparator()); - - // TODO: needs to check language applicability here - - selectedLocalization = localization[0]; - } - else - { - // Populate default from package manifest - selectedLocalization.Description = manifest.Description; - selectedLocalization.Homepage = manifest.Homepage; - selectedLocalization.LicenseUrl = manifest.LicenseUrl; - } - - AICLI_LOG(CLI, Info, << "Completed localization selection. Selected localization language: " << selectedLocalization.Language); - - return selectedLocalization; - } } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/ManifestComparator.h b/src/AppInstallerCLICore/Workflows/ManifestComparator.h @@ -15,7 +15,6 @@ namespace AppInstaller::CLI::Workflow m_installationMetadata(std::move(installationMetadata)) {} std::optional<Manifest::ManifestInstaller> GetPreferredInstaller(const Manifest::Manifest& manifest); - Manifest::ManifestLocalization GetPreferredLocalization(const Manifest::Manifest& manifest); private: // TODO: Handle args to change how we select. diff --git a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp @@ -69,19 +69,19 @@ namespace AppInstaller::CLI::Workflow // Construct install experience arg. // SilentWithProgress is default, so look for it first. - auto experienceArgsItr = installerSwitches.find(ManifestInstaller::InstallerSwitchType::SilentWithProgress); + auto experienceArgsItr = installerSwitches.find(InstallerSwitchType::SilentWithProgress); if (context.Args.Contains(Execution::Args::Type::Interactive)) { // If interactive requested, always use Interactive (or nothing). If the installer supports // interactive it is usually the default, and thus it is cumbersome to put a blank entry in // the manifest. - experienceArgsItr = installerSwitches.find(ManifestInstaller::InstallerSwitchType::Interactive); + experienceArgsItr = installerSwitches.find(InstallerSwitchType::Interactive); } // If no SilentWithProgress exists, or Silent requested, try to find Silent. else if (experienceArgsItr == installerSwitches.end() || context.Args.Contains(Execution::Args::Type::Silent)) { - auto silentItr = installerSwitches.find(ManifestInstaller::InstallerSwitchType::Silent); + auto silentItr = installerSwitches.find(InstallerSwitchType::Silent); // If Silent requested, but doesn't exist, then continue using SilentWithProgress. if (silentItr != installerSwitches.end()) { @@ -95,35 +95,35 @@ namespace AppInstaller::CLI::Workflow } // Construct language arg if necessary. - if (context.Args.Contains(Execution::Args::Type::Language) && installerSwitches.find(ManifestInstaller::InstallerSwitchType::Language) != installerSwitches.end()) + if (context.Args.Contains(Execution::Args::Type::Language) && installerSwitches.find(InstallerSwitchType::Language) != installerSwitches.end()) { - installerArgs += ' ' + installerSwitches.at(ManifestInstaller::InstallerSwitchType::Language); + installerArgs += ' ' + installerSwitches.at(InstallerSwitchType::Language); } // Construct log path arg. - if (installerSwitches.find(ManifestInstaller::InstallerSwitchType::Log) != installerSwitches.end()) + if (installerSwitches.find(InstallerSwitchType::Log) != installerSwitches.end()) { - installerArgs += ' ' + installerSwitches.at(ManifestInstaller::InstallerSwitchType::Log); + installerArgs += ' ' + installerSwitches.at(InstallerSwitchType::Log); } // Construct custom arg. - if (installerSwitches.find(ManifestInstaller::InstallerSwitchType::Custom) != installerSwitches.end()) + if (installerSwitches.find(InstallerSwitchType::Custom) != installerSwitches.end()) { - installerArgs += ' ' + installerSwitches.at(ManifestInstaller::InstallerSwitchType::Custom); + installerArgs += ' ' + installerSwitches.at(InstallerSwitchType::Custom); } // Construct update arg if applicable - if (isUpdate && installerSwitches.find(ManifestInstaller::InstallerSwitchType::Update) != installerSwitches.end()) + if (isUpdate && installerSwitches.find(InstallerSwitchType::Update) != installerSwitches.end()) { - installerArgs += ' ' + installerSwitches.at(ManifestInstaller::InstallerSwitchType::Update); + installerArgs += ' ' + installerSwitches.at(InstallerSwitchType::Update); } // Construct install location arg if necessary. if (!isUpdate && context.Args.Contains(Execution::Args::Type::InstallLocation) && - installerSwitches.find(ManifestInstaller::InstallerSwitchType::InstallLocation) != installerSwitches.end()) + installerSwitches.find(InstallerSwitchType::InstallLocation) != installerSwitches.end()) { - installerArgs += ' ' + installerSwitches.at(ManifestInstaller::InstallerSwitchType::InstallLocation); + installerArgs += ' ' + installerSwitches.at(InstallerSwitchType::InstallLocation); } return installerArgs; @@ -249,14 +249,14 @@ namespace AppInstaller::CLI::Workflow switch(context.Get<Execution::Data::Installer>()->InstallerType) { - case ManifestInstaller::InstallerTypeEnum::Burn: - case ManifestInstaller::InstallerTypeEnum::Exe: - case ManifestInstaller::InstallerTypeEnum::Inno: - case ManifestInstaller::InstallerTypeEnum::Nullsoft: + case InstallerTypeEnum::Burn: + case InstallerTypeEnum::Exe: + case InstallerTypeEnum::Inno: + case InstallerTypeEnum::Nullsoft: renamedDownloadedInstaller += L".exe"; break; - case ManifestInstaller::InstallerTypeEnum::Msi: - case ManifestInstaller::InstallerTypeEnum::Wix: + case InstallerTypeEnum::Msi: + case InstallerTypeEnum::Wix: renamedDownloadedInstaller += L".msi"; break; } diff --git a/src/AppInstallerCLICore/Workflows/ShowFlow.cpp b/src/AppInstallerCLICore/Workflows/ShowFlow.cpp @@ -15,44 +15,47 @@ namespace AppInstaller::CLI::Workflow const auto& manifest = context.Get<Execution::Data::Manifest>(); const auto& installer = context.Get<Execution::Data::Installer>(); - ManifestComparator manifestComparator(context.Args); - auto selectedLocalization = manifestComparator.GetPreferredLocalization(manifest); - // TODO: Come up with a prettier format context.Reporter.Info() << "Version: " << manifest.Version << std::endl; - context.Reporter.Info() << "Publisher: " << manifest.Publisher << std::endl; - if (!manifest.Author.empty()) + context.Reporter.Info() << "Publisher: " << manifest.CurrentLocalization.Get<Manifest::Localization::Publisher>() << std::endl; + auto author = manifest.CurrentLocalization.Get<Manifest::Localization::Author>(); + if (!author.empty()) { - context.Reporter.Info() << "Author: " << manifest.Author << std::endl; + context.Reporter.Info() << "Author: " << author << std::endl; } - if (!manifest.AppMoniker.empty()) + if (!manifest.Moniker.empty()) { - context.Reporter.Info() << "AppMoniker: " << manifest.AppMoniker << std::endl; + context.Reporter.Info() << "Moniker: " << manifest.Moniker << std::endl; } - if (!selectedLocalization.Description.empty()) + auto description = manifest.CurrentLocalization.Get<Manifest::Localization::Description>(); + if (description.empty()) { - context.Reporter.Info() << "Description: " << selectedLocalization.Description << std::endl; + // Fall back to short description + description = manifest.CurrentLocalization.Get<Manifest::Localization::ShortDescription>(); } - if (!selectedLocalization.Homepage.empty()) + if (!description.empty()) { - context.Reporter.Info() << "Homepage: " << selectedLocalization.Homepage << std::endl; + context.Reporter.Info() << "Description: " << description << std::endl; } - if (!manifest.License.empty()) + auto homepage = manifest.CurrentLocalization.Get<Manifest::Localization::PackageUrl>(); + if (!homepage.empty()) { - context.Reporter.Info() << "License: " << manifest.License << std::endl; + context.Reporter.Info() << "Homepage: " << homepage << std::endl; } - if (!selectedLocalization.LicenseUrl.empty()) + context.Reporter.Info() << "License: " << manifest.CurrentLocalization.Get<Manifest::Localization::License>() << std::endl; + auto licenseUrl = manifest.CurrentLocalization.Get<Manifest::Localization::LicenseUrl>(); + if (!licenseUrl.empty()) { - context.Reporter.Info() << "License Url: " << selectedLocalization.LicenseUrl << std::endl; + context.Reporter.Info() << "License Url: " << licenseUrl << std::endl; } context.Reporter.Info() << "Installer:" << std::endl; if (installer) { - context.Reporter.Info() << " Type: " << Manifest::ManifestInstaller::InstallerTypeToString(installer->InstallerType) << std::endl; - if (!installer->Language.empty()) + context.Reporter.Info() << " Type: " << Manifest::InstallerTypeToString(installer->InstallerType) << std::endl; + if (!installer->Locale.empty()) { - context.Reporter.Info() << " Language: " << installer->Language << std::endl; + context.Reporter.Info() << " Locale: " << installer->Locale << std::endl; } if (!installer->Url.empty()) { @@ -76,7 +79,6 @@ namespace AppInstaller::CLI::Workflow void ShowManifestVersion(Execution::Context& context) { const auto& manifest = context.Get<Execution::Data::Manifest>(); - Execution::TableOutput<2> table(context.Reporter, { Resource::String::ShowVersion, Resource::String::ShowChannel }); table.OutputLine({ manifest.Version, manifest.Channel }); table.Complete(); diff --git a/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp b/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp @@ -17,12 +17,12 @@ namespace AppInstaller::CLI::Workflow { auto installedPackageVersion = context.Get<Execution::Data::InstalledPackageVersion>(); const std::string installedTypeString = installedPackageVersion->GetMetadata()[PackageVersionMetadata::InstalledType]; - switch (ManifestInstaller::ConvertToInstallerTypeEnum(installedTypeString)) + switch (ConvertToInstallerTypeEnum(installedTypeString)) { - case ManifestInstaller::InstallerTypeEnum::Exe: - case ManifestInstaller::InstallerTypeEnum::Burn: - case ManifestInstaller::InstallerTypeEnum::Inno: - case ManifestInstaller::InstallerTypeEnum::Nullsoft: + case InstallerTypeEnum::Exe: + case InstallerTypeEnum::Burn: + case InstallerTypeEnum::Inno: + case InstallerTypeEnum::Nullsoft: { IPackageVersion::Metadata packageMetadata = installedPackageVersion->GetMetadata(); @@ -46,8 +46,8 @@ namespace AppInstaller::CLI::Workflow context.Add<Execution::Data::UninstallString>(uninstallCommandItr->second); break; } - case ManifestInstaller::InstallerTypeEnum::Msi: - case ManifestInstaller::InstallerTypeEnum::Wix: + case InstallerTypeEnum::Msi: + case InstallerTypeEnum::Wix: { // Uninstall strings for MSI don't include UI level (/q) needed to avoid interactivity, // so we handle them differently. @@ -61,8 +61,8 @@ namespace AppInstaller::CLI::Workflow context.Add<Execution::Data::ProductCodes>(std::move(productCodes)); break; } - case ManifestInstaller::InstallerTypeEnum::Msix: - case ManifestInstaller::InstallerTypeEnum::MSStore: + case InstallerTypeEnum::Msix: + case InstallerTypeEnum::MSStore: { auto packageFamilyNames = installedPackageVersion->GetMultiProperty(PackageVersionMultiProperty::PackageFamilyName); if (packageFamilyNames.empty()) @@ -82,20 +82,20 @@ namespace AppInstaller::CLI::Workflow void ExecuteUninstaller(Execution::Context& context) { const std::string installedTypeString = context.Get<Execution::Data::InstalledPackageVersion>()->GetMetadata()[PackageVersionMetadata::InstalledType]; - switch (ManifestInstaller::ConvertToInstallerTypeEnum(installedTypeString)) + switch (ConvertToInstallerTypeEnum(installedTypeString)) { - case ManifestInstaller::InstallerTypeEnum::Exe: - case ManifestInstaller::InstallerTypeEnum::Burn: - case ManifestInstaller::InstallerTypeEnum::Inno: - case ManifestInstaller::InstallerTypeEnum::Nullsoft: + case InstallerTypeEnum::Exe: + case InstallerTypeEnum::Burn: + case InstallerTypeEnum::Inno: + case InstallerTypeEnum::Nullsoft: context << Workflow::ShellExecuteUninstallImpl; break; - case ManifestInstaller::InstallerTypeEnum::Msi: - case ManifestInstaller::InstallerTypeEnum::Wix: + case InstallerTypeEnum::Msi: + case InstallerTypeEnum::Wix: context << Workflow::ShellExecuteMsiExecUninstall; break; - case ManifestInstaller::InstallerTypeEnum::Msix: - case ManifestInstaller::InstallerTypeEnum::MSStore: + case InstallerTypeEnum::Msix: + case InstallerTypeEnum::MSStore: context << Workflow::MsixUninstall; break; default: diff --git a/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp b/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp @@ -37,13 +37,6 @@ namespace AppInstaller::CLI::Workflow auto packageVersion = package->GetAvailableVersion(key); auto manifest = packageVersion->GetManifest(); - // Check MinOSVersion - if (!manifest.MinOSVersion.empty() && - !Runtime::IsCurrentOSVersionGreaterThanOrEqual(Utility::Version(manifest.MinOSVersion))) - { - continue; - } - // Check applicable Installer auto installer = manifestComparator.GetPreferredInstaller(manifest); if (!installer.has_value()) @@ -52,6 +45,7 @@ namespace AppInstaller::CLI::Workflow } // Since we already did installer selection, just populate the context Data + manifest.ApplyLocale(); context.Add<Execution::Data::Manifest>(std::move(manifest)); context.Add<Execution::Data::PackageVersion>(std::move(packageVersion)); context.Add<Execution::Data::Installer>(std::move(installer)); diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -527,7 +527,8 @@ namespace AppInstaller::CLI::Workflow AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_MANIFEST_FOUND); } - Logging::Telemetry().LogManifestFields(manifest->Id, manifest->Name, manifest->Version); + Logging::Telemetry().LogManifestFields(manifest->Id, manifest->DefaultLocalization.Get<Manifest::Localization::PackageName>(), manifest->Version); + manifest->ApplyLocale(); context.Add<Execution::Data::Manifest>(std::move(manifest.value())); context.Add<Execution::Data::PackageVersion>(std::move(requestedVersion)); } @@ -554,16 +555,28 @@ namespace AppInstaller::CLI::Workflow } } + void VerifyPath::operator()(Execution::Context& context) const + { + std::filesystem::path path = Utility::ConvertToUTF16(context.Args.GetArg(m_arg)); + + if (!std::filesystem::exists(path)) + { + context.Reporter.Error() << Resource::String::VerifyPathFailedNotExist << ' ' << path.u8string() << std::endl; + AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND)); + } + } + void GetManifestFromArg(Execution::Context& context) { Logging::Telemetry().LogIsManifestLocal(true); context << - VerifyFile(Execution::Args::Type::Manifest) << + VerifyPath(Execution::Args::Type::Manifest) << [](Execution::Context& context) { Manifest::Manifest manifest = Manifest::YamlParser::CreateFromPath(Utility::ConvertToUTF16(context.Args.GetArg(Execution::Args::Type::Manifest))); - Logging::Telemetry().LogManifestFields(manifest.Id, manifest.Name, manifest.Version); + Logging::Telemetry().LogManifestFields(manifest.Id, manifest.DefaultLocalization.Get<Manifest::Localization::PackageName>(), manifest.Version); + manifest.ApplyLocale(); context.Add<Execution::Data::Manifest>(std::move(manifest)); }; } @@ -577,7 +590,7 @@ namespace AppInstaller::CLI::Workflow void ReportManifestIdentity(Execution::Context& context) { const auto& manifest = context.Get<Execution::Data::Manifest>(); - ReportIdentity(context, manifest.Name, manifest.Id); + ReportIdentity(context, manifest.CurrentLocalization.Get<Manifest::Localization::PackageName>(), manifest.Id); } void GetManifest(Execution::Context& context) @@ -667,7 +680,7 @@ namespace AppInstaller::CLI::Workflow SearchRequest searchRequest; searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Id, MatchType::CaseInsensitive, manifest.Id)); // In case there're same Ids from different sources, filter the result using package name - searchRequest.Filters.emplace_back(PackageMatchFilter(PackageMatchField::Name, MatchType::CaseInsensitive, manifest.Name)); + searchRequest.Filters.emplace_back(PackageMatchFilter(PackageMatchField::Name, MatchType::CaseInsensitive, manifest.DefaultLocalization.Get<Manifest::Localization::PackageName>())); context.Add<Execution::Data::SearchResult>(source->Search(searchRequest)); } diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -223,7 +223,7 @@ namespace AppInstaller::CLI::Workflow // Outputs: Manifest, PackageVersion void GetManifestFromPackage(Execution::Context& context); - // Ensures the the file exists and is not a directory. + // Ensures the file exists and is not a directory. // Required Args: the one given // Inputs: None // Outputs: None @@ -237,6 +237,20 @@ namespace AppInstaller::CLI::Workflow Execution::Args::Type m_arg; }; + // Ensures the path exists. + // Required Args: the one given + // Inputs: None + // Outputs: None + struct VerifyPath : public WorkflowTask + { + VerifyPath(Execution::Args::Type arg) : WorkflowTask("VerifyPath"), m_arg(arg) {} + + void operator()(Execution::Context& context) const override; + + private: + Execution::Args::Type m_arg; + }; + // Opens the manifest file provided on the command line. // Required Args: Manifest // Inputs: None diff --git a/src/AppInstallerCLICore/pch.h b/src/AppInstallerCLICore/pch.h @@ -16,6 +16,7 @@ #include <memory> #include <numeric> #include <optional> +#include <set> #include <sstream> #include <string_view> #include <vector> diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs @@ -60,6 +60,7 @@ namespace AppInstallerCLIE2ETests { public const int S_OK = 0; public const int ERROR_FILE_NOT_FOUND = unchecked((int)0x80070002); + public const int ERROR_PATH_NOT_FOUND = unchecked((int)0x80070003); public const int ERROR_NO_RANGES_PROCESSED = unchecked((int)0x80070138); public const int OPC_E_ZIP_MISSING_END_OF_CENTRAL_DIRECTORY = unchecked((int)0x8051100f); public const int ERROR_OLD_WIN_VERSION = unchecked((int)0x8007047e); diff --git a/src/AppInstallerCLIE2ETests/InstallCommand.cs b/src/AppInstallerCLIE2ETests/InstallCommand.cs @@ -43,8 +43,8 @@ namespace AppInstallerCLIE2ETests { var installDir = TestCommon.GetRandomTestDir(); var result = TestCommon.RunAICLICommand("install", $"InapplicableOsVersion --silent -l {installDir}"); - Assert.AreEqual(Constants.ErrorCode.ERROR_OLD_WIN_VERSION, result.ExitCode); - Assert.True(result.StdOut.Contains("Cannot install package, as it requires a higher version of Windows")); + // MinOSVersion is moved to installer level, the check is performed during installer selection + Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICABLE_INSTALLER, result.ExitCode); Assert.False(VerifyTestExeInstalled(installDir)); } diff --git a/src/AppInstallerCLIE2ETests/ValidateCommand.cs b/src/AppInstallerCLIE2ETests/ValidateCommand.cs @@ -35,8 +35,8 @@ namespace AppInstallerCLIE2ETests public void ValidateManifestDoesNotExist() { var result = TestCommon.RunAICLICommand("validate", TestCommon.GetTestDataFile("Manifests\\DoesNotExist")); - Assert.AreEqual(Constants.ErrorCode.ERROR_FILE_NOT_FOUND, result.ExitCode); - Assert.True(result.StdOut.Contains("File does not exist")); + Assert.AreEqual(Constants.ErrorCode.ERROR_PATH_NOT_FOUND, result.ExitCode); + Assert.True(result.StdOut.Contains("Path does not exist")); } } } \ No newline at end of file diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -811,4 +811,7 @@ They can be configured through the settings file 'winget settings'.</value> <data name="ImportIgnoreVersionsArgumentDescription" xml:space="preserve"> <value>Ignore package versions from import file</value> </data> + <data name="VerifyPathFailedNotExist" xml:space="preserve"> + <value>Path does not exist:</value> + </data> </root> \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -54,6 +54,7 @@ </ImportGroup> <ImportGroup Label="Shared"> <Import Project="..\catch2\catch2.vcxitems" Label="Shared" /> + <Import Project="..\ManifestSchema\ManifestSchema.vcxitems" Label="Shared" /> </ImportGroup> <ImportGroup Label="PropertySheets"> <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> @@ -285,9 +286,6 @@ <CopyFileToFolders Include="TestData\Manifest-Bad-ArchMissing.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> - <CopyFileToFolders Include="TestData\Manifest-Bad-Channel-NotSupported.yaml"> - <DeploymentContent>true</DeploymentContent> - </CopyFileToFolders> <CopyFileToFolders Include="TestData\Manifest-Bad-DifferentCase-camelCase.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> @@ -465,6 +463,21 @@ <CopyFileToFolders Include="TestData\NormalizationInitialIds.txt"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\ManifestV1-Singleton.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\MultiFileManifestV1\ManifestV1-MultiFile-DefaultLocale.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\MultiFileManifestV1\ManifestV1-MultiFile-Installer.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\MultiFileManifestV1\ManifestV1-MultiFile-Locale.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\MultiFileManifestV1\ManifestV1-MultiFile-Version.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> </ItemGroup> <ItemGroup> <ProjectReference Include="..\AppInstallerCLICore\AppInstallerCLICore.vcxproj"> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -16,6 +16,9 @@ <Filter Include="TestData"> <UniqueIdentifier>{d5cac203-3846-4b39-a1cd-8de9303757b4}</UniqueIdentifier> </Filter> + <Filter Include="TestData\MultiFileManifestV1"> + <UniqueIdentifier>{69fcd25c-e737-4d28-a6d1-39ce491bf293}</UniqueIdentifier> + </Filter> </ItemGroup> <ItemGroup> <ClInclude Include="pch.h"> @@ -132,9 +135,6 @@ <CopyFileToFolders Include="TestData\Manifest-Bad-ArchMissing.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> - <CopyFileToFolders Include="TestData\Manifest-Bad-Channel-NotSupported.yaml"> - <Filter>TestData</Filter> - </CopyFileToFolders> <CopyFileToFolders Include="TestData\Manifest-Bad-DifferentCase-camelCase.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> @@ -372,5 +372,20 @@ <CopyFileToFolders Include="TestData\NormalizationInitialIds.txt"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\ManifestV1-Singleton.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\MultiFileManifestV1\ManifestV1-MultiFile-DefaultLocale.yaml"> + <Filter>TestData\MultiFileManifestV1</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\MultiFileManifestV1\ManifestV1-MultiFile-Installer.yaml"> + <Filter>TestData\MultiFileManifestV1</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\MultiFileManifestV1\ManifestV1-MultiFile-Locale.yaml"> + <Filter>TestData\MultiFileManifestV1</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\MultiFileManifestV1\ManifestV1-MultiFile-Version.yaml"> + <Filter>TestData\MultiFileManifestV1</Filter> + </CopyFileToFolders> </ItemGroup> </Project> \ No newline at end of file diff --git a/src/AppInstallerCLITests/CompositeSource.cpp b/src/AppInstallerCLITests/CompositeSource.cpp @@ -74,8 +74,8 @@ Manifest::Manifest MakeDefaultManifest() Manifest::Manifest result; result.Id = "Id"; - result.Name = "Name"; - result.Publisher = "Publisher"; + result.DefaultLocalization.Add<Manifest::Localization::PackageName>("Name"); + result.DefaultLocalization.Add<Manifest::Localization::Publisher>("Publisher"); result.Version = "1.0"; result.Installers.push_back({}); @@ -191,7 +191,7 @@ TEST_CASE("CompositeSource_MultiMatch_FindsId", "[CompositeSource]") { SearchResult result; result.Matches.emplace_back(MakeAvailable([](Manifest::Manifest& m) { m.Id = "A different ID"; }), Criteria()); - result.Matches.emplace_back(MakeAvailable([&](Manifest::Manifest& m) { m.Name = name; }), Criteria()); + result.Matches.emplace_back(MakeAvailable([&](Manifest::Manifest& m) { m.DefaultLocalization.Add<Manifest::Localization::PackageName>(name); }), Criteria()); return result; }; @@ -482,7 +482,7 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_MatchFirst", "[CompositeSour REQUIRE(request.Inclusions[0].Value == pfn); SearchResult result; - result.Matches.emplace_back(MakeAvailable([&](Manifest::Manifest& m) { m.Name = firstName; }), Criteria()); + result.Matches.emplace_back(MakeAvailable([&](Manifest::Manifest& m) { m.DefaultLocalization.Add<Manifest::Localization::PackageName>(firstName); }), Criteria()); return result; }; @@ -492,7 +492,7 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_MatchFirst", "[CompositeSour REQUIRE(request.Inclusions[0].Value == pfn); SearchResult result; - result.Matches.emplace_back(MakeAvailable([&](Manifest::Manifest& m) { m.Name = secondName; }), Criteria()); + result.Matches.emplace_back(MakeAvailable([&](Manifest::Manifest& m) { m.DefaultLocalization.Add<Manifest::Localization::PackageName>(secondName); }), Criteria()); return result; }; @@ -522,7 +522,7 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_MatchSecond", "[CompositeSou REQUIRE(request.Inclusions[0].Value == pfn); SearchResult result; - result.Matches.emplace_back(MakeAvailable([&](Manifest::Manifest& m) { m.Name = secondName; }), Criteria()); + result.Matches.emplace_back(MakeAvailable([&](Manifest::Manifest& m) { m.DefaultLocalization.Add<Manifest::Localization::PackageName>(secondName); }), Criteria()); return result; }; diff --git a/src/AppInstallerCLITests/PredefinedInstalledSource.cpp b/src/AppInstallerCLITests/PredefinedInstalledSource.cpp @@ -85,11 +85,11 @@ SQLiteIndex::MetadataResult::const_iterator Find(const SQLiteIndex::MetadataResu return std::find_if(metadata.begin(), metadata.end(), [value](const auto& m) { return m.first == value; }); } -void VerifyInstalledType(const SQLiteIndex::MetadataResult& metadata, ManifestInstaller::InstallerTypeEnum type) +void VerifyInstalledType(const SQLiteIndex::MetadataResult& metadata, InstallerTypeEnum type) { auto itr = Find(metadata, PackageVersionMetadata::InstalledType); REQUIRE(itr != metadata.end()); - REQUIRE(ManifestInstaller::ConvertToInstallerTypeEnum(itr->second) == type); + REQUIRE(ConvertToInstallerTypeEnum(itr->second) == type); } void VerifyTestScope(const SQLiteIndex::MetadataResult& metadata) @@ -126,7 +126,7 @@ void VerifyEntryAgainstIndex(const SQLiteIndex& index, SQLiteIndex::IdType manif auto metadata = index.GetMetadataByManifestId(manifestId); - VerifyInstalledType(metadata, entry.WindowsInstaller.value_or(false) ? ManifestInstaller::InstallerTypeEnum::Msi : ManifestInstaller::InstallerTypeEnum::Exe); + VerifyInstalledType(metadata, entry.WindowsInstaller.value_or(false) ? InstallerTypeEnum::Msi : InstallerTypeEnum::Exe); VerifyTestScope(metadata); VerifyMetadataString(metadata, PackageVersionMetadata::InstalledLocation, entry.InstallLocation); VerifyMetadataString(metadata, PackageVersionMetadata::StandardUninstallCommand, entry.UninstallString); @@ -151,7 +151,7 @@ TEST_CASE("ARPHelper_GetARPForArchitecture", "[arphelper][list]") ARPHelper helper; - auto nativeMachineKey = helper.GetARPKey(ManifestInstaller::ScopeEnum::Machine, systemArch); + auto nativeMachineKey = helper.GetARPKey(ScopeEnum::Machine, systemArch); REQUIRE(nativeMachineKey); } diff --git a/src/AppInstallerCLITests/SQLiteIndex.cpp b/src/AppInstallerCLITests/SQLiteIndex.cpp @@ -64,13 +64,14 @@ SQLiteIndex SimpleTestSetup(const std::string& filePath, Manifest& manifest, std { SQLiteIndex index = CreateTestIndex(filePath, version); + manifest.Installers.push_back({}); manifest.Id = "Test.Id"; - manifest.Name = "Test Name"; - manifest.AppMoniker = "testmoniker"; + manifest.DefaultLocalization.Add<Localization::PackageName>("Test Name"); + manifest.Moniker = "testmoniker"; manifest.Version = "1.0.0"; manifest.Channel = "test"; - manifest.Tags = { "t1", "t2" }; - manifest.Commands = { "test1", "test2" }; + manifest.DefaultLocalization.Add<Localization::Tags>({ "t1", "t2" }); + manifest.Installers[0].Commands = { "test1", "test2" }; relativePath = "test/id/1.0.0.yaml"; @@ -143,17 +144,24 @@ SQLiteIndex SearchTestSetup(const std::string& filePath, std::initializer_list<I Manifest manifest; - auto addFunc = [&](const IndexFields& d) { + auto addFunc = [&](const IndexFields& d) + { manifest.Id = d.Id; - manifest.Name = d.Name; - manifest.AppMoniker = d.Moniker; + manifest.DefaultLocalization.Add<Localization::PackageName>(d.Name); + manifest.Moniker = d.Moniker; manifest.Version = d.Version; - manifest.Channel = d.Channel; - manifest.Tags = d.Tags; - manifest.Commands = d.Commands; + manifest.DefaultLocalization.Add<Localization::Tags>(d.Tags); manifest.Installers.resize(std::max(d.PackageFamilyNames.size(), d.ProductCodes.size())); + if (manifest.Installers.size() == 0) + { + manifest.Installers.push_back({}); + } + + manifest.Channel = d.Channel; + manifest.Installers[0].Commands = d.Commands; + for (size_t i = 0; i < d.PackageFamilyNames.size(); ++i) { manifest.Installers[i].PackageFamilyName = d.PackageFamilyNames[i]; @@ -319,24 +327,26 @@ TEST_CASE("SQLiteIndex_RemoveManifest", "[sqliteindex][V1_0]") std::string manifest1Path = "test/id/test.id-1.0.0.yaml"; Manifest manifest1; + manifest1.Installers.push_back({}); manifest1.Id = "test.id"; - manifest1.Name = "Test Name"; - manifest1.AppMoniker = "testmoniker"; + manifest1.DefaultLocalization.Add<Localization::PackageName>("Test Name"); + manifest1.Moniker = "testmoniker"; manifest1.Version = "1.0.0"; manifest1.Channel = "test"; - manifest1.Tags = { "t1", "t2" }; - manifest1.Commands = { "test1", "test2" }; + manifest1.DefaultLocalization.Add<Localization::Tags>({ "t1", "t2" }); + manifest1.Installers[0].Commands = { "test1", "test2" }; std::string manifest2Path = "test/woah/test.id-1.0.0.yaml"; Manifest manifest2; + manifest2.Installers.push_back({}); manifest2.Id = "test.woah"; - manifest2.Name = "Test Name WOAH"; - manifest2.AppMoniker = "testmoniker"; + manifest2.DefaultLocalization.Add<Localization::PackageName>("Test Name WOAH"); + manifest2.Moniker = "testmoniker"; manifest2.Version = "1.0.0"; manifest2.Channel = "test"; - manifest2.Tags = {}; - manifest2.Commands = { "test1", "test2", "test3" }; - + manifest2.DefaultLocalization.Add<Localization::Tags>({}); + manifest2.Installers[0].Commands = { "test1", "test2", "test3" }; + { SQLiteIndex index = SQLiteIndex::CreateNew(tempFile, { 1, 0 }); @@ -391,23 +401,25 @@ TEST_CASE("SQLiteIndex_RemoveManifest_EnsureConsistentRowId", "[sqliteindex]") std::string manifest1Path = "test/id/test.id-1.0.0.yaml"; Manifest manifest1; + manifest1.Installers.push_back({}); manifest1.Id = "test.id"; - manifest1.Name = "Test Name"; - manifest1.AppMoniker = "testmoniker"; + manifest1.DefaultLocalization.Add<Localization::PackageName>("Test Name"); + manifest1.Moniker = "testmoniker"; manifest1.Version = "1.0.0"; manifest1.Channel = "test"; - manifest1.Tags = { "t1", "t2" }; - manifest1.Commands = { "test1", "test2" }; + manifest1.DefaultLocalization.Add<Localization::Tags>({ "t1", "t2" }); + manifest1.Installers[0].Commands = { "test1", "test2" }; std::string manifest2Path = "test/woah/test.id-1.0.0.yaml"; Manifest manifest2; + manifest2.Installers.push_back({}); manifest2.Id = "test.woah"; - manifest2.Name = "Test Name WOAH"; - manifest2.AppMoniker = "testmoniker"; + manifest2.DefaultLocalization.Add<Localization::PackageName>("Test Name WOAH"); + manifest2.Moniker = "testmoniker"; manifest2.Version = "1.0.0"; manifest2.Channel = "test"; - manifest2.Tags = {}; - manifest2.Commands = { "test1", "test2", "test3" }; + manifest2.DefaultLocalization.Add<Localization::Tags>({}); + manifest2.Installers[0].Commands = { "test1", "test2", "test3" }; SQLiteIndex index = CreateTestIndex(tempFile); @@ -442,7 +454,7 @@ TEST_CASE("SQLiteIndex_RemoveManifest_EnsureConsistentRowId", "[sqliteindex]") REQUIRE(rowId.value() == manifest2RowId); REQUIRE(manifest2.Id == index.GetPropertyByManifestId(manifest2RowId, PackageVersionProperty::Id)); - REQUIRE(manifest2.Name == index.GetPropertyByManifestId(manifest2RowId, PackageVersionProperty::Name)); + REQUIRE(manifest2.DefaultLocalization.Get<Localization::PackageName>() == index.GetPropertyByManifestId(manifest2RowId, PackageVersionProperty::Name)); REQUIRE(manifest2.Version == index.GetPropertyByManifestId(manifest2RowId, PackageVersionProperty::Version)); REQUIRE(manifest2.Channel == index.GetPropertyByManifestId(manifest2RowId, PackageVersionProperty::Channel)); REQUIRE(manifest2Path == index.GetPropertyByManifestId(manifest2RowId, PackageVersionProperty::RelativePath)); @@ -486,13 +498,14 @@ TEST_CASE("SQLiteIndex_UpdateManifest", "[sqliteindex][V1_0]") std::string manifestPath = "test/id/test.id-1.0.0.yaml"; Manifest manifest; + manifest.Installers.push_back({}); manifest.Id = "test.id"; - manifest.Name = "Test Name"; - manifest.AppMoniker = "testmoniker"; + manifest.DefaultLocalization.Add < Localization::PackageName>("Test Name"); + manifest.Moniker = "testmoniker"; manifest.Version = "1.0.0"; manifest.Channel = "test"; - manifest.Tags = { "t1", "t2" }; - manifest.Commands = { "test1", "test2" }; + manifest.DefaultLocalization.Add<Localization::Tags>({ "t1", "t2" }); + manifest.Installers[0].Commands = { "test1", "test2" }; { SQLiteIndex index = SQLiteIndex::CreateNew(tempFile, { 1, 0 }); @@ -521,16 +534,16 @@ TEST_CASE("SQLiteIndex_UpdateManifest", "[sqliteindex][V1_0]") // Update with no updates should return false REQUIRE(!index.UpdateManifest(manifest, manifestPath)); - manifest.Description = "description2"; + manifest.DefaultLocalization.Add<Localization::Description>("description2"); // Update with no indexed updates should return false REQUIRE(!index.UpdateManifest(manifest, manifestPath)); // Update with indexed changes - manifest.Name = "Test Name2"; - manifest.AppMoniker = "testmoniker2"; - manifest.Tags = { "t1", "t2", "t3" }; - manifest.Commands = {}; + manifest.DefaultLocalization.Add<Localization::PackageName>("Test Name2"); + manifest.Moniker = "testmoniker2"; + manifest.DefaultLocalization.Add<Localization::Tags>({ "t1", "t2", "t3" }); + manifest.Installers[0].Commands = {}; REQUIRE(index.UpdateManifest(manifest, manifestPath)); } @@ -579,13 +592,14 @@ TEST_CASE("SQLiteIndex_UpdateManifestChangePath", "[sqliteindex][V1_0]") std::string manifestPath = "test/id/test.id-1.0.0.yaml"; Manifest manifest; + manifest.Installers.push_back({}); manifest.Id = "test.id"; - manifest.Name = "Test Name"; - manifest.AppMoniker = "testmoniker"; + manifest.DefaultLocalization.Add<Localization::PackageName>("Test Name"); + manifest.Moniker = "testmoniker"; manifest.Version = "1.0.0"; manifest.Channel = "test"; - manifest.Tags = { "t1", "t2" }; - manifest.Commands = { "test1", "test2" }; + manifest.DefaultLocalization.Add<Localization::Tags>({ "t1", "t2" }); + manifest.Installers[0].Commands = { "test1", "test2" }; { SQLiteIndex index = SQLiteIndex::CreateNew(tempFile, { 1, 0 }); @@ -660,13 +674,14 @@ TEST_CASE("SQLiteIndex_UpdateManifestChangeCase", "[sqliteindex][V1_0]") std::string manifestPath = "test/id/test.id-1.0.0.yaml"; Manifest manifest; + manifest.Installers.push_back({}); manifest.Id = "test.id"; - manifest.Name = "Test Name"; - manifest.AppMoniker = "testmoniker"; + manifest.DefaultLocalization.Add<Localization::PackageName>("Test Name"); + manifest.Moniker = "testmoniker"; manifest.Version = "1.0.0-test"; manifest.Channel = "test"; - manifest.Tags = { "t1", "t2" }; - manifest.Commands = { "test1", "test2" }; + manifest.DefaultLocalization.Add<Localization::Tags>({ "t1", "t2" }); + manifest.Installers[0].Commands = { "test1", "test2" }; { SQLiteIndex index = SQLiteIndex::CreateNew(tempFile, { 1, 0 }); @@ -704,7 +719,7 @@ TEST_CASE("SQLiteIndex_UpdateManifestChangeCase", "[sqliteindex][V1_0]") { SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteIndex::OpenDisposition::ReadWrite); - manifest.Name = "test name"; + manifest.DefaultLocalization.Add<Localization::PackageName>("test name"); // Update with path update should indicate change REQUIRE(index.UpdateManifest(manifest, manifestPath)); @@ -738,12 +753,13 @@ TEST_CASE("SQLiteIndex_IdCaseInsensitivity", "[sqliteindex][V1_0]") std::string manifest1Path = "test/id/test.id-1.0.0.yaml"; Manifest manifest1; + manifest1.Installers.push_back({}); manifest1.Id = "test.id"; - manifest1.Name = "Test Name"; - manifest1.AppMoniker = "testmoniker"; + manifest1.DefaultLocalization.Add<Localization::PackageName>("Test Name"); + manifest1.Moniker = "testmoniker"; manifest1.Version = "1.0.0"; - manifest1.Tags = { "t1", "t2" }; - manifest1.Commands = { "test1", "test2" }; + manifest1.DefaultLocalization.Add<Localization::Tags>({ "t1", "t2" }); + manifest1.Installers[0].Commands = { "test1", "test2" }; std::string manifest2Path = "test/id/test.id-2.0.0.yaml"; Manifest manifest2 = manifest1; @@ -999,7 +1015,7 @@ TEST_CASE("SQLiteIndex_NameString", "[sqliteindex]") REQUIRE(results.Matches.size() == 1); auto result = GetNameStringById(index, results.Matches[0].first); - REQUIRE(result == manifest.Name); + REQUIRE(result == manifest.DefaultLocalization.Get<Localization::PackageName>()); } TEST_CASE("SQLiteIndex_PathString", "[sqliteindex]") @@ -1821,23 +1837,25 @@ TEST_CASE("SQLiteIndex_CheckConsistency_Failure", "[sqliteindex][V1_1]") std::string manifest1Path = "test/id/test.id-1.0.0.yaml"; Manifest manifest1; + manifest1.Installers.push_back({}); manifest1.Id = "test.id"; - manifest1.Name = "Test Name"; - manifest1.AppMoniker = "testmoniker"; + manifest1.DefaultLocalization.Add<Localization::PackageName>("Test Name"); + manifest1.Moniker = "testmoniker"; manifest1.Version = "1.0.0"; manifest1.Channel = "test"; - manifest1.Tags = { "t1", "t2" }; - manifest1.Commands = { "test1", "test2" }; + manifest1.DefaultLocalization.Add<Localization::Tags>({ "t1", "t2" }); + manifest1.Installers[0].Commands = { "test1", "test2" }; std::string manifest2Path = "test/woah/test.id-1.0.0.yaml"; Manifest manifest2; + manifest2.Installers.push_back({}); manifest2.Id = "test.woah"; - manifest2.Name = "Test Name WOAH"; - manifest2.AppMoniker = "testmoniker"; + manifest2.DefaultLocalization.Add<Localization::PackageName>("Test Name WOAH"); + manifest2.Moniker = "testmoniker"; manifest2.Version = "1.0.0"; manifest2.Channel = "test"; - manifest2.Tags = {}; - manifest2.Commands = { "test1", "test2", "test3" }; + manifest2.DefaultLocalization.Add<Localization::Tags>({}); + manifest2.Installers[0].Commands = { "test1", "test2", "test3" }; SQLite::rowid_t manifestRowId = 0; diff --git a/src/AppInstallerCLITests/SQLiteIndexSource.cpp b/src/AppInstallerCLITests/SQLiteIndexSource.cpp @@ -108,7 +108,7 @@ TEST_CASE("SQLiteIndexSource_Name", "[sqliteindexsource]") REQUIRE(results.Matches[0].Package); auto latestVersion = results.Matches[0].Package->GetLatestAvailableVersion(); - REQUIRE(latestVersion->GetProperty(PackageVersionProperty::Name).get() == manifest.Name); + REQUIRE(latestVersion->GetProperty(PackageVersionProperty::Name).get() == manifest.DefaultLocalization.Get<Localization::PackageName>()); } TEST_CASE("SQLiteIndexSource_Versions", "[sqliteindexsource]") @@ -156,7 +156,7 @@ TEST_CASE("SQLiteIndexSource_GetManifest", "[sqliteindexsource]") REQUIRE(specificResultVersion); auto specificResult = specificResultVersion->GetManifest(); REQUIRE(specificResult.Id == manifest.Id); - REQUIRE(specificResult.Name == manifest.Name); + REQUIRE(specificResult.DefaultLocalization.Get<Localization::PackageName>() == manifest.DefaultLocalization.Get<Localization::PackageName>()); REQUIRE(specificResult.Version == manifest.Version); REQUIRE(specificResult.Channel == manifest.Channel); @@ -164,7 +164,7 @@ TEST_CASE("SQLiteIndexSource_GetManifest", "[sqliteindexsource]") REQUIRE(latestResultVersion); auto latestResult = latestResultVersion->GetManifest(); REQUIRE(latestResult.Id == manifest.Id); - REQUIRE(latestResult.Name == manifest.Name); + REQUIRE(latestResult.DefaultLocalization.Get<Localization::PackageName>() == manifest.DefaultLocalization.Get<Localization::PackageName>()); REQUIRE(latestResult.Version == manifest.Version); REQUIRE(latestResult.Channel == manifest.Channel); diff --git a/src/AppInstallerCLITests/TestData/Manifest-Bad-Channel-NotSupported.yaml b/src/AppInstallerCLITests/TestData/Manifest-Bad-Channel-NotSupported.yaml @@ -1,13 +0,0 @@ -# Minimum required -Id: microsoft.msixsdk -Name: MSIX SDK -Version: 1.7.32 -Publisher: Microsoft -Channel: release -InstallerType: Zip -License: Test -Installers: - - Arch: x86 - Url: https://rubengustorage.blob.core.windows.net/publiccontainer/msixsdk-x86.zip - Sha256: 98B67758CEAFFCBB3FE47838FD0A8D7BD581C2650842D6B2B0E0D49A23270CCD -ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLITests/TestData/Manifest-Good-InstallerUniqueness-DiffScope.yaml b/src/AppInstallerCLITests/TestData/Manifest-Good-InstallerUniqueness-DiffScope.yaml @@ -13,5 +13,5 @@ Installers: - Arch: x86 Url: https://rubengustorage.blob.core.windows.net/publiccontainer/msixsdk-x86.zip Sha256: 98B67758CEAFFCBB3FE47838FD0A8D7BD581C2650842D6B2B0E0D49A23270CCD - Scope: system + Scope: machine ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLITests/TestData/ManifestV1-Singleton.yaml b/src/AppInstallerCLITests/TestData/ManifestV1-Singleton.yaml @@ -0,0 +1,121 @@ +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.7.32 +PackageLocale: en-US +Publisher: Microsoft +PublisherUrl: https://www.microsoft.com +PublisherSupportUrl: https://www.microsoft.com/support +PrivacyUrl: https://www.microsoft.com/privacy +Author: Microsoft +PackageName: MSIX SDK +PackageUrl: https://www.microsoft.com/msixsdk/home +License: MIT License +LicenseUrl: https://www.microsoft.com/msixsdk/license +Copyright: Copyright Microsoft Corporation +CopyrightUrl: https://www.microsoft.com/msixsdk/copyright +ShortDescription: This is MSIX SDK +Description: The MSIX SDK project is an effort to enable developers +Moniker: msixsdk +Tags: + - "appxsdk" + - "msixsdk" +Channel: release +InstallerLocale: en-US +Platform: + - Windows.Desktop + - Windows.Universal +MinimumOSVersion: 10.0.0.0 +InstallerType: zip +Scope: machine +InstallModes: + - user + - machine +InstallerSwitches: + Custom: /custom + SilentWithProgress: /silentwithprogress + Silent: /silence + Interactive: /interactive + Log: /log=<LOGPATH> + InstallLocation: /dir=<INSTALLPATH> + Upgrade: /upgrade +InstallerSuccessCodes: + - 1 + - 0x80070005 +UpgradeBehavior: uninstallPrevious +Commands: + - makemsix + - makeappx +Protocols: + - protocol1 + - protocol2 +FileExtensions: + - appx + - msix + - appxbundle + - msixbundle +Dependencies: + WindowsFeatures: + - IIS + WindowsLibraries: + - VC Runtime + PackageDependencies: + - PackageIdentifier: Microsoft.MsixSdkDep + MinimumVersion: 1.0.0 + ExternalDependencies: + - Outside dependencies +Capabilities: + - internetClient +RestrictedCapabilities: + - runFullTrust +PackageFamilyName: Microsoft.DesktopAppInstaller_8wekyb3d8bbwe +ProductCode: "{Foo}" + +Installers: + - Architecture: x86 + InstallerLocale: en-GB + Platform: + - Windows.Desktop + MinimumOSVersion: 10.0.1.0 + InstallerType: msix + InstallerUrl: https://www.microsoft.com/msixsdk/msixsdkx86.msix + InstallerSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + SignatureSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + Scope: user + InstallModes: + - user + InstallerSwitches: + Custom: /c + SilentWithProgress: /sp + Silent: /s + Interactive: /i + Log: /l=<LOGPATH> + InstallLocation: /d=<INSTALLPATH> + Upgrade: /u + UpgradeBehavior: install + Commands: + - makemsixPreview + - makeappxPreview + Protocols: + - protocol1preview + - protocol2preview + FileExtensions: + - appxbundle + - msixbundle + - appx + - msix + Dependencies: + WindowsFeatures: + - PreviewIIS + WindowsLibraries: + - Preview VC Runtime + PackageDependencies: + - PackageIdentifier: Microsoft.MsixSdkDepPreview + ExternalDependencies: + - Preview Outside dependencies + PackageFamilyName: Microsoft.DesktopAppInstallerPreview_8wekyb3d8bbwe + Capabilities: + - internetClientPreview + RestrictedCapabilities: + - runFullTrustPreview + +ManifestType: singleton +ManifestVersion: 1.0.0 diff --git a/src/AppInstallerCLITests/TestData/MultiFileManifestV1/ManifestV1-MultiFile-DefaultLocale.yaml b/src/AppInstallerCLITests/TestData/MultiFileManifestV1/ManifestV1-MultiFile-DefaultLocale.yaml @@ -0,0 +1,23 @@ +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.7.32 +PackageLocale: en-US +Publisher: Microsoft +PublisherUrl: https://www.microsoft.com +PublisherSupportUrl: https://www.microsoft.com/support +PrivacyUrl: https://www.microsoft.com/privacy +Author: Microsoft +PackageName: MSIX SDK +PackageUrl: https://www.microsoft.com/msixsdk/home +License: MIT License +LicenseUrl: https://www.microsoft.com/msixsdk/license +Copyright: Copyright Microsoft Corporation +CopyrightUrl: https://www.microsoft.com/msixsdk/copyright +ShortDescription: This is MSIX SDK +Description: The MSIX SDK project is an effort to enable developers +Moniker: msixsdk +Tags: + - "appxsdk" + - "msixsdk" + +ManifestType: defaultLocale +ManifestVersion: 1.0.0 diff --git a/src/AppInstallerCLITests/TestData/MultiFileManifestV1/ManifestV1-MultiFile-Installer.yaml b/src/AppInstallerCLITests/TestData/MultiFileManifestV1/ManifestV1-MultiFile-Installer.yaml @@ -0,0 +1,108 @@ +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.7.32 +Channel: release +InstallerLocale: en-US +Platform: + - Windows.Desktop + - Windows.Universal +MinimumOSVersion: 10.0.0.0 +InstallerType: zip +Scope: machine +InstallModes: + - user + - machine +InstallerSwitches: + Custom: /custom + SilentWithProgress: /silentwithprogress + Silent: /silence + Interactive: /interactive + Log: /log=<LOGPATH> + InstallLocation: /dir=<INSTALLPATH> + Upgrade: /upgrade +InstallerSuccessCodes: + - 1 + - 0x80070005 +UpgradeBehavior: uninstallPrevious +Commands: + - makemsix + - makeappx +Protocols: + - protocol1 + - protocol2 +FileExtensions: + - appx + - msix + - appxbundle + - msixbundle +Dependencies: + WindowsFeatures: + - IIS + WindowsLibraries: + - VC Runtime + PackageDependencies: + - PackageIdentifier: Microsoft.MsixSdkDep + MinimumVersion: 1.0.0 + ExternalDependencies: + - Outside dependencies +Capabilities: + - internetClient +RestrictedCapabilities: + - runFullTrust +PackageFamilyName: Microsoft.DesktopAppInstaller_8wekyb3d8bbwe +ProductCode: "{Foo}" + +Installers: + - Architecture: x86 + InstallerLocale: en-GB + Platform: + - Windows.Desktop + MinimumOSVersion: 10.0.1.0 + InstallerType: msix + InstallerUrl: https://www.microsoft.com/msixsdk/msixsdkx86.msix + InstallerSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + SignatureSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + Scope: user + InstallModes: + - user + InstallerSwitches: + Custom: /c + SilentWithProgress: /sp + Silent: /s + Interactive: /i + Log: /l=<LOGPATH> + InstallLocation: /d=<INSTALLPATH> + Upgrade: /u + UpgradeBehavior: install + Commands: + - makemsixPreview + - makeappxPreview + Protocols: + - protocol1preview + - protocol2preview + FileExtensions: + - appxbundle + - msixbundle + - appx + - msix + Dependencies: + WindowsFeatures: + - PreviewIIS + WindowsLibraries: + - Preview VC Runtime + PackageDependencies: + - PackageIdentifier: Microsoft.MsixSdkDepPreview + ExternalDependencies: + - Preview Outside dependencies + PackageFamilyName: Microsoft.DesktopAppInstallerPreview_8wekyb3d8bbwe + Capabilities: + - internetClientPreview + RestrictedCapabilities: + - runFullTrustPreview + - Architecture: x64 + InstallerType: exe + InstallerUrl: https://www.microsoft.com/msixsdk/msixsdkx64.exe + InstallerSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + ProductCode: "{Bar}" + +ManifestType: installer +ManifestVersion: 1.0.0 diff --git a/src/AppInstallerCLITests/TestData/MultiFileManifestV1/ManifestV1-MultiFile-Locale.yaml b/src/AppInstallerCLITests/TestData/MultiFileManifestV1/ManifestV1-MultiFile-Locale.yaml @@ -0,0 +1,22 @@ +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.7.32 +PackageLocale: en-GB +Publisher: Microsoft UK +PublisherUrl: https://www.microsoft.com/UK +PublisherSupportUrl: https://www.microsoft.com/support/UK +PrivacyUrl: https://www.microsoft.com/privacy/UK +Author: Microsoft UK +PackageName: MSIX SDK UK +PackageUrl: https://www.microsoft.com/msixsdk/home/UK +License: MIT License UK +LicenseUrl: https://www.microsoft.com/msixsdk/license/UK +Copyright: Copyright Microsoft Corporation UK +CopyrightUrl: https://www.microsoft.com/msixsdk/copyright/UK +ShortDescription: This is MSIX SDK UK +Description: The MSIX SDK project is an effort to enable developers UK +Tags: + - "appxsdkUK" + - "msixsdkUK" + +ManifestType: locale +ManifestVersion: 1.0.0 diff --git a/src/AppInstallerCLITests/TestData/MultiFileManifestV1/ManifestV1-MultiFile-Version.yaml b/src/AppInstallerCLITests/TestData/MultiFileManifestV1/ManifestV1-MultiFile-Version.yaml @@ -0,0 +1,6 @@ +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.7.32 +DefaultLocale: en-US + +ManifestType: version +ManifestVersion: 1.0.0 diff --git a/src/AppInstallerCLITests/TestSource.cpp b/src/AppInstallerCLITests/TestSource.cpp @@ -22,7 +22,7 @@ namespace TestCommon case PackageVersionProperty::Id: return LocIndString{ VersionManifest.Id }; case PackageVersionProperty::Name: - return LocIndString{ VersionManifest.Name }; + return LocIndString{ VersionManifest.DefaultLocalization.Get<AppInstaller::Manifest::Localization::PackageName>() }; case PackageVersionProperty::Version: return LocIndString{ VersionManifest.Version }; case PackageVersionProperty::Channel: diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp @@ -4,10 +4,13 @@ #include "TestCommon.h" #include <AppInstallerSHA256.h> #include <winget/ManifestYamlParser.h> +#include <winget/Yaml.h> using namespace TestCommon; using namespace AppInstaller::Manifest; +using namespace AppInstaller::Manifest::YamlParser; using namespace AppInstaller::Utility; +using namespace AppInstaller::YAML; using MultiValue = std::vector<NormalizedString>; bool operator==(const MultiValue& a, const MultiValue& b) @@ -28,41 +31,41 @@ bool operator==(const MultiValue& a, const MultiValue& b) return true; } -TEST_CASE("ReadGoodManifestAndVerifyContents", "[ManifestValidation]") +TEST_CASE("ReadPreviewGoodManifestAndVerifyContents", "[ManifestValidation]") { Manifest manifest = YamlParser::CreateFromPath(TestDataFile("Manifest-Good.yaml")); REQUIRE(manifest.Id == "microsoft.msixsdk"); - REQUIRE(manifest.Name == "MSIX SDK"); - REQUIRE(manifest.AppMoniker == "msixsdk"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PackageName>() == "MSIX SDK"); + REQUIRE(manifest.Moniker == "msixsdk"); REQUIRE(manifest.Version == "1.7.32"); - REQUIRE(manifest.Publisher == "Microsoft"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Publisher>() == "Microsoft"); REQUIRE(manifest.Channel == "release"); - REQUIRE(manifest.Author == "Microsoft"); - REQUIRE(manifest.License == "MIT License"); - REQUIRE(manifest.LicenseUrl == "https://github.com/microsoft/msix-packaging/blob/master/LICENSE"); - REQUIRE(manifest.MinOSVersion == "0.0.0.0"); - REQUIRE(manifest.Description == "The MSIX SDK project is an effort to enable developers"); - REQUIRE(manifest.Homepage == "https://github.com/microsoft/msix-packaging"); - REQUIRE(manifest.Tags == MultiValue{ "msix", "appx" }); - REQUIRE(manifest.Commands == MultiValue{ "makemsix", "makeappx" }); - REQUIRE(manifest.Protocols == MultiValue{ "protocol1", "protocol2" }); - REQUIRE(manifest.FileExtensions == MultiValue{ "appx", "appxbundle", "msix", "msixbundle" }); - REQUIRE(manifest.InstallerType == ManifestInstaller::InstallerTypeEnum::Zip); - REQUIRE(manifest.PackageFamilyName == "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe"); - REQUIRE(manifest.ProductCode == "{Foo}"); - REQUIRE(manifest.UpdateBehavior == ManifestInstaller::UpdateBehaviorEnum::UninstallPrevious); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Author>() == "Microsoft"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::License>() == "MIT License"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::LicenseUrl>() == "https://github.com/microsoft/msix-packaging/blob/master/LICENSE"); + REQUIRE(manifest.DefaultInstallerInfo.MinOSVersion == "0.0.0.0"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Description>() == "The MSIX SDK project is an effort to enable developers"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PackageUrl>() == "https://github.com/microsoft/msix-packaging"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Tags>() == MultiValue{ "msix", "appx" }); + REQUIRE(manifest.DefaultInstallerInfo.Commands == MultiValue{ "makemsix", "makeappx" }); + REQUIRE(manifest.DefaultInstallerInfo.Protocols == MultiValue{ "protocol1", "protocol2" }); + REQUIRE(manifest.DefaultInstallerInfo.FileExtensions == MultiValue{ "appx", "appxbundle", "msix", "msixbundle" }); + REQUIRE(manifest.DefaultInstallerInfo.InstallerType == InstallerTypeEnum::Zip); + REQUIRE(manifest.DefaultInstallerInfo.PackageFamilyName == "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe"); + REQUIRE(manifest.DefaultInstallerInfo.ProductCode == "{Foo}"); + REQUIRE(manifest.DefaultInstallerInfo.UpdateBehavior == UpdateBehaviorEnum::UninstallPrevious); // default switches - auto switches = manifest.Switches; - REQUIRE(switches.at(ManifestInstaller::InstallerSwitchType::Custom) == "/custom"); - REQUIRE(switches.at(ManifestInstaller::InstallerSwitchType::SilentWithProgress) == "/silentwithprogress"); - REQUIRE(switches.at(ManifestInstaller::InstallerSwitchType::Silent) == "/silence"); - REQUIRE(switches.at(ManifestInstaller::InstallerSwitchType::Interactive) == "/interactive"); - REQUIRE(switches.at(ManifestInstaller::InstallerSwitchType::Language) == "/en-us"); - REQUIRE(switches.at(ManifestInstaller::InstallerSwitchType::Log) == "/log=<LOGPATH>"); - REQUIRE(switches.at(ManifestInstaller::InstallerSwitchType::InstallLocation) == "/dir=<INSTALLPATH>"); - REQUIRE(switches.at(ManifestInstaller::InstallerSwitchType::Update) == "/update"); + auto switches = manifest.DefaultInstallerInfo.Switches; + REQUIRE(switches.at(InstallerSwitchType::Custom) == "/custom"); + REQUIRE(switches.at(InstallerSwitchType::SilentWithProgress) == "/silentwithprogress"); + REQUIRE(switches.at(InstallerSwitchType::Silent) == "/silence"); + REQUIRE(switches.at(InstallerSwitchType::Interactive) == "/interactive"); + REQUIRE(switches.at(InstallerSwitchType::Language) == "/en-us"); + REQUIRE(switches.at(InstallerSwitchType::Log) == "/log=<LOGPATH>"); + REQUIRE(switches.at(InstallerSwitchType::InstallLocation) == "/dir=<INSTALLPATH>"); + REQUIRE(switches.at(InstallerSwitchType::Update) == "/update"); // installers REQUIRE(manifest.Installers.size() == 2); @@ -70,52 +73,52 @@ TEST_CASE("ReadGoodManifestAndVerifyContents", "[ManifestValidation]") REQUIRE(installer1.Arch == Architecture::X86); REQUIRE(installer1.Url == "https://rubengustorage.blob.core.windows.net/publiccontainer/msixsdkx86.zip"); REQUIRE(installer1.Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); - REQUIRE(installer1.Language == "en-US"); - REQUIRE(installer1.InstallerType == ManifestInstaller::InstallerTypeEnum::Zip); - REQUIRE(installer1.Scope == ManifestInstaller::ScopeEnum::User); + REQUIRE(installer1.Locale == "en-US"); + REQUIRE(installer1.InstallerType == InstallerTypeEnum::Zip); + REQUIRE(installer1.Scope == ScopeEnum::User); REQUIRE(installer1.PackageFamilyName == ""); REQUIRE(installer1.ProductCode == ""); - REQUIRE(installer1.UpdateBehavior == ManifestInstaller::UpdateBehaviorEnum::Install); + REQUIRE(installer1.UpdateBehavior == UpdateBehaviorEnum::Install); auto installer1Switches = installer1.Switches; - REQUIRE(installer1Switches.at(ManifestInstaller::InstallerSwitchType::Custom) == "/c"); - REQUIRE(installer1Switches.at(ManifestInstaller::InstallerSwitchType::SilentWithProgress) == "/sp"); - REQUIRE(installer1Switches.at(ManifestInstaller::InstallerSwitchType::Silent) == "/s"); - REQUIRE(installer1Switches.at(ManifestInstaller::InstallerSwitchType::Interactive) == "/i"); - REQUIRE(installer1Switches.at(ManifestInstaller::InstallerSwitchType::Language) == "/en"); - REQUIRE(installer1Switches.at(ManifestInstaller::InstallerSwitchType::Log) == "/l=<LOGPATH>"); - REQUIRE(installer1Switches.at(ManifestInstaller::InstallerSwitchType::InstallLocation) == "/d=<INSTALLPATH>"); - REQUIRE(installer1Switches.at(ManifestInstaller::InstallerSwitchType::Update) == "/u"); + 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::Language) == "/en"); + REQUIRE(installer1Switches.at(InstallerSwitchType::Log) == "/l=<LOGPATH>"); + REQUIRE(installer1Switches.at(InstallerSwitchType::InstallLocation) == "/d=<INSTALLPATH>"); + REQUIRE(installer1Switches.at(InstallerSwitchType::Update) == "/u"); ManifestInstaller installer2 = manifest.Installers.at(1); REQUIRE(installer2.Arch == Architecture::X64); REQUIRE(installer2.Url == "https://rubengustorage.blob.core.windows.net/publiccontainer/msixsdkx64.zip"); REQUIRE(installer2.Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF0000")); - REQUIRE(installer2.Language == "en-US"); - REQUIRE(installer2.InstallerType == ManifestInstaller::InstallerTypeEnum::Zip); - REQUIRE(installer2.Scope == ManifestInstaller::ScopeEnum::User); + REQUIRE(installer2.Locale == "en-US"); + REQUIRE(installer2.InstallerType == InstallerTypeEnum::Zip); + REQUIRE(installer2.Scope == ScopeEnum::User); REQUIRE(installer2.PackageFamilyName == ""); REQUIRE(installer2.ProductCode == ""); - REQUIRE(installer2.UpdateBehavior == ManifestInstaller::UpdateBehaviorEnum::UninstallPrevious); + REQUIRE(installer2.UpdateBehavior == UpdateBehaviorEnum::UninstallPrevious); // Installer2 does not declare switches, it inherits switches from package default. auto installer2Switches = installer2.Switches; - REQUIRE(installer2Switches.at(ManifestInstaller::InstallerSwitchType::Custom) == "/custom"); - REQUIRE(installer2Switches.at(ManifestInstaller::InstallerSwitchType::SilentWithProgress) == "/silentwithprogress"); - REQUIRE(installer2Switches.at(ManifestInstaller::InstallerSwitchType::Silent) == "/silence"); - REQUIRE(installer2Switches.at(ManifestInstaller::InstallerSwitchType::Interactive) == "/interactive"); - REQUIRE(installer2Switches.at(ManifestInstaller::InstallerSwitchType::Language) == "/en-us"); - REQUIRE(installer2Switches.at(ManifestInstaller::InstallerSwitchType::Log) == "/log=<LOGPATH>"); - REQUIRE(installer2Switches.at(ManifestInstaller::InstallerSwitchType::InstallLocation) == "/dir=<INSTALLPATH>"); - REQUIRE(installer2Switches.at(ManifestInstaller::InstallerSwitchType::Update) == "/update"); + REQUIRE(installer2Switches.at(InstallerSwitchType::Custom) == "/custom"); + REQUIRE(installer2Switches.at(InstallerSwitchType::SilentWithProgress) == "/silentwithprogress"); + REQUIRE(installer2Switches.at(InstallerSwitchType::Silent) == "/silence"); + REQUIRE(installer2Switches.at(InstallerSwitchType::Interactive) == "/interactive"); + REQUIRE(installer2Switches.at(InstallerSwitchType::Language) == "/en-us"); + REQUIRE(installer2Switches.at(InstallerSwitchType::Log) == "/log=<LOGPATH>"); + REQUIRE(installer2Switches.at(InstallerSwitchType::InstallLocation) == "/dir=<INSTALLPATH>"); + REQUIRE(installer2Switches.at(InstallerSwitchType::Update) == "/update"); // Localization - REQUIRE(manifest.Localization.size() == 1); - ManifestLocalization localization1 = manifest.Localization.at(0); - REQUIRE(localization1.Language == "es-MX"); - REQUIRE(localization1.Description == "El proyecto MSIX SDK es habilita desarrolladores de diferentes"); - REQUIRE(localization1.Homepage == "https://github.com/microsoft/msix-packaging/es-MX"); - REQUIRE(localization1.LicenseUrl == "https://github.com/microsoft/msix-packaging/blob/master/LICENSE-es-MX"); + REQUIRE(manifest.Localizations.size() == 1); + ManifestLocalization localization1 = manifest.Localizations.at(0); + REQUIRE(localization1.Locale == "es-MX"); + REQUIRE(localization1.Get<Localization::Description>() == "El proyecto MSIX SDK es habilita desarrolladores de diferentes"); + REQUIRE(localization1.Get<Localization::PackageUrl>() == "https://github.com/microsoft/msix-packaging/es-MX"); + REQUIRE(localization1.Get<Localization::LicenseUrl>() == "https://github.com/microsoft/msix-packaging/blob/master/LICENSE-es-MX"); } TEST_CASE("ReadGoodManifestWithSpaces", "[ManifestValidation]") @@ -123,20 +126,20 @@ TEST_CASE("ReadGoodManifestWithSpaces", "[ManifestValidation]") Manifest manifest = YamlParser::CreateFromPath(TestDataFile("Manifest-Good-Spaces.yaml")); REQUIRE(manifest.Id == "microsoft.msixsdk"); - REQUIRE(manifest.Name == "MSIX SDK"); - REQUIRE(manifest.AppMoniker == "msixsdk"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PackageName>() == "MSIX SDK"); + REQUIRE(manifest.Moniker == "msixsdk"); REQUIRE(manifest.Version == "1.7.32"); REQUIRE(manifest.Channel == "release"); - REQUIRE(manifest.MinOSVersion == "0.0.0.0"); - REQUIRE(manifest.Tags == MultiValue{ "msix", "appx" }); - REQUIRE(manifest.Commands == MultiValue{ "makemsix", "makeappx" }); - REQUIRE(manifest.Protocols == MultiValue{ "protocol1", "protocol2" }); - REQUIRE(manifest.FileExtensions == MultiValue{ "appx", "appxbundle", "msix", "msixbundle" }); + REQUIRE(manifest.DefaultInstallerInfo.MinOSVersion == "0.0.0.0"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Tags>() == MultiValue{ "msix", "appx" }); + REQUIRE(manifest.DefaultInstallerInfo.Commands == MultiValue{ "makemsix", "makeappx" }); + REQUIRE(manifest.DefaultInstallerInfo.Protocols == MultiValue{ "protocol1", "protocol2" }); + REQUIRE(manifest.DefaultInstallerInfo.FileExtensions == MultiValue{ "appx", "appxbundle", "msix", "msixbundle" }); } struct ManifestExceptionMatcher : public Catch::MatcherBase<ManifestException> { - ManifestExceptionMatcher(std::string expectedMessage, bool expectedWarningOnly) : + ManifestExceptionMatcher(std::string expectedMessage, bool expectedWarningOnly = false) : m_expectedMessage(expectedMessage), m_expectedWarningOnly(expectedWarningOnly) {} // Performs the test for this matcher @@ -148,7 +151,7 @@ struct ManifestExceptionMatcher : public Catch::MatcherBase<ManifestException> virtual std::string describe() const override { std::ostringstream ss; - ss << std::boolalpha << "Expected exception message: " << m_expectedMessage << "Expected IsWarningOnly: " << m_expectedWarningOnly; + ss << std::boolalpha << "Expected exception message: " << m_expectedMessage << " Expected IsWarningOnly: " << m_expectedWarningOnly; return ss.str(); } @@ -204,17 +207,16 @@ TEST_CASE("ReadBadManifests", "[ManifestValidation]") ManifestTestCase TestCases[] = { { "Manifest-Bad-ArchInvalid.yaml", "Invalid field value. Field: Arch" }, - { "Manifest-Bad-ArchMissing.yaml", "Required field missing. Field: Arch" }, - { "Manifest-Bad-Channel-NotSupported.yaml", "Field is not supported. Field: Channel" }, + { "Manifest-Bad-ArchMissing.yaml", "Missing required property 'Arch'" }, { "Manifest-Bad-DifferentCase-camelCase.yaml", "All field names should be PascalCased. Field: installerType" }, { "Manifest-Bad-DifferentCase-lower.yaml", "All field names should be PascalCased. Field: installertype" }, { "Manifest-Bad-DifferentCase-UPPER.yaml", "All field names should be PascalCased. Field: INSTALLERTYPE" }, { "Manifest-Bad-DuplicateKey.yaml", "Duplicate field found in the manifest." }, { "Manifest-Bad-DuplicateKey-DifferentCase.yaml", "Duplicate field found in the manifest." }, { "Manifest-Bad-DuplicateKey-DifferentCase-lower.yaml", "Duplicate field found in the manifest." }, - { "Manifest-Bad-IdInvalid.yaml", "Invalid field value. Field: Id" }, - { "Manifest-Bad-IdMissing.yaml", "Required field missing. Field: Id" }, - { "Manifest-Bad-InstallersMissing.yaml", "Required field missing. Field: Installers" }, + { "Manifest-Bad-IdInvalid.yaml", "Failed to validate against schema associated with property name 'Id'" }, + { "Manifest-Bad-IdMissing.yaml", "Missing required property 'Id'" }, + { "Manifest-Bad-InstallersMissing.yaml", "Missing required property 'Installers'" }, { "Manifest-Bad-InstallerTypeExe-NoSilent.yaml", "Silent and SilentWithProgress switches are not specified for InstallerType exe.", true }, { "Manifest-Bad-InstallerTypeExe-NoSilentRoot.yaml", "Silent and SilentWithProgress switches are not specified for InstallerType exe.", true }, { "Manifest-Bad-InstallerTypeExeRoot-NoSilent.yaml", "Silent and SilentWithProgress switches are not specified for InstallerType exe.", true }, @@ -225,19 +227,19 @@ TEST_CASE("ReadBadManifests", "[ManifestValidation]") { "Manifest-Bad-InstallerUniqueness-DefaultScope.yaml", "Duplicate installer entry found." }, { "Manifest-Bad-InstallerUniqueness-DefaultValues.yaml", "Duplicate installer entry found." }, { "Manifest-Bad-InstallerUniqueness-SameLang.yaml", "Duplicate installer entry found." }, - { "Manifest-Bad-LicenseMissing.yaml", "Required field missing. Field: License" }, - { "Manifest-Bad-NameMissing.yaml", "Required field missing. Field: Name" }, - { "Manifest-Bad-PublisherMissing.yaml", "Required field missing. Field: Publisher" }, - { "Manifest-Bad-Sha256Invalid.yaml", "Invalid field value. Field: Sha256" }, + { "Manifest-Bad-LicenseMissing.yaml", "Missing required property 'License'" }, + { "Manifest-Bad-NameMissing.yaml", "Missing required property 'Name'" }, + { "Manifest-Bad-PublisherMissing.yaml", "Missing required property 'Publisher'" }, + { "Manifest-Bad-Sha256Invalid.yaml", "Failed to validate against schema associated with property name 'Sha256'" }, { "Manifest-Bad-Sha256Missing.yaml", "Required field missing. Field: Sha256" }, { "Manifest-Bad-SwitchInvalid.yaml", "Unknown field. Field: NotASwitch", true }, { "Manifest-Bad-UnknownProperty.yaml", "Unknown field. Field: Fake", true }, { "Manifest-Bad-UnsupportedVersion.yaml", "Unsupported ManifestVersion" }, { "Manifest-Bad-UrlInvalid.yaml", "Invalid field value. Field: Url" }, { "Manifest-Bad-UrlMissing.yaml", "Required field missing. Field: Url" }, - { "Manifest-Bad-VersionInvalid.yaml", "Invalid field value. Field: Version" }, - { "Manifest-Bad-VersionMissing.yaml", "Required field missing. Field: Version" }, - { "Manifest-Bad-InvalidManifestVersionValue.yaml", "Invalid field value. Field: ManifestVersion" }, + { "Manifest-Bad-VersionInvalid.yaml", "Failed to validate against schema associated with property name 'Version'" }, + { "Manifest-Bad-VersionMissing.yaml", "Missing required property 'Version'" }, + { "Manifest-Bad-InvalidManifestVersionValue.yaml", "Failed to validate against schema associated with property name 'ManifestVersion'" }, { "InstallFlowTest_MSStore.yaml", "Field value is not supported. Field: InstallerType Value: MSStore" }, { "Manifest-Bad-PackageFamilyNameOnMSI.yaml", "The specified installer type does not support PackageFamilyName. Field: InstallerType Value: Msi" }, { "Manifest-Bad-ProductCodeOnMSIX.yaml", "The specified installer type does not support ProductCode. Field: InstallerType Value: Msix" }, @@ -267,7 +269,7 @@ TEST_CASE("ManifestEncoding", "[ManifestValidation]") { INFO(testCase.TestFile); Manifest manifest = YamlParser::CreateFromPath(TestDataFile(testCase.TestFile), true, true); - REQUIRE(manifest.Name == u8"MSIX SDK\xA9"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PackageName>() == u8"MSIX SDK\xA9"); } } @@ -278,30 +280,30 @@ TEST_CASE("ComplexSystemReference", "[ManifestValidation]") REQUIRE(manifest.Installers.size() == 5); // Zip installer does not inherit - REQUIRE(manifest.Installers[0].InstallerType == ManifestInstaller::InstallerTypeEnum::Zip); + REQUIRE(manifest.Installers[0].InstallerType == InstallerTypeEnum::Zip); REQUIRE(manifest.Installers[0].PackageFamilyName == ""); REQUIRE(manifest.Installers[0].ProductCode == ""); // MSIX installer does inherit - REQUIRE(manifest.Installers[1].InstallerType == ManifestInstaller::InstallerTypeEnum::Msix); + REQUIRE(manifest.Installers[1].InstallerType == InstallerTypeEnum::Msix); REQUIRE(manifest.Installers[1].Arch == Architecture::X86); REQUIRE(manifest.Installers[1].PackageFamilyName == "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe"); REQUIRE(manifest.Installers[1].ProductCode == ""); // MSI installer does inherit - REQUIRE(manifest.Installers[2].InstallerType == ManifestInstaller::InstallerTypeEnum::Msi); + REQUIRE(manifest.Installers[2].InstallerType == InstallerTypeEnum::Msi); REQUIRE(manifest.Installers[2].Arch == Architecture::X86); REQUIRE(manifest.Installers[2].PackageFamilyName == ""); REQUIRE(manifest.Installers[2].ProductCode == "{Foo}"); // MSIX installer with override - REQUIRE(manifest.Installers[3].InstallerType == ManifestInstaller::InstallerTypeEnum::Msix); + REQUIRE(manifest.Installers[3].InstallerType == InstallerTypeEnum::Msix); REQUIRE(manifest.Installers[3].Arch == Architecture::X64); REQUIRE(manifest.Installers[3].PackageFamilyName == "Override_8wekyb3d8bbwe"); REQUIRE(manifest.Installers[3].ProductCode == ""); // MSI installer with override - REQUIRE(manifest.Installers[4].InstallerType == ManifestInstaller::InstallerTypeEnum::Msi); + REQUIRE(manifest.Installers[4].InstallerType == InstallerTypeEnum::Msi); REQUIRE(manifest.Installers[4].Arch == Architecture::X64); REQUIRE(manifest.Installers[4].PackageFamilyName == ""); REQUIRE(manifest.Installers[4].ProductCode == "Override"); @@ -318,3 +320,256 @@ TEST_CASE("ManifestVersionExtensions", "[ManifestValidation]") REQUIRE(ManifestVer("1.0.0-other-msstore.2"sv).HasExtension("msstore")); 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) +{ + 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" }); + REQUIRE(manifest.Channel == "release"); + 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.InstallerType == InstallerTypeEnum::Zip); + REQUIRE(manifest.DefaultInstallerInfo.Scope == ScopeEnum::Machine); + REQUIRE(manifest.DefaultInstallerInfo.InstallModes == std::vector<ScopeEnum>{ ScopeEnum::User ,ScopeEnum::Machine }); + + 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<int>{ 1, static_cast<int>(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.WindowsFeatures == MultiValue{ "IIS" }); + REQUIRE(dependencies.WindowsLibraries == MultiValue{ "VC Runtime" }); + REQUIRE(dependencies.PackageDependencies.size() == 1); + REQUIRE(dependencies.PackageDependencies[0].Id == "Microsoft.MsixSdkDep"); + REQUIRE(dependencies.PackageDependencies[0].MinVersion == "1.0.0"); + REQUIRE(dependencies.ExternalDependencies == MultiValue{ "Outside dependencies" }); + + 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 (isSingleton) + { + REQUIRE(manifest.Installers.size() == 1); + } + 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.InstallerType == 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<ScopeEnum>{ ScopeEnum::User }); + + 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.WindowsFeatures == MultiValue{ "PreviewIIS" }); + REQUIRE(installer1Dependencies.WindowsLibraries == MultiValue{ "Preview VC Runtime" }); + REQUIRE(installer1Dependencies.PackageDependencies.size() == 1); + REQUIRE(installer1Dependencies.PackageDependencies[0].Id == "Microsoft.MsixSdkDepPreview"); + REQUIRE(installer1Dependencies.ExternalDependencies == MultiValue{ "Preview Outside dependencies" }); + + REQUIRE(installer1.Capabilities == MultiValue{ "internetClientPreview" }); + REQUIRE(installer1.RestrictedCapabilities == MultiValue{ "runFullTrustPreview" }); + REQUIRE(installer1.PackageFamilyName == "Microsoft.DesktopAppInstallerPreview_8wekyb3d8bbwe"); + + if (!isSingleton) + { + ManifestInstaller installer2 = manifest.Installers.at(1); + REQUIRE(installer2.Arch == Architecture::X64); + REQUIRE(installer2.InstallerType == InstallerTypeEnum::Exe); + REQUIRE(installer2.Url == "https://www.microsoft.com/msixsdk/msixsdkx64.exe"); + REQUIRE(installer2.Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); + REQUIRE(installer2.ProductCode == "{Bar}"); + + // 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" }); + } +} + +TEST_CASE("ValidateV1GoodManifestAndVerifyContents", "[ManifestValidation]") +{ + TempDirectory singletonDirectory{ "SingletonManifest" }; + CopyTestDataFilesToFolder({ "ManifestV1-Singleton.yaml" }, singletonDirectory); + Manifest singletonManifest = YamlParser::CreateFromPath(singletonDirectory, true, true); + VerifyV1ManifestContent(singletonManifest, true); + + TempDirectory multiFileDirectory{ "MultiFileManifest" }; + CopyTestDataFilesToFolder({ + "ManifestV1-MultiFile-Version.yaml", + "ManifestV1-MultiFile-Installer.yaml", + "ManifestV1-MultiFile-DefaultLocale.yaml", + "ManifestV1-MultiFile-Locale.yaml" }, multiFileDirectory); + + TempFile mergedManifestFile{ "merged.yaml" }; + Manifest multiFileManifest = YamlParser::CreateFromPath(multiFileDirectory, true, true, mergedManifestFile); + VerifyV1ManifestContent(multiFileManifest, false); + + // Read from merged manifest should have the same content as multi file manifest + Manifest mergedManifest = YamlParser::CreateFromPath(mergedManifestFile); + VerifyV1ManifestContent(mergedManifest, false); +} + +YamlManifestInfo CreateYamlManifestInfo(std::string testDataFile) +{ + YamlManifestInfo result; + result.Root = AppInstaller::YAML::Load(TestDataFile(testDataFile)); + result.FileName = testDataFile; + return result; +} + +TEST_CASE("MultifileManifestInputValidation", "[ManifestValidation]") +{ + auto previewManifest = CreateYamlManifestInfo("Manifest-Good.yaml"); + auto v1SingletonManifest = CreateYamlManifestInfo("ManifestV1-Singleton.yaml"); + auto v1VersionManifest = CreateYamlManifestInfo("ManifestV1-MultiFile-Version.yaml"); + auto v1InstallerManifest = CreateYamlManifestInfo("ManifestV1-MultiFile-Installer.yaml"); + auto v1DefaultLocaleManifest = CreateYamlManifestInfo("ManifestV1-MultiFile-DefaultLocale.yaml"); + auto v1LocaleManifest = CreateYamlManifestInfo("ManifestV1-MultiFile-Locale.yaml"); + + { + // Preview and multi file manifest together + std::vector<YamlManifestInfo> input = { previewManifest, v1VersionManifest, v1InstallerManifest, v1DefaultLocaleManifest }; + REQUIRE_THROWS_MATCHES(YamlParser::ParseManifest(input), ManifestException, ManifestExceptionMatcher("Preview manifest does not support multi file manifest format")); + } + + { + // Singleton and multi file manifest together + std::vector<YamlManifestInfo> input = { v1SingletonManifest, v1VersionManifest, v1InstallerManifest, v1DefaultLocaleManifest }; + REQUIRE_THROWS_MATCHES(YamlParser::ParseManifest(input), ManifestException, ManifestExceptionMatcher("The multi file manifest should not contain file with the particular ManifestType. Field: ManifestType Value: singleton")); + } + + { + // More than 1 version manifest + std::vector<YamlManifestInfo> input = { v1VersionManifest, v1VersionManifest, v1InstallerManifest, v1DefaultLocaleManifest }; + REQUIRE_THROWS_MATCHES(YamlParser::ParseManifest(input), ManifestException, ManifestExceptionMatcher("The multi file manifest should contain only one file with the particular ManifestType. Field: ManifestType Value: version")); + } + + { + // More than 1 installer manifest + std::vector<YamlManifestInfo> input = { v1VersionManifest, v1InstallerManifest, v1InstallerManifest, v1DefaultLocaleManifest }; + REQUIRE_THROWS_MATCHES(YamlParser::ParseManifest(input), ManifestException, ManifestExceptionMatcher("The multi file manifest should contain only one file with the particular ManifestType. Field: ManifestType Value: installer")); + } + + { + // More than 1 default locale manifest + std::vector<YamlManifestInfo> input = { v1VersionManifest, v1InstallerManifest, v1DefaultLocaleManifest, v1DefaultLocaleManifest }; + REQUIRE_THROWS_MATCHES(YamlParser::ParseManifest(input), ManifestException, ManifestExceptionMatcher("The multi file manifest should contain only one file with the particular ManifestType. Field: ManifestType Value: defaultLocale")); + } + + { + // Duplicate locales + std::vector<YamlManifestInfo> input = { v1VersionManifest, v1InstallerManifest, v1DefaultLocaleManifest, v1LocaleManifest, v1LocaleManifest }; + REQUIRE_THROWS_MATCHES(YamlParser::ParseManifest(input), ManifestException, ManifestExceptionMatcher("The multi file manifest contains duplicate PackageLocale. Field: PackageLocale Value: en-GB")); + } + + { + // default locale not match + auto defaultLocaleManifestCopy = v1DefaultLocaleManifest; + defaultLocaleManifestCopy.Root["PackageLocale"].SetScalar("fr-fr"); + std::vector<YamlManifestInfo> input = { v1VersionManifest, v1InstallerManifest, defaultLocaleManifestCopy, v1LocaleManifest }; + REQUIRE_THROWS_MATCHES(YamlParser::ParseManifest(input), ManifestException, ManifestExceptionMatcher("DefaultLocale value in version manifest does not match PackageLocale value in defaultLocale manifest")); + } + + { + // Package Id does not match + auto installerManifestCopy = v1InstallerManifest; + installerManifestCopy.Root["PackageIdentifier"].SetScalar("Another.Identifier"); + std::vector<YamlManifestInfo> input = { v1VersionManifest, installerManifestCopy, v1DefaultLocaleManifest, v1LocaleManifest }; + REQUIRE_THROWS_MATCHES(YamlParser::ParseManifest(input), ManifestException, ManifestExceptionMatcher("The multi file manifest has inconsistent field values. Field: PackageIdentifier Value: Another.Identifier")); + } + + { + // Package Version does not match + auto installerManifestCopy = v1InstallerManifest; + installerManifestCopy.Root["PackageVersion"].SetScalar("Another.Version"); + std::vector<YamlManifestInfo> input = { v1VersionManifest, installerManifestCopy, v1DefaultLocaleManifest, v1LocaleManifest }; + REQUIRE_THROWS_MATCHES(YamlParser::ParseManifest(input), ManifestException, ManifestExceptionMatcher("The multi file manifest has inconsistent field values. Field: PackageVersion Value: Another.Version")); + } + + { + // Incomplete multi file manifest, missing installer + std::vector<YamlManifestInfo> input = { v1VersionManifest, v1DefaultLocaleManifest }; + REQUIRE_THROWS_MATCHES(YamlParser::ParseManifest(input), ManifestException, ManifestExceptionMatcher("The multi file manifest is incomplete")); + } + + { + // Incomplete multi file manifest, missing default locale + std::vector<YamlManifestInfo> input = { v1VersionManifest, v1InstallerManifest }; + REQUIRE_THROWS_MATCHES(YamlParser::ParseManifest(input), ManifestException, ManifestExceptionMatcher("The multi file manifest is incomplete")); + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -90,7 +90,10 @@ <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" /> <ImportGroup Label="ExtensionSettings"> </ImportGroup> - <ImportGroup Label="Shared" /> + <ImportGroup Label="Shared"> + <Import Project="..\Valijson\Valijson.vcxitems" Label="Shared" /> + <Import Project="..\ManifestSchema\ManifestSchema.vcxitems" Label="Shared" /> + </ImportGroup> <ImportGroup Label="PropertySheets"> <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> </ImportGroup> @@ -160,9 +163,9 @@ <ClCompile> <Optimization>Disabled</Optimization> <PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">true</TreatWarningAsError> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">true</TreatWarningAsError> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</TreatWarningAsError> @@ -177,7 +180,7 @@ <ItemDefinitionGroup Condition="'$(Platform)'=='Win32'"> <ClCompile> <PreprocessorDefinitions>WIN32;%(PreprocessorDefinitions);CLICOREDLLBUILD</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</TreatWarningAsError> </ClCompile> <Link> @@ -190,10 +193,10 @@ <FunctionLevelLinking>true</FunctionLevelLinking> <IntrinsicFunctions>true</IntrinsicFunctions> <PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">true</TreatWarningAsError> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">true</TreatWarningAsError> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</TreatWarningAsError> @@ -215,10 +218,10 @@ <FunctionLevelLinking>true</FunctionLevelLinking> <IntrinsicFunctions>true</IntrinsicFunctions> <PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD;WINGET_DISABLE_FOR_FUZZING</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Fuzzing|ARM'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Fuzzing|ARM64'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Fuzzing|Win32'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Fuzzing|x64'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Fuzzing|ARM'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Fuzzing|ARM64'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Fuzzing|Win32'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Fuzzing|x64'">$(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Fuzzing|ARM'">true</TreatWarningAsError> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Fuzzing|ARM64'">true</TreatWarningAsError> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Fuzzing|Win32'">true</TreatWarningAsError> @@ -268,11 +271,14 @@ <ClInclude Include="Public\winget\Manifest.h" /> <ClInclude Include="Public\winget\ManifestInstaller.h" /> <ClInclude Include="Public\winget\ManifestLocalization.h" /> + <ClInclude Include="Public\winget\ManifestCommon.h" /> <ClInclude Include="Public\winget\ManifestValidation.h" /> <ClInclude Include="Public\winget\ManifestYamlParser.h" /> + <ClInclude Include="Public\winget\ManifestYamlPopulator.h" /> <ClInclude Include="Public\winget\NameNormalization.h" /> <ClInclude Include="Public\winget\Regex.h" /> <ClInclude Include="Public\winget\Registry.h" /> + <ClInclude Include="Public\winget\ManifestSchemaValidation.h" /> <ClInclude Include="Public\winget\Settings.h" /> <ClInclude Include="Public\winget\UserSettings.h" /> <ClInclude Include="Public\winget\Yaml.h" /> @@ -306,8 +312,10 @@ </ClCompile> <ClCompile Include="JsonUtil.cpp" /> <ClCompile Include="Manifest\Manifest.cpp" /> - <ClCompile Include="Manifest\ManifestInstaller.cpp" /> + <ClCompile Include="Manifest\ManifestCommon.cpp" /> <ClCompile Include="Manifest\ManifestValidation.cpp" /> + <ClCompile Include="Manifest\ManifestSchemaValidation.cpp" /> + <ClCompile Include="Manifest\ManifestYamlPopulator.cpp" /> <ClCompile Include="Manifest\YamlParser.cpp" /> <ClCompile Include="MsixInfo.cpp"> <ExcludedFromBuild Condition="'$(Configuration)'=='Fuzzing'">true</ExcludedFromBuild> diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -123,9 +123,6 @@ <ClInclude Include="Public\winget\Yaml.h"> <Filter>Public\winget</Filter> </ClInclude> - <ClInclude Include="Public\winget\Manifest.h"> - <Filter>Public\winget</Filter> - </ClInclude> <ClInclude Include="Public\winget\ManifestInstaller.h"> <Filter>Public\winget</Filter> </ClInclude> @@ -147,6 +144,18 @@ <ClInclude Include="Public\winget\Regex.h"> <Filter>Public\winget</Filter> </ClInclude> + <ClInclude Include="Public\winget\ManifestSchemaValidation.h"> + <Filter>Public\winget</Filter> + </ClInclude> + <ClInclude Include="Public\winget\Manifest.h"> + <Filter>Public\winget</Filter> + </ClInclude> + <ClInclude Include="Public\winget\ManifestYamlPopulator.h"> + <Filter>Public\winget</Filter> + </ClInclude> + <ClInclude Include="Public\winget\ManifestCommon.h"> + <Filter>Public\winget</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -227,12 +236,6 @@ <ClCompile Include="Yaml.cpp"> <Filter>Source Files</Filter> </ClCompile> - <ClCompile Include="Manifest\Manifest.cpp"> - <Filter>Manifest</Filter> - </ClCompile> - <ClCompile Include="Manifest\ManifestInstaller.cpp"> - <Filter>Manifest</Filter> - </ClCompile> <ClCompile Include="Manifest\ManifestValidation.cpp"> <Filter>Manifest</Filter> </ClCompile> @@ -248,6 +251,18 @@ <ClCompile Include="Regex.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="Manifest\ManifestSchemaValidation.cpp"> + <Filter>Manifest</Filter> + </ClCompile> + <ClCompile Include="Manifest\ManifestCommon.cpp"> + <Filter>Manifest</Filter> + </ClCompile> + <ClCompile Include="Manifest\ManifestYamlPopulator.cpp"> + <Filter>Manifest</Filter> + </ClCompile> + <ClCompile Include="Manifest\Manifest.cpp"> + <Filter>Manifest</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCommonCore/AppInstallerStrings.cpp b/src/AppInstallerCommonCore/AppInstallerStrings.cpp @@ -427,6 +427,13 @@ namespace AppInstaller::Utility return str; } + std::string Trim(std::string&& str) + { + std::string result = std::move(str); + Utility::Trim(result); + return result; + } + std::string ReadEntireStream(std::istream& stream) { std::streampos currentPos = stream.tellg(); diff --git a/src/AppInstallerCommonCore/Manifest/Manifest.cpp b/src/AppInstallerCommonCore/Manifest/Manifest.cpp @@ -2,87 +2,49 @@ // Licensed under the MIT License. #include "pch.h" #include "winget/Manifest.h" -#include "winget/ManifestValidation.h" namespace AppInstaller::Manifest { - ManifestVer::ManifestVer(std::string_view version) + void Manifest::ApplyLocale(const std::string&) { - bool validationSuccess = true; - - // Separate the extensions out - size_t hyphenPos = version.find_first_of('-'); - if (hyphenPos != std::string_view::npos) - { - // The first part is the main version - Assign(std::string{ version.substr(0, hyphenPos) }, "."); - - // The second part is the extensions - hyphenPos += 1; - while (hyphenPos < version.length()) - { - size_t newPos = version.find_first_of('-', hyphenPos); - - size_t length = (newPos == std::string::npos ? version.length() : newPos) - hyphenPos; - m_extensions.emplace_back(std::string{ version.substr(hyphenPos, length) }, "."); + // TODO: need more work in locale processing + CurrentLocalization = DefaultLocalization; + } - hyphenPos += length + 1; - } - } - else - { - Assign(std::string{ version }, "."); - } + std::vector<string_t> Manifest::GetAggregatedTags() const + { + std::vector<string_t> resultTags = DefaultLocalization.Get<Localization::Tags>(); - if (m_parts.size() > 3) - { - validationSuccess = false; - } - else + for (const auto& locale : Localizations) { - for (size_t i = 0; i < m_parts.size(); i++) - { - if (!m_parts[i].Other.empty()) - { - validationSuccess = false; - break; - } - } - - for (const Version& ext : m_extensions) + auto tags = locale.Get<Localization::Tags>(); + for (const auto& tag : tags) { - if (ext.GetParts().empty() || ext.GetParts()[0].Integer != 0) + if (std::find(resultTags.begin(), resultTags.end(), tag) == resultTags.end()) { - validationSuccess = false; - break; + resultTags.emplace_back(tag); } } } - if (!validationSuccess) - { - std::vector<ValidationError> errors; - errors.emplace_back(ManifestError::InvalidFieldValue, "ManifestVersion", std::string{ version }); - THROW_EXCEPTION(ManifestException(std::move(errors))); - } + return resultTags; } - bool ManifestVer::HasExtension() const + std::vector<string_t> Manifest::GetAggregatedCommands() const { - return !m_extensions.empty(); - } + std::vector<string_t> resultCommands; - bool ManifestVer::HasExtension(std::string_view extension) const - { - for (const Version& ext : m_extensions) + for (const auto& installer : Installers) { - const auto& parts = ext.GetParts(); - if (!parts.empty() && parts[0].Integer == 0 && parts[0].Other == extension) + for (const auto& command : installer.Commands) { - return true; + if (std::find(resultCommands.begin(), resultCommands.end(), command) == resultCommands.end()) + { + resultCommands.emplace_back(command); + } } } - return false; + return resultCommands; } -} +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp @@ -0,0 +1,362 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "winget/ManifestCommon.h" +#include "winget/ManifestValidation.h" + +namespace AppInstaller::Manifest +{ + namespace + { + enum class CompatibilitySet + { + None, + Exe, + Msi, + Msix, + }; + + CompatibilitySet GetCompatibilitySet(InstallerTypeEnum type) + { + switch (type) + { + case InstallerTypeEnum::Inno: + case InstallerTypeEnum::Nullsoft: + case InstallerTypeEnum::Exe: + case InstallerTypeEnum::Burn: + return CompatibilitySet::Exe; + case InstallerTypeEnum::Wix: + case InstallerTypeEnum::Msi: + return CompatibilitySet::Msi; + case InstallerTypeEnum::Msix: + case InstallerTypeEnum::MSStore: + return CompatibilitySet::Msix; + default: + return CompatibilitySet::None; + } + } + } + + ManifestVer::ManifestVer(std::string_view version) + { + bool validationSuccess = true; + + // Separate the extensions out + size_t hyphenPos = version.find_first_of('-'); + if (hyphenPos != std::string_view::npos) + { + // The first part is the main version + Assign(std::string{ version.substr(0, hyphenPos) }, "."); + + // The second part is the extensions + hyphenPos += 1; + while (hyphenPos < version.length()) + { + size_t newPos = version.find_first_of('-', hyphenPos); + + size_t length = (newPos == std::string::npos ? version.length() : newPos) - hyphenPos; + m_extensions.emplace_back(std::string{ version.substr(hyphenPos, length) }, "."); + + hyphenPos += length + 1; + } + } + else + { + Assign(std::string{ version }, "."); + } + + if (m_parts.size() > 3) + { + validationSuccess = false; + } + else + { + for (size_t i = 0; i < m_parts.size(); i++) + { + if (!m_parts[i].Other.empty()) + { + validationSuccess = false; + break; + } + } + + for (const Version& ext : m_extensions) + { + if (ext.GetParts().empty() || ext.GetParts()[0].Integer != 0) + { + validationSuccess = false; + break; + } + } + } + + if (!validationSuccess) + { + std::vector<ValidationError> errors; + errors.emplace_back(ManifestError::InvalidFieldValue, "ManifestVersion", std::string{ version }); + THROW_EXCEPTION(ManifestException(std::move(errors))); + } + } + + bool ManifestVer::HasExtension() const + { + return !m_extensions.empty(); + } + + bool ManifestVer::HasExtension(std::string_view extension) const + { + for (const Version& ext : m_extensions) + { + const auto& parts = ext.GetParts(); + if (!parts.empty() && parts[0].Integer == 0 && parts[0].Other == extension) + { + return true; + } + } + + return false; + } + + InstallerTypeEnum ConvertToInstallerTypeEnum(const std::string& in) + { + std::string inStrLower = Utility::ToLower(in); + InstallerTypeEnum result = InstallerTypeEnum::Unknown; + + if (inStrLower == "inno") + { + result = InstallerTypeEnum::Inno; + } + else if (inStrLower == "wix") + { + result = InstallerTypeEnum::Wix; + } + else if (inStrLower == "msi") + { + result = InstallerTypeEnum::Msi; + } + else if (inStrLower == "nullsoft") + { + result = InstallerTypeEnum::Nullsoft; + } + else if (inStrLower == "zip") + { + result = InstallerTypeEnum::Zip; + } + else if (inStrLower == "appx" || inStrLower == "msix") + { + result = InstallerTypeEnum::Msix; + } + else if (inStrLower == "exe") + { + result = InstallerTypeEnum::Exe; + } + else if (inStrLower == "burn") + { + result = InstallerTypeEnum::Burn; + } + else if (inStrLower == "msstore") + { + result = InstallerTypeEnum::MSStore; + } + + return result; + } + + UpdateBehaviorEnum ConvertToUpdateBehaviorEnum(const std::string& in) + { + UpdateBehaviorEnum result = UpdateBehaviorEnum::Unknown; + + if (Utility::CaseInsensitiveEquals(in, "install")) + { + result = UpdateBehaviorEnum::Install; + } + else if (Utility::CaseInsensitiveEquals(in, "uninstallprevious")) + { + result = UpdateBehaviorEnum::UninstallPrevious; + } + + return result; + } + + ScopeEnum ConvertToScopeEnum(const std::string& in) + { + ScopeEnum result = ScopeEnum::Unknown; + + if (Utility::CaseInsensitiveEquals(in, "user")) + { + result = ScopeEnum::User; + } + else if (Utility::CaseInsensitiveEquals(in, "machine")) + { + result = ScopeEnum::Machine; + } + + return result; + } + + PlatformEnum ConvertToPlatformEnum(const std::string& in) + { + PlatformEnum result = PlatformEnum::Unknown; + + if (Utility::CaseInsensitiveEquals(in, "windows.desktop")) + { + result = PlatformEnum::Desktop; + } + else if (Utility::CaseInsensitiveEquals(in, "windows.universal")) + { + result = PlatformEnum::Universal; + } + + return result; + } + + ManifestTypeEnum ConvertToManifestTypeEnum(const std::string& in) + { + if (in == "singleton") + { + return ManifestTypeEnum::Singleton; + } + else if (in == "version") + { + return ManifestTypeEnum::Version; + } + else if (in == "installer") + { + return ManifestTypeEnum::Installer; + } + else if (in == "defaultLocale") + { + return ManifestTypeEnum::DefaultLocale; + } + else if (in == "locale") + { + return ManifestTypeEnum::Locale; + } + else if (in == "merged") + { + return ManifestTypeEnum::Merged; + } + else + { + THROW_HR_MSG(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), "Unsupported ManifestType: %s", in.c_str()); + } + } + + std::string_view InstallerTypeToString(InstallerTypeEnum installerType) + { + switch (installerType) + { + case InstallerTypeEnum::Exe: + return "Exe"sv; + case InstallerTypeEnum::Inno: + return "Inno"sv; + case InstallerTypeEnum::Msi: + return "Msi"sv; + case InstallerTypeEnum::Msix: + return "Msix"sv; + case InstallerTypeEnum::Nullsoft: + return "Nullsoft"sv; + case InstallerTypeEnum::Wix: + return "Wix"sv; + case InstallerTypeEnum::Zip: + return "Zip"sv; + case InstallerTypeEnum::Burn: + return "Burn"sv; + case InstallerTypeEnum::MSStore: + return "MSStore"sv; + } + + return "Unknown"sv; + } + + std::string_view ScopeToString(ScopeEnum scope) + { + switch (scope) + { + case ScopeEnum::User: + return "User"sv; + case ScopeEnum::Machine: + return "Machine"sv; + } + + return "Unknown"sv; + } + + bool DoesInstallerTypeUsePackageFamilyName(InstallerTypeEnum installerType) + { + return (installerType == InstallerTypeEnum::Msix || installerType == InstallerTypeEnum::MSStore); + } + + bool DoesInstallerTypeUseProductCode(InstallerTypeEnum installerType) + { + return ( + installerType == InstallerTypeEnum::Exe || + installerType == InstallerTypeEnum::Inno || + installerType == InstallerTypeEnum::Msi || + installerType == InstallerTypeEnum::Nullsoft || + installerType == InstallerTypeEnum::Wix || + installerType == InstallerTypeEnum::Burn + ); + } + + bool IsInstallerTypeCompatible(InstallerTypeEnum type1, InstallerTypeEnum type2) + { + // Unknown type cannot be compatible with any other + if (type1 == InstallerTypeEnum::Unknown || type2 == InstallerTypeEnum::Unknown) + { + return false; + } + + // Not unknown, so must be compatible + if (type1 == type2) + { + return true; + } + + CompatibilitySet set1 = GetCompatibilitySet(type1); + CompatibilitySet set2 = GetCompatibilitySet(type2); + + // If either is none, they can't be compatible + if (set1 == CompatibilitySet::None || set2 == CompatibilitySet::None) + { + return false; + } + + return set1 == set2; + } + + std::map<InstallerSwitchType, Utility::NormalizedString> GetDefaultKnownSwitches(InstallerTypeEnum installerType) + { + switch (installerType) + { + case InstallerTypeEnum::Burn: + case InstallerTypeEnum::Wix: + case InstallerTypeEnum::Msi: + return + { + {InstallerSwitchType::Silent, ManifestInstaller::string_t("/quiet")}, + {InstallerSwitchType::SilentWithProgress, ManifestInstaller::string_t("/passive")}, + {InstallerSwitchType::Log, ManifestInstaller::string_t("/log \"" + std::string(ARG_TOKEN_LOGPATH) + "\"")}, + {InstallerSwitchType::InstallLocation, ManifestInstaller::string_t("TARGETDIR=\"" + std::string(ARG_TOKEN_INSTALLPATH) + "\"")}, + {InstallerSwitchType::Update, ManifestInstaller::string_t("REINSTALL=ALL REINSTALLMODE=vamus")} + }; + case InstallerTypeEnum::Nullsoft: + return + { + {InstallerSwitchType::Silent, ManifestInstaller::string_t("/S")}, + {InstallerSwitchType::SilentWithProgress, ManifestInstaller::string_t("/S")}, + {InstallerSwitchType::InstallLocation, ManifestInstaller::string_t("/D=" + std::string(ARG_TOKEN_INSTALLPATH))} + }; + case InstallerTypeEnum::Inno: + return + { + {InstallerSwitchType::Silent, ManifestInstaller::string_t("/VERYSILENT")}, + {InstallerSwitchType::SilentWithProgress, ManifestInstaller::string_t("/SILENT")}, + {InstallerSwitchType::Log, ManifestInstaller::string_t("/LOG=\"" + std::string(ARG_TOKEN_LOGPATH) + "\"")}, + {InstallerSwitchType::InstallLocation, ManifestInstaller::string_t("/DIR=\"" + std::string(ARG_TOKEN_INSTALLPATH) + "\"")} + }; + default: + return {}; + } + } +} diff --git a/src/AppInstallerCommonCore/Manifest/ManifestInstaller.cpp b/src/AppInstallerCommonCore/Manifest/ManifestInstaller.cpp @@ -1,198 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#include "pch.h" -#include "winget/ManifestInstaller.h" - -namespace AppInstaller::Manifest -{ - namespace - { - enum class CompatibilitySet - { - None, - Exe, - Msi, - Msix, - }; - - CompatibilitySet GetCompatibilitySet(ManifestInstaller::InstallerTypeEnum type) - { - switch (type) - { - case ManifestInstaller::InstallerTypeEnum::Inno: - case ManifestInstaller::InstallerTypeEnum::Nullsoft: - case ManifestInstaller::InstallerTypeEnum::Exe: - case ManifestInstaller::InstallerTypeEnum::Burn: - return CompatibilitySet::Exe; - case ManifestInstaller::InstallerTypeEnum::Wix: - case ManifestInstaller::InstallerTypeEnum::Msi: - return CompatibilitySet::Msi; - case ManifestInstaller::InstallerTypeEnum::Msix: - case ManifestInstaller::InstallerTypeEnum::MSStore: - return CompatibilitySet::Msix; - default: - return CompatibilitySet::None; - } - } - } - - ManifestInstaller::InstallerTypeEnum ManifestInstaller::ConvertToInstallerTypeEnum(const std::string& in) - { - std::string inStrLower = Utility::ToLower(in); - InstallerTypeEnum result = InstallerTypeEnum::Unknown; - - if (inStrLower == "inno") - { - result = InstallerTypeEnum::Inno; - } - else if (inStrLower == "wix") - { - result = InstallerTypeEnum::Wix; - } - else if (inStrLower == "msi") - { - result = InstallerTypeEnum::Msi; - } - else if (inStrLower == "nullsoft") - { - result = InstallerTypeEnum::Nullsoft; - } - else if (inStrLower == "zip") - { - result = InstallerTypeEnum::Zip; - } - else if (inStrLower == "appx" || inStrLower == "msix") - { - result = InstallerTypeEnum::Msix; - } - else if (inStrLower == "exe") - { - result = InstallerTypeEnum::Exe; - } - else if (inStrLower == "burn") - { - result = InstallerTypeEnum::Burn; - } - else if (inStrLower == "msstore") - { - result = InstallerTypeEnum::MSStore; - } - - return result; - } - - ManifestInstaller::UpdateBehaviorEnum ManifestInstaller::ConvertToUpdateBehaviorEnum(const std::string& in) - { - UpdateBehaviorEnum result = UpdateBehaviorEnum::Unknown; - - if (Utility::CaseInsensitiveEquals(in, "install")) - { - result = UpdateBehaviorEnum::Install; - } - else if (Utility::CaseInsensitiveEquals(in, "uninstallprevious")) - { - result = UpdateBehaviorEnum::UninstallPrevious; - } - - return result; - } - - ManifestInstaller::ScopeEnum ManifestInstaller::ConvertToScopeEnum(const std::string& in) - { - ScopeEnum result = ScopeEnum::Unknown; - - if (Utility::CaseInsensitiveEquals(in, "user")) - { - result = ScopeEnum::User; - } - else if (Utility::CaseInsensitiveEquals(in, "machine")) - { - result = ScopeEnum::Machine; - } - - return result; - } - - std::string_view ManifestInstaller::InstallerTypeToString(ManifestInstaller::InstallerTypeEnum installerType) - { - switch (installerType) - { - case InstallerTypeEnum::Exe: - return "Exe"sv; - case InstallerTypeEnum::Inno: - return "Inno"sv; - case InstallerTypeEnum::Msi: - return "Msi"sv; - case InstallerTypeEnum::Msix: - return "Msix"sv; - case InstallerTypeEnum::Nullsoft: - return "Nullsoft"sv; - case InstallerTypeEnum::Wix: - return "Wix"sv; - case InstallerTypeEnum::Zip: - return "Zip"sv; - case InstallerTypeEnum::Burn: - return "Burn"sv; - case InstallerTypeEnum::MSStore: - return "MSStore"sv; - } - - return "Unknown"sv; - } - - std::string_view ManifestInstaller::ScopeToString(ScopeEnum scope) - { - switch (scope) - { - case ScopeEnum::User: - return "User"sv; - case ScopeEnum::Machine: - return "Machine"sv; - } - - return "Unknown"sv; - } - - bool ManifestInstaller::DoesInstallerTypeUsePackageFamilyName(InstallerTypeEnum installerType) - { - return (installerType == InstallerTypeEnum::Msix || installerType == InstallerTypeEnum::MSStore); - } - - bool ManifestInstaller::DoesInstallerTypeUseProductCode(InstallerTypeEnum installerType) - { - return ( - installerType == InstallerTypeEnum::Exe || - installerType == InstallerTypeEnum::Inno || - installerType == InstallerTypeEnum::Msi || - installerType == InstallerTypeEnum::Nullsoft || - installerType == InstallerTypeEnum::Wix || - installerType == InstallerTypeEnum::Burn - ); - } - - bool ManifestInstaller::IsInstallerTypeCompatible(InstallerTypeEnum type1, InstallerTypeEnum type2) - { - // Unknown type cannot be compatible with any other - if (type1 == InstallerTypeEnum::Unknown || type2 == InstallerTypeEnum::Unknown) - { - return false; - } - - // Not unknown, so must be compatible - if (type1 == type2) - { - return true; - } - - CompatibilitySet set1 = GetCompatibilitySet(type1); - CompatibilitySet set2 = GetCompatibilitySet(type2); - - // If either is none, they can't be compatible - if (set1 == CompatibilitySet::None || set2 == CompatibilitySet::None) - { - return false; - } - - return set1 == set2; - } -} diff --git a/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp @@ -0,0 +1,211 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "winget/Yaml.h" +#include "winget/ManifestCommon.h" +#include "winget/ManifestSchemaValidation.h" +#include "winget/ManifestYamlParser.h" + +#include <ManifestSchema.h> + +namespace AppInstaller::Manifest::YamlParser +{ + using namespace std::string_view_literals; + + namespace + { + enum class YamlScalarType + { + String, + Int + }; + + // List of fields that use non string scalar types + const std::map<std::string_view, YamlScalarType> ManifestFieldTypes= + { + { "InstallerSuccessCodes"sv, YamlScalarType::Int } + }; + + YamlScalarType GetManifestScalarValueType(const std::string& key) + { + auto iter = ManifestFieldTypes.find(key); + if (iter != ManifestFieldTypes.end()) + { + return iter->second; + } + + return YamlScalarType::String; + } + + Json::Value YamlScalarNodeToJson(const YAML::Node& scalarNode, YamlScalarType scalarType) + { + if (scalarType == YamlScalarType::Int) + { + return Json::Value(scalarNode.as<int>()); + } + else + { + return Json::Value(scalarNode.as<std::string>()); + } + } + + Json::Value ManifestYamlNodeToJson(const YAML::Node& rootNode, YamlScalarType scalarType = YamlScalarType::String) + { + Json::Value result; + + if (rootNode.IsNull()) + { + result = Json::Value::nullSingleton(); + } + else if (rootNode.IsMap()) + { + for (auto const& keyValuePair : rootNode.Mapping()) + { + // We only support string type as key in our manifest + auto key = keyValuePair.first.as<std::string>(); + result[keyValuePair.first.as<std::string>()] = ManifestYamlNodeToJson(keyValuePair.second, GetManifestScalarValueType(key)); + } + } + else if (rootNode.IsSequence()) + { + for (auto const& value : rootNode.Sequence()) + { + result.append(ManifestYamlNodeToJson(value, scalarType)); + } + } + else if (rootNode.IsScalar()) + { + result = YamlScalarNodeToJson(rootNode, scalarType); + } + else + { + THROW_HR(E_UNEXPECTED); + } + + return result; + } + } + + std::string LoadResourceAsString(PCWSTR resourceName, PCWSTR resourceType) + { + HMODULE resourceModule = NULL; + GetModuleHandleEx( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, + (PCWSTR)LoadResourceAsString, + &resourceModule); + THROW_LAST_ERROR_IF_NULL(resourceModule); + + HRSRC resourceInfoHandle = FindResource(resourceModule, resourceName, resourceType); + THROW_LAST_ERROR_IF_NULL(resourceInfoHandle); + + HGLOBAL resourceMemoryHandle = LoadResource(resourceModule, resourceInfoHandle); + THROW_LAST_ERROR_IF_NULL(resourceMemoryHandle); + + ULONG resourceSize = 0; + char* resourceContent = NULL; + resourceSize = SizeofResource(resourceModule, resourceInfoHandle); + THROW_LAST_ERROR_IF(resourceSize == 0); + + resourceContent = reinterpret_cast<char*>(LockResource(resourceMemoryHandle)); + THROW_HR_IF_NULL(E_UNEXPECTED, resourceContent); + + std::string resourceStr; + resourceStr.assign(resourceContent, resourceSize); + + return resourceStr; + } + + Json::Value LoadSchemaDoc(const ManifestVer& manifestVersion, ManifestTypeEnum manifestType) + { + std::string schemaStr; + + if (manifestVersion >= ManifestVer{ s_ManifestVersionV1 }) + { + switch (manifestType) + { + case AppInstaller::Manifest::ManifestTypeEnum::Singleton: + schemaStr = LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_V1_SINGLETON), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); + break; + case AppInstaller::Manifest::ManifestTypeEnum::Version: + schemaStr = LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_V1_VERSION), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); + break; + case AppInstaller::Manifest::ManifestTypeEnum::Installer: + schemaStr = LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_V1_INSTALLER), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); + break; + case AppInstaller::Manifest::ManifestTypeEnum::DefaultLocale: + schemaStr = LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_V1_DEFAULTLOCALE), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); + break; + case AppInstaller::Manifest::ManifestTypeEnum::Locale: + schemaStr = LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_V1_LOCALE), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); + break; + default: + THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); + } + } + else + { + schemaStr = LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_PREVIEW), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); + } + + Json::Value schemaJson; + int schemaLength = static_cast<int>(schemaStr.length()); + Json::CharReaderBuilder charReaderBuilder; + const std::unique_ptr<Json::CharReader> jsonReader(charReaderBuilder.newCharReader()); + std::string errorMsg; + if (!jsonReader->parse(schemaStr.c_str(), schemaStr.c_str() + schemaLength, &schemaJson, &errorMsg)) { + THROW_HR_MSG(E_UNEXPECTED, "Jsoncpp parser failed to parse the schema doc. Reason: %s", errorMsg.c_str()); + } + + return schemaJson; + } + + std::vector<ValidationError> ValidateAgainstSchema(const std::vector<YamlManifestInfo>& manifestList, const ManifestVer& manifestVersion) + { + std::vector<ValidationError> errors; + // A list of schema validator to avoid multiple loadings of same schema + std::map<ManifestTypeEnum, valijson::Schema> schemaList; + valijson::Validator schemaValidator; + + for (const auto& entry : manifestList) + { + if (schemaList.find(entry.ManifestType) == schemaList.end()) + { + // Copy constructor of valijson::Schema was private + valijson::Schema& newSchema = schemaList.emplace( + std::piecewise_construct, std::make_tuple(entry.ManifestType), std::make_tuple()).first->second; + valijson::SchemaParser schemaParser; + Json::Value schemaJson = LoadSchemaDoc(manifestVersion, entry.ManifestType); + valijson::adapters::JsonCppAdapter jsonSchemaAdapter(schemaJson); + schemaParser.populateSchema(jsonSchemaAdapter, newSchema); + } + + const auto& schema = schemaList.find(entry.ManifestType)->second; + + Json::Value manifestJson = ManifestYamlNodeToJson(entry.Root); + valijson::adapters::JsonCppAdapter manifestJsonAdapter(manifestJson); + valijson::ValidationResults results; + + if (!schemaValidator.validate(schema, manifestJsonAdapter, &results)) + { + valijson::ValidationResults::Error error; + std::stringstream ss; + + ss << "Schema validation failed." << std::endl; + while (results.popError(error)) + { + std::string context; + for (auto itr = error.context.begin(); itr != error.context.end(); itr++) + { + context += *itr; + } + + ss << "Error context: " << context << " Description: " << error.description << std::endl; + } + + errors.emplace_back(ValidationError::MessageWithFile(ss.str(), entry.FileName)); + } + } + + return errors; + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp @@ -9,12 +9,6 @@ namespace AppInstaller::Manifest { std::vector<ValidationError> resultErrors; - // Channel is not supported currently - if (!manifest.Channel.empty()) - { - resultErrors.emplace_back(ManifestError::FieldNotSupported, "Channel", manifest.Channel); - } - try { // Version value should be successfully parsed @@ -25,12 +19,6 @@ namespace AppInstaller::Manifest resultErrors.emplace_back(ManifestError::InvalidFieldValue, "Version", manifest.Version); } - // License field is required - if (manifest.License.empty()) - { - resultErrors.emplace_back(ManifestError::RequiredFieldMissing, "License"); - } - // Comparison function to check duplicate installer entry. {installerType, arch, language and scope} combination is the key. // Todo: use the comparator from ManifestComparator when that one is fully implemented. auto installerCmp = [](const ManifestInstaller& in1, const ManifestInstaller& in2) @@ -45,9 +33,9 @@ namespace AppInstaller::Manifest return in1.Arch < in2.Arch; } - if (in1.Language != in2.Language) + if (in1.Locale != in2.Locale) { - return in1.Language < in2.Language; + return in1.Locale < in2.Locale; } if (in1.Scope != in2.Scope) @@ -75,33 +63,33 @@ namespace AppInstaller::Manifest resultErrors.emplace_back(ManifestError::InvalidFieldValue, "Arch"); } - if (installer.InstallerType == ManifestInstaller::InstallerTypeEnum::Unknown) + if (installer.InstallerType == InstallerTypeEnum::Unknown) { resultErrors.emplace_back(ManifestError::InvalidFieldValue, "InstallerType"); } - if (installer.UpdateBehavior == ManifestInstaller::UpdateBehaviorEnum::Unknown) + if (installer.UpdateBehavior == UpdateBehaviorEnum::Unknown) { resultErrors.emplace_back(ManifestError::InvalidFieldValue, "UpdateBehavior"); } // Validate system reference strings if they are set at the installer level - if (!installer.PackageFamilyName.empty() && !ManifestInstaller::DoesInstallerTypeUsePackageFamilyName(installer.InstallerType)) + if (!installer.PackageFamilyName.empty() && !DoesInstallerTypeUsePackageFamilyName(installer.InstallerType)) { - resultErrors.emplace_back(ManifestError::InstallerTypeDoesNotSupportPackageFamilyName, "InstallerType", ManifestInstaller::InstallerTypeToString(installer.InstallerType)); + resultErrors.emplace_back(ManifestError::InstallerTypeDoesNotSupportPackageFamilyName, "InstallerType", InstallerTypeToString(installer.InstallerType)); } - if (!installer.ProductCode.empty() && !ManifestInstaller::DoesInstallerTypeUseProductCode(installer.InstallerType)) + if (!installer.ProductCode.empty() && !DoesInstallerTypeUseProductCode(installer.InstallerType)) { - resultErrors.emplace_back(ManifestError::InstallerTypeDoesNotSupportProductCode, "InstallerType", ManifestInstaller::InstallerTypeToString(installer.InstallerType)); + resultErrors.emplace_back(ManifestError::InstallerTypeDoesNotSupportProductCode, "InstallerType", InstallerTypeToString(installer.InstallerType)); } - if (installer.InstallerType == ManifestInstaller::InstallerTypeEnum::MSStore) + if (installer.InstallerType == InstallerTypeEnum::MSStore) { // MSStore type is not supported in community repo resultErrors.emplace_back( ManifestError::FieldValueNotSupported, "InstallerType", - ManifestInstaller::InstallerTypeToString(installer.InstallerType)); + InstallerTypeToString(installer.InstallerType)); if (installer.ProductId.empty()) { @@ -126,9 +114,9 @@ namespace AppInstaller::Manifest } } - if (installer.InstallerType == ManifestInstaller::InstallerTypeEnum::Exe && - (installer.Switches.find(ManifestInstaller::InstallerSwitchType::SilentWithProgress) == installer.Switches.end() || - installer.Switches.find(ManifestInstaller::InstallerSwitchType::Silent) == installer.Switches.end())) + if (installer.InstallerType == InstallerTypeEnum::Exe && + (installer.Switches.find(InstallerSwitchType::SilentWithProgress) == installer.Switches.end() || + installer.Switches.find(InstallerSwitchType::Silent) == installer.Switches.end())) { resultErrors.emplace_back(ManifestError::ExeInstallerMissingSilentSwitches, ValidationError::Level::Warning); } diff --git a/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp b/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp @@ -0,0 +1,555 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "AppInstallerSHA256.h" +#include "winget/ManifestYamlPopulator.h" + +namespace AppInstaller::Manifest +{ + using ValidationErrors = std::vector<ValidationError>; + + namespace + { + // Only used in preview manifest + std::vector<Manifest::string_t> SplitMultiValueField(const std::string& input) + { + if (input.empty()) + { + return {}; + } + + std::vector<Manifest::string_t> result; + size_t currentPos = 0; + while (currentPos < input.size()) + { + size_t splitPos = input.find(',', currentPos); + if (splitPos == std::string::npos) + { + splitPos = input.size(); + } + + std::string splitVal = input.substr(currentPos, splitPos - currentPos); + Utility::Trim(splitVal); + if (!splitVal.empty()) + { + result.emplace_back(std::move(splitVal)); + } + currentPos = splitPos + 1; + } + + return result; + } + + std::vector<Manifest::string_t> ProcessStringSequenceNode(const YAML::Node& node, bool trim = true) + { + THROW_HR_IF(E_INVALIDARG, !node.IsSequence()); + + std::vector<Manifest::string_t> result; + + for (auto const& entry : node.Sequence()) + { + std::string value = entry.as<std::string>(); + if (trim) + { + Utility::Trim(value); + } + + result.emplace_back(std::move(value)); + } + + return result; + } + + std::vector<int> ProcessIntSequenceNode(const YAML::Node& node) + { + THROW_HR_IF(E_INVALIDARG, !node.IsSequence()); + + std::vector<int> result; + + for (auto const& entry : node.Sequence()) + { + result.emplace_back(entry.as<int>()); + } + + return result; + } + + std::vector<PlatformEnum> ProcessPlatformSequenceNode(const YAML::Node& node) + { + THROW_HR_IF(E_INVALIDARG, !node.IsSequence()); + + std::vector<PlatformEnum> result; + + for (auto const& entry : node.Sequence()) + { + result.emplace_back(ConvertToPlatformEnum(entry.as<std::string>())); + } + + return result; + } + + std::vector<ScopeEnum> ProcessScopeSequenceNode(const YAML::Node& node) + { + THROW_HR_IF(E_INVALIDARG, !node.IsSequence()); + + std::vector<ScopeEnum> result; + + for (auto const& entry : node.Sequence()) + { + result.emplace_back(ConvertToScopeEnum(entry.as<std::string>())); + } + + return result; + } + } + + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetRootFieldProcessInfo(const ManifestVer& manifestVersion) + { + // Common fields across versions + std::vector<FieldProcessInfo> result = + { + { "ManifestVersion", [](const YAML::Node&)->ValidationErrors { /* ManifestVersion already populated. Field listed here for duplicate and PascalCase check */ return {}; } }, + { "Installers", [this](const YAML::Node& value)->ValidationErrors { m_p_installersNode = &value; return {}; } }, + { "Localization", [this](const YAML::Node& value)->ValidationErrors { m_p_localizationsNode = &value; return {}; } }, + { "Channel", [this](const YAML::Node& value)->ValidationErrors { m_p_manifest->Channel = Utility::Trim(value.as<std::string>()); return {}; } }, + }; + + // Additional version specific fields + if (manifestVersion.Major() == 0) + { + std::vector<FieldProcessInfo> previewRootFields + { + { "Id", [this](const YAML::Node& value)->ValidationErrors { m_p_manifest->Id = Utility::Trim(value.as<std::string>()); return {}; } }, + { "Version", [this](const YAML::Node& value)->ValidationErrors { m_p_manifest->Version = Utility::Trim(value.as<std::string>()); return {}; } }, + { "AppMoniker", [this](const YAML::Node& value)->ValidationErrors { m_p_manifest->Moniker = Utility::Trim(value.as<std::string>()); return {}; } }, + }; + + std::move(previewRootFields.begin(), previewRootFields.end(), std::inserter(result, result.end())); + } + else if (manifestVersion.Major() == 1) + { + // Starting v1, we should be only adding new fields for each minor version increase + if (manifestVersion >= ManifestVer{ s_ManifestVersionV1 }) + { + std::vector<FieldProcessInfo> v1RootFields + { + { "PackageIdentifier", [this](const YAML::Node& value)->ValidationErrors { m_p_manifest->Id = Utility::Trim(value.as<std::string>()); return {}; } }, + { "PackageVersion", [this](const YAML::Node& value)->ValidationErrors { m_p_manifest->Version = Utility::Trim(value.as<std::string>()); return {}; } }, + { "Moniker", [this](const YAML::Node& value)->ValidationErrors { m_p_manifest->Moniker = Utility::Trim(value.as<std::string>()); return {}; } }, + { "ManifestType", [](const YAML::Node&)->ValidationErrors { /* ManifestType already checked. Field listed here for duplicate and PascalCase check */ return {}; } }, + }; + + std::move(v1RootFields.begin(), v1RootFields.end(), std::inserter(result, result.end())); + } + } + + // Root fields mapped as Installer and Localization values + auto rootInstallerFields = GetInstallerFieldProcessInfo(manifestVersion, true); + std::move(rootInstallerFields.begin(), rootInstallerFields.end(), std::inserter(result, result.end())); + + auto rootLocalizationFields = GetLocalizationFieldProcessInfo(manifestVersion, 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) + { + // Common fields across versions + std::vector<FieldProcessInfo> result = + { + { "InstallerType", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->InstallerType = ConvertToInstallerTypeEnum(value.as<std::string>()); return {}; } }, + { "PackageFamilyName", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->PackageFamilyName = value.as<std::string>(); return {}; } }, + { "ProductCode", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->ProductCode = value.as<std::string>(); return {}; } }, + }; + + // Additional version specific fields + if (manifestVersion.Major() == 0) + { + // Root level and Localization node level + std::vector<FieldProcessInfo> previewCommonFields = + { + { "UpdateBehavior", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->UpdateBehavior = ConvertToUpdateBehaviorEnum(value.as<std::string>()); return {}; } }, + { "Switches", [this](const YAML::Node& value)->ValidationErrors { m_p_switches = &(m_p_installer->Switches); return ValidateAndProcessFields(value, SwitchesFieldInfos); } }, + }; + + std::move(previewCommonFields.begin(), previewCommonFields.end(), std::inserter(result, result.end())); + + if (!forRootFields) + { + // Installer node only + std::vector<FieldProcessInfo> installerOnlyFields = + { + { "Arch", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Arch = Utility::ConvertToArchitectureEnum(value.as<std::string>()); return {}; } }, + { "Url", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Url = value.as<std::string>(); return {}; } }, + { "Sha256", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Sha256 = Utility::SHA256::ConvertToBytes(value.as<std::string>()); return {}; } }, + { "SignatureSha256", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->SignatureSha256 = Utility::SHA256::ConvertToBytes(value.as<std::string>()); return {}; } }, + { "Language", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Locale = value.as<std::string>(); return {}; } }, + { "Scope", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Scope = ConvertToScopeEnum(value.as<std::string>()); return {}; } }, + }; + + if (manifestVersion.HasExtension(s_MSStoreExtension)) + { + installerOnlyFields.emplace_back("ProductId", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->ProductId = value.as<std::string>(); return {}; }); + } + + std::move(installerOnlyFields.begin(), installerOnlyFields.end(), std::inserter(result, result.end())); + } + else + { + // Root node only + std::vector<FieldProcessInfo> rootOnlyFields = + { + { "MinOSVersion", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->MinOSVersion = value.as<std::string>(); return {}; } }, + { "Commands", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Commands = SplitMultiValueField(value.as<std::string>()); return {}; } }, + { "Protocols", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Protocols = SplitMultiValueField(value.as<std::string>()); return {}; } }, + { "FileExtensions", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->FileExtensions = SplitMultiValueField(value.as<std::string>()); return {}; } }, + }; + + std::move(rootOnlyFields.begin(), rootOnlyFields.end(), std::inserter(result, result.end())); + } + } + else if (manifestVersion.Major() == 1) + { + // Starting v1, we should be only adding new fields for each minor version increase + if (manifestVersion >= ManifestVer{ s_ManifestVersionV1 }) + { + // Root level and Localization node level + std::vector<FieldProcessInfo> v1CommonFields = + { + { "InstallerLocale", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Locale = value.as<std::string>(); return {}; } }, + { "Platform", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Platform = ProcessPlatformSequenceNode(value); return {}; } }, + { "MinimumOSVersion", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->MinOSVersion = value.as<std::string>(); return {}; } }, + { "Scope", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Scope = ConvertToScopeEnum(value.as<std::string>()); return {}; } }, + { "InstallModes", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->InstallModes = ProcessScopeSequenceNode(value); return {}; } }, + { "InstallerSwitches", [this](const YAML::Node& value)->ValidationErrors { m_p_switches = &(m_p_installer->Switches); return ValidateAndProcessFields(value, SwitchesFieldInfos); } }, + { "InstallerSuccessCodes", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->InstallerSuccessCodes = ProcessIntSequenceNode(value); return {}; } }, + { "UpgradeBehavior", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->UpdateBehavior = ConvertToUpdateBehaviorEnum(value.as<std::string>()); return {}; } }, + { "Commands", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Commands = ProcessStringSequenceNode(value); return {}; } }, + { "Protocols", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Protocols = ProcessStringSequenceNode(value); return {}; } }, + { "FileExtensions", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->FileExtensions = ProcessStringSequenceNode(value); return {}; } }, + { "Dependencies", [this](const YAML::Node& value)->ValidationErrors { m_p_dependency = &(m_p_installer->Dependencies); return ValidateAndProcessFields(value, DependenciesFieldInfos); } }, + { "Capabilities", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Capabilities = ProcessStringSequenceNode(value); return {}; } }, + { "RestrictedCapabilities", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->RestrictedCapabilities = ProcessStringSequenceNode(value); return {}; } }, + }; + + std::move(v1CommonFields.begin(), v1CommonFields.end(), std::inserter(result, result.end())); + + if (!forRootFields) + { + // Installer level only fields + std::vector<FieldProcessInfo> v1InstallerFields = + { + { "Architecture", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Arch = Utility::ConvertToArchitectureEnum(value.as<std::string>()); return {}; } }, + { "InstallerUrl", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Url = value.as<std::string>(); return {}; } }, + { "InstallerSha256", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Sha256 = Utility::SHA256::ConvertToBytes(value.as<std::string>()); return {}; } }, + { "SignatureSha256", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->SignatureSha256 = Utility::SHA256::ConvertToBytes(value.as<std::string>()); return {}; } }, + }; + + std::move(v1InstallerFields.begin(), v1InstallerFields.end(), std::inserter(result, result.end())); + } + } + } + + return result; + } + + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetSwitchesFieldProcessInfo(const ManifestVer& manifestVersion) + { + // Common fields across versions + std::vector<FieldProcessInfo> result = + { + { "Custom", [this](const YAML::Node& value)->ValidationErrors { (*m_p_switches)[InstallerSwitchType::Custom] = value.as<std::string>(); return{}; } }, + { "Silent", [this](const YAML::Node& value)->ValidationErrors { (*m_p_switches)[InstallerSwitchType::Silent] = value.as<std::string>(); return{}; } }, + { "SilentWithProgress", [this](const YAML::Node& value)->ValidationErrors { (*m_p_switches)[InstallerSwitchType::SilentWithProgress] = value.as<std::string>(); return{}; } }, + { "Interactive", [this](const YAML::Node& value)->ValidationErrors { (*m_p_switches)[InstallerSwitchType::Interactive] = value.as<std::string>(); return{}; } }, + { "Log", [this](const YAML::Node& value)->ValidationErrors { (*m_p_switches)[InstallerSwitchType::Log] = value.as<std::string>(); return{}; } }, + { "InstallLocation", [this](const YAML::Node& value)->ValidationErrors { (*m_p_switches)[InstallerSwitchType::InstallLocation] = value.as<std::string>(); return{}; } }, + }; + + // Additional version specific fields + if (manifestVersion.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", [this](const YAML::Node& value)->ValidationErrors { (*m_p_switches)[InstallerSwitchType::Language] = value.as<std::string>(); return{}; }); + result.emplace_back("Update", [this](const YAML::Node& value)->ValidationErrors { (*m_p_switches)[InstallerSwitchType::Update] = value.as<std::string>(); return{}; }); + } + else if (manifestVersion.Major() == 1) + { + result.emplace_back("Upgrade", [this](const YAML::Node& value)->ValidationErrors { (*m_p_switches)[InstallerSwitchType::Update] = value.as<std::string>(); return{}; }); + } + + return result; + } + + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetLocalizationFieldProcessInfo(const ManifestVer& manifestVersion, bool forRootFields) + { + // Common fields across versions + std::vector<FieldProcessInfo> result = + { + { "Description", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add<Localization::Description>(value.as<std::string>()); return {}; } }, + { "LicenseUrl", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add<Localization::LicenseUrl>(value.as<std::string>()); return {}; } }, + }; + + // Additional version specific fields + if (manifestVersion.Major() == 0) + { + // Root level and Localization node level + result.emplace_back("Homepage", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add<Localization::PackageUrl>(value.as<std::string>()); return {}; }); + + if (!forRootFields) + { + // Localization node only + result.emplace_back("Language", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Locale = value.as<std::string>(); return {}; }); + } + else + { + // Root node only + std::vector<FieldProcessInfo> rootOnlyFields = + { + { "Name", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add<Localization::PackageName>(Utility::Trim(value.as<std::string>())); return {}; } }, + { "Publisher", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add<Localization::Publisher>(value.as<std::string>()); return {}; } }, + { "Author", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add<Localization::Author>(value.as<std::string>()); return {}; } }, + { "License", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add<Localization::License>(value.as<std::string>()); return {}; } }, + { "Tags", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add<Localization::Tags>(SplitMultiValueField(value.as<std::string>())); return {}; } }, + }; + + std::move(rootOnlyFields.begin(), rootOnlyFields.end(), std::inserter(result, result.end())); + } + } + else if (manifestVersion.Major() == 1) + { + // Starting v1, we should be only adding new fields for each minor version increase + if (manifestVersion >= ManifestVer{ s_ManifestVersionV1 }) + { + // Root level and Localization node level + std::vector<FieldProcessInfo> v1CommonFields = + { + { "PackageLocale", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Locale = value.as<std::string>(); return {}; } }, + { "Publisher", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add<Localization::Publisher>(value.as<std::string>()); return {}; } }, + { "PublisherUrl", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add<Localization::PublisherUrl>(value.as<std::string>()); return {}; } }, + { "PublisherSupportUrl", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add<Localization::PublisherSupportUrl>(value.as<std::string>()); return {}; } }, + { "PrivacyUrl", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add<Localization::PrivacyUrl>(value.as<std::string>()); return {}; } }, + { "Author", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add<Localization::Author>(value.as<std::string>()); return {}; } }, + { "PackageName", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add<Localization::PackageName>(Utility::Trim(value.as<std::string>())); return {}; } }, + { "PackageUrl", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add<Localization::PackageUrl>(value.as<std::string>()); return {}; } }, + { "License", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add<Localization::License>(value.as<std::string>()); return {}; } }, + { "Copyright", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add<Localization::Copyright>(value.as<std::string>()); return {}; } }, + { "CopyrightUrl", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add<Localization::CopyrightUrl>(value.as<std::string>()); return {}; } }, + { "ShortDescription", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add<Localization::ShortDescription>(value.as<std::string>()); return {}; } }, + { "Tags", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add<Localization::Tags>(ProcessStringSequenceNode(value)); return {}; } }, + }; + + std::move(v1CommonFields.begin(), v1CommonFields.end(), std::inserter(result, result.end())); + } + } + + return result; + } + + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetDependenciesFieldProcessInfo(const ManifestVer& manifestVersion) + { + std::vector<FieldProcessInfo> result = {}; + + if (manifestVersion >= ManifestVer{ s_ManifestVersionV1 }) + { + result = + { + { "WindowsFeatures", [this](const YAML::Node& value)->ValidationErrors { m_p_dependency->WindowsFeatures = ProcessStringSequenceNode(value); return {}; } }, + { "WindowsLibraries", [this](const YAML::Node& value)->ValidationErrors { m_p_dependency->WindowsLibraries = ProcessStringSequenceNode(value); return {}; } }, + { "PackageDependencies", [this](const YAML::Node& value)->ValidationErrors { return ProcessPackageDependenciesNode(value, m_p_dependency->PackageDependencies); } }, + { "ExternalDependencies", [this](const YAML::Node& value)->ValidationErrors { m_p_dependency->ExternalDependencies = ProcessStringSequenceNode(value); return {}; } }, + }; + } + + return result; + } + + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetPackageDependenciesFieldProcessInfo(const ManifestVer& manifestVersion) + { + std::vector<FieldProcessInfo> result = {}; + + if (manifestVersion >= ManifestVer{ s_ManifestVersionV1 }) + { + result = + { + { "PackageIdentifier", [this](const YAML::Node& value)->ValidationErrors { m_p_packageDependency->Id = Utility::Trim(value.as<std::string>()); return {}; } }, + { "MinimumVersion", [this](const YAML::Node& value)->ValidationErrors { m_p_packageDependency->MinVersion = Utility::Trim(value.as<std::string>()); return {}; } }, + }; + } + + return result; + } + + ValidationErrors ManifestYamlPopulator::ValidateAndProcessFields( + const YAML::Node& rootNode, + const std::vector<FieldProcessInfo>& fieldInfos) + { + ValidationErrors resultErrors; + + if (!rootNode.IsMap() || rootNode.size() == 0) + { + resultErrors.emplace_back(ManifestError::InvalidRootNode, "", "", m_isMergedManifest ? 0 : rootNode.Mark().line, m_isMergedManifest ? 0 : rootNode.Mark().column); + return resultErrors; + } + + // Keeps track of already processed fields. Used to check duplicate fields. + std::set<std::string> processedFields; + + for (auto const& keyValuePair : rootNode.Mapping()) + { + std::string key = keyValuePair.first.as<std::string>(); + const YAML::Node& valueNode = keyValuePair.second; + + // We'll do case insensitive search first and validate correct case later. + auto fieldIter = std::find_if(fieldInfos.begin(), fieldInfos.end(), + [&](auto const& s) + { + return Utility::CaseInsensitiveEquals(s.Name, key); + }); + + if (fieldIter != fieldInfos.end()) + { + const FieldProcessInfo& fieldInfo = *fieldIter; + + // Make sure the found key is in Pascal Case + if (key != fieldInfo.Name) + { + resultErrors.emplace_back(ManifestError::FieldIsNotPascalCase, key, "", m_isMergedManifest ? 0 : keyValuePair.first.Mark().line, m_isMergedManifest ? 0 : keyValuePair.first.Mark().column); + } + + // Make sure it's not a duplicate key + if (!processedFields.insert(fieldInfo.Name).second) + { + resultErrors.emplace_back(ManifestError::FieldDuplicate, fieldInfo.Name, "", m_isMergedManifest ? 0 : keyValuePair.first.Mark().line, m_isMergedManifest ? 0 : keyValuePair.first.Mark().column); + } + + if (!valueNode.IsNull()) + { + try + { + auto errors = fieldInfo.ProcessFunc(valueNode); + std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); + } + catch (const std::exception&) + { + resultErrors.emplace_back(ManifestError::FieldFailedToProcess, fieldInfo.Name); + } + } + } + else + { + // For full validation, also reports unrecognized fields as warning + if (m_fullValidation) + { + resultErrors.emplace_back(ManifestError::FieldUnknown, key, "", m_isMergedManifest ? 0 : keyValuePair.first.Mark().line, m_isMergedManifest ? 0 : keyValuePair.first.Mark().column, ValidationError::Level::Warning); + } + } + } + + return resultErrors; + } + + ValidationErrors ManifestYamlPopulator::ProcessPackageDependenciesNode(const YAML::Node& rootNode, std::vector<PackageDependency>& packageDependencies) + { + ValidationErrors resultErrors; + + packageDependencies.clear(); + for (auto const& entry : rootNode.Sequence()) + { + PackageDependency packageDependency; + m_p_packageDependency = &packageDependency; + auto errors = ValidateAndProcessFields(entry, PackageDependenciesFieldInfos); + std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); + packageDependencies.emplace_back(std::move(std::move(packageDependency))); + } + + return resultErrors; + } + + ValidationErrors ManifestYamlPopulator::PopulateManifestInternal(const YAML::Node& rootNode, Manifest& manifest, const ManifestVer& manifestVersion, bool fullValidation) + { + m_fullValidation = fullValidation; + m_isMergedManifest = !rootNode["ManifestType"sv].IsNull() && rootNode["ManifestType"sv].as<std::string>() == "merged"; + + ValidationErrors resultErrors; + manifest.ManifestVersion = manifestVersion; + + // Prepare field infos + RootFieldInfos = GetRootFieldProcessInfo(manifestVersion); + InstallerFieldInfos = GetInstallerFieldProcessInfo(manifestVersion); + SwitchesFieldInfos = GetSwitchesFieldProcessInfo(manifestVersion); + DependenciesFieldInfos = GetDependenciesFieldProcessInfo(manifestVersion); + PackageDependenciesFieldInfos = GetPackageDependenciesFieldProcessInfo(manifestVersion); + LocalizationFieldInfos = GetLocalizationFieldProcessInfo(manifestVersion); + + // Populate root + m_p_manifest = &manifest; + m_p_installer = &(manifest.DefaultInstallerInfo); + m_p_localization = &(manifest.DefaultLocalization); + resultErrors = ValidateAndProcessFields(rootNode, RootFieldInfos); + + if (!m_p_installersNode) + { + return resultErrors; + } + + // Populate installers + for (auto const& entry : m_p_installersNode->Sequence()) + { + ManifestInstaller installer = manifest.DefaultInstallerInfo; + + // Clear these defaults as PackageFamilyName and ProductCode needs to be copied based on InstallerType + installer.PackageFamilyName.clear(); + installer.ProductCode.clear(); + + m_p_installer = &installer; + auto errors = ValidateAndProcessFields(entry, InstallerFieldInfos); + std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); + + // Copy in system reference strings from the root if not set in the installer and appropriate + if (installer.PackageFamilyName.empty() && DoesInstallerTypeUsePackageFamilyName(installer.InstallerType)) + { + installer.PackageFamilyName = manifest.DefaultInstallerInfo.PackageFamilyName; + } + + if (installer.ProductCode.empty() && DoesInstallerTypeUseProductCode(installer.InstallerType)) + { + installer.ProductCode = manifest.DefaultInstallerInfo.ProductCode; + } + + // Populate installer default switches if not exists + auto defaultSwitches = GetDefaultKnownSwitches(installer.InstallerType); + for (auto const& defaultSwitch : defaultSwitches) + { + if (installer.Switches.find(defaultSwitch.first) == installer.Switches.end()) + { + installer.Switches[defaultSwitch.first] = defaultSwitch.second; + } + } + + manifest.Installers.emplace_back(std::move(installer)); + } + + // Populate additional localizations + if (m_p_localizationsNode && m_p_localizationsNode->IsSequence()) + { + for (auto const& entry : m_p_localizationsNode->Sequence()) + { + ManifestLocalization localization; + m_p_localization = &localization; + auto errors = ValidateAndProcessFields(entry, LocalizationFieldInfos); + std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); + manifest.Localizations.emplace_back(std::move(std::move(localization))); + } + } + + return resultErrors; + } + + ValidationErrors ManifestYamlPopulator::PopulateManifest(const YAML::Node& rootNode, Manifest& manifest, const ManifestVer& manifestVersion, bool fullValidation) + { + ManifestYamlPopulator manifestPopulator; + return manifestPopulator.PopulateManifestInternal(rootNode, manifest, manifestVersion, fullValidation); + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Manifest/YamlParser.cpp b/src/AppInstallerCommonCore/Manifest/YamlParser.cpp @@ -3,448 +3,480 @@ #include "pch.h" #include "AppInstallerSHA256.h" #include "winget/Yaml.h" +#include "winget/ManifestSchemaValidation.h" +#include "winget/ManifestYamlPopulator.h" #include "winget/ManifestYamlParser.h" -namespace AppInstaller::Manifest +namespace AppInstaller::Manifest::YamlParser { namespace { - // The maximum supported major version known about by this code. - constexpr uint64_t s_MaxSupportedMajorVersion = 0; + // Input validations: + // - Determine manifest version + // - Check multi file manifest input integrity + // - All manifests use same PackageIdentifier, PackageVersion, ManifestVersion + // - All required types exist and exist only once. i.e. version, installer, defaultLocale + // - No duplicate locales across manifests + // - DefaultLocale matches in version manifest and defaultLocale manifest + // - Validate manifest type correctness + // - Allowed file type in multi file manifest: version, installer, defaultLocale, locale + // - Allowed file type in single file manifest: preview manifest, merged and singleton + ManifestVer ValidateInput(std::vector<YamlManifestInfo>& input, bool fullValidation, bool schemaValidationOnly) + { + std::vector<ValidationError> errors; - // The default manifest version assigned to manifests without a ManifestVersion field. - constexpr std::string_view s_DefaultManifestVersion = "0.1.0"sv; + std::string manifestVersionStr; + ManifestVer manifestVersion; + ManifestVer ManifestVersionV1{ s_ManifestVersionV1 }; + bool isMultifileManifest = input.size() > 1; - // The manifest extension for the MS Store - constexpr std::string_view s_MSStoreExtension = "msstore"sv; + // Use the first manifest doc to determine ManifestVersion, there'll be checks for manifest version consistency later + auto& firstYamlManifest = input[0]; + if (!firstYamlManifest.Root.IsMap()) + { + THROW_EXCEPTION_MSG(ManifestException(APPINSTALLER_CLI_ERROR_INVALID_MANIFEST), "The manifest does not contain a valid root. File: %S", firstYamlManifest.FileName.c_str()); + } - std::vector<Manifest::string_t> SplitMultiValueField(const std::string& input) - { - if (input.empty()) + if (firstYamlManifest.Root["ManifestVersion"sv]) + { + manifestVersionStr = firstYamlManifest.Root["ManifestVersion"sv].as<std::string>(); + } + else { - return {}; + manifestVersionStr = s_DefaultManifestVersion; } + manifestVersion = ManifestVer{ manifestVersionStr }; - std::vector<Manifest::string_t> result; - size_t currentPos = 0; - while (currentPos < input.size()) + // Check max supported version + if (manifestVersion.Major() > s_MaxSupportedMajorVersion) { - size_t splitPos = input.find(',', currentPos); - if (splitPos == std::string::npos) + THROW_EXCEPTION_MSG(ManifestException(APPINSTALLER_CLI_ERROR_UNSUPPORTED_MANIFESTVERSION), "Unsupported ManifestVersion: %S", manifestVersion.ToString().c_str()); + } + + // multi file manifest is only supported starting ManifestVersion 1.0.0 + if (isMultifileManifest && manifestVersion < ManifestVersionV1) + { + THROW_EXCEPTION_MSG(ManifestException(APPINSTALLER_CLI_ERROR_INVALID_MANIFEST), "Preview manifest does not support multi file manifest format."); + } + + if (isMultifileManifest) + { + // Populates the PackageIdentifier and PackageVersion from first doc for later consistency check + std::string packageId = firstYamlManifest.Root["PackageIdentifier"].as<std::string>(); + std::string packageVersion = firstYamlManifest.Root["PackageVersion"].as<std::string>(); + + std::set<std::string> localesSet; + + bool isVersionManifestFound = false; + bool isInstallerManifestFound = false; + bool isDefaultLocaleManifestFound = false; + std::string defaultLocaleFromVersionManifest; + std::string defaultLocaleFromDefaultLocaleManifest; + + for (auto& entry : input) + { + if (!entry.Root.IsMap()) + { + THROW_EXCEPTION_MSG(ManifestException(APPINSTALLER_CLI_ERROR_INVALID_MANIFEST), "The manifest does not contain a valid root. File: %S", entry.FileName.c_str()); + } + + std::string localPackageId = entry.Root["PackageIdentifier"].as<std::string>(); + if (localPackageId != packageId) + { + errors.emplace_back(ValidationError::MessageFieldValueWithFile( + ManifestError::InconsistentMultiFileManifestFieldValue, "PackageIdentifier", localPackageId, entry.FileName)); + } + + std::string localPackageVersion = entry.Root["PackageVersion"].as<std::string>(); + if (localPackageVersion != packageVersion) + { + errors.emplace_back(ValidationError::MessageFieldValueWithFile( + ManifestError::InconsistentMultiFileManifestFieldValue, "PackageVersion", localPackageVersion, entry.FileName)); + } + + std::string localManifestVersion = entry.Root["ManifestVersion"].as<std::string>(); + if (localManifestVersion != manifestVersionStr) + { + errors.emplace_back(ValidationError::MessageFieldValueWithFile( + ManifestError::InconsistentMultiFileManifestFieldValue, "ManifestVersion", localManifestVersion, entry.FileName)); + } + + std::string manifestTypeStr = entry.Root["ManifestType"sv].as<std::string>(); + ManifestTypeEnum manifestType = ConvertToManifestTypeEnum(manifestTypeStr); + entry.ManifestType = manifestType; + + switch (manifestType) + { + case ManifestTypeEnum::Version: + if (isVersionManifestFound) + { + errors.emplace_back(ValidationError::MessageFieldValueWithFile( + ManifestError::DuplicateMultiFileManifestType, "ManifestType", manifestTypeStr, entry.FileName)); + } + else + { + isVersionManifestFound = true; + defaultLocaleFromVersionManifest = entry.Root["DefaultLocale"sv].as<std::string>(); + } + break; + case ManifestTypeEnum::Installer: + if (isInstallerManifestFound) + { + errors.emplace_back(ValidationError::MessageFieldValueWithFile( + ManifestError::DuplicateMultiFileManifestType, "ManifestType", manifestTypeStr, entry.FileName)); + } + else + { + isInstallerManifestFound = true; + } + break; + case ManifestTypeEnum::DefaultLocale: + if (isDefaultLocaleManifestFound) + { + errors.emplace_back(ValidationError::MessageFieldValueWithFile( + ManifestError::DuplicateMultiFileManifestType, "ManifestType", manifestTypeStr, entry.FileName)); + } + else + { + isDefaultLocaleManifestFound = true; + auto packageLocale = entry.Root["PackageLocale"sv].as<std::string>(); + defaultLocaleFromDefaultLocaleManifest = packageLocale; + + if (localesSet.find(packageLocale) != localesSet.end()) + { + errors.emplace_back(ValidationError::MessageFieldValueWithFile( + ManifestError::DuplicateMultiFileManifestLocale, "PackageLocale", packageLocale, entry.FileName)); + } + else + { + localesSet.insert(packageLocale); + } + } + break; + case ManifestTypeEnum::Locale: + { + auto packageLocale = entry.Root["PackageLocale"sv].as<std::string>(); + if (localesSet.find(packageLocale) != localesSet.end()) + { + errors.emplace_back(ValidationError::MessageFieldValueWithFile( + ManifestError::DuplicateMultiFileManifestLocale, "PackageLocale", packageLocale, entry.FileName)); + } + else + { + localesSet.insert(packageLocale); + } + } + break; + default: + errors.emplace_back(ValidationError::MessageFieldValueWithFile( + ManifestError::UnsupportedMultiFileManifestType, "ManifestType", manifestTypeStr, entry.FileName)); + } + } + + if (isVersionManifestFound && isDefaultLocaleManifestFound && defaultLocaleFromDefaultLocaleManifest != defaultLocaleFromVersionManifest) { - splitPos = input.size(); + errors.emplace_back(ManifestError::InconsistentMultiFileManifestDefaultLocale); } - std::string splitVal = input.substr(currentPos, splitPos - currentPos); - Utility::Trim(splitVal); - if (!splitVal.empty()) + if (!schemaValidationOnly && !(isVersionManifestFound && isInstallerManifestFound && isDefaultLocaleManifestFound)) { - result.emplace_back(std::move(splitVal)); + errors.emplace_back(ManifestError::IncompleteMultiFileManifest); } - currentPos = splitPos + 1; } + else + { + if (manifestVersion >= ManifestVersionV1) + { + std::string manifestTypeStr = firstYamlManifest.Root["ManifestType"sv].as<std::string>(); + ManifestTypeEnum manifestType = ConvertToManifestTypeEnum(manifestTypeStr); + firstYamlManifest.ManifestType = manifestType; - return result; - } - } - - void YamlParser::PrepareManifestFieldInfos(const ManifestVer& manifestVer) - { - // Initially supported fields - RootFieldInfos = - { - { "ManifestVersion", [](const YAML::Node&) { /* ManifestVersion already processed */ }, false, - // Regex here is to prevent leading 0s in the version, this also keeps consistent with other versions in the manifest - "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){2}$" }, - { "Id", [this](const YAML::Node& value) { m_p_manifest->Id = value.as<std::string>(); Utility::Trim(m_p_manifest->Id); }, true, "^[\\S]+\\.[\\S]+$" }, - { "Name", [this](const YAML::Node& value) { m_p_manifest->Name = value.as<std::string>(); Utility::Trim(m_p_manifest->Name); }, true }, - { "Version", [this](const YAML::Node& value) { m_p_manifest->Version = value.as<std::string>(); Utility::Trim(m_p_manifest->Version); }, true, - /* File name chars not allowed */ "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$" }, - { "Publisher", [this](const YAML::Node& value) { m_p_manifest->Publisher = value.as<std::string>(); }, true }, - { "AppMoniker", [this](const YAML::Node& value) { m_p_manifest->AppMoniker = value.as<std::string>(); Utility::Trim(m_p_manifest->AppMoniker); } }, - { "Channel", [this](const YAML::Node& value) { m_p_manifest->Channel = value.as<std::string>(); Utility::Trim(m_p_manifest->Channel); } }, - { "Author", [this](const YAML::Node& value) { m_p_manifest->Author = value.as<std::string>(); } }, - { "License", [this](const YAML::Node& value) { m_p_manifest->License = value.as<std::string>(); } }, - { "MinOSVersion", [this](const YAML::Node& value) { m_p_manifest->MinOSVersion = value.as<std::string>(); Utility::Trim(m_p_manifest->MinOSVersion); }, false, - "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){0,3}$" }, - { "Tags", [this](const YAML::Node& value) { m_p_manifest->Tags = SplitMultiValueField(value.as<std::string>()); } }, - { "Commands", [this](const YAML::Node& value) { m_p_manifest->Commands = SplitMultiValueField(value.as<std::string>()); } }, - { "Protocols", [this](const YAML::Node& value) { m_p_manifest->Protocols = SplitMultiValueField(value.as<std::string>()); } }, - { "FileExtensions", [this](const YAML::Node& value) { m_p_manifest->FileExtensions = SplitMultiValueField(value.as<std::string>()); } }, - { "InstallerType", [this](const YAML::Node& value) { m_p_manifest->InstallerType = ManifestInstaller::ConvertToInstallerTypeEnum(value.as<std::string>()); } }, - { "UpdateBehavior", [this](const YAML::Node& value) { m_p_manifest->UpdateBehavior = ManifestInstaller::ConvertToUpdateBehaviorEnum(value.as<std::string>()); } }, - { "PackageFamilyName", [this](const YAML::Node& value) { m_p_manifest->PackageFamilyName = value.as<std::string>(); }, false, "[-.A-Za-z0-9]+_[A-Za-z0-9]{13}" }, - { "ProductCode", [this](const YAML::Node& value) { m_p_manifest->ProductCode = value.as<std::string>(); } }, - { "Description", [this](const YAML::Node& value) { m_p_manifest->Description = value.as<std::string>(); } }, - { "Homepage", [this](const YAML::Node& value) { m_p_manifest->Homepage = value.as<std::string>(); } }, - { "LicenseUrl", [this](const YAML::Node& value) { m_p_manifest->LicenseUrl = value.as<std::string>(); } }, - { "Switches", [this](const YAML::Node& value) { *m_p_switchesNode = value; } }, - { "Installers", [this](const YAML::Node& value) { *m_p_installersNode = value; }, true }, - { "Localization", [this](const YAML::Node& value) { *m_p_localizationsNode = value; } }, - }; - - InstallerFieldInfos = - { - { "Arch", [this](const YAML::Node& value) { m_p_installer->Arch = Utility::ConvertToArchitectureEnum(value.as<std::string>()); }, true }, - { "Url", [this](const YAML::Node& value) { m_p_installer->Url = value.as<std::string>(); } }, - { "Sha256", [this](const YAML::Node& value) { m_p_installer->Sha256 = Utility::SHA256::ConvertToBytes(value.as<std::string>()); }, false, "^[A-Fa-f0-9]{64}$" }, - { "SignatureSha256", [this](const YAML::Node& value) { m_p_installer->SignatureSha256 = Utility::SHA256::ConvertToBytes(value.as<std::string>()); }, false, "^[A-Fa-f0-9]{64}$" }, - { "Language", [this](const YAML::Node& value) { m_p_installer->Language = value.as<std::string>(); } }, - { "Scope", [this](const YAML::Node& value) { m_p_installer->Scope = ManifestInstaller::ConvertToScopeEnum(value.as<std::string>()); } }, - { "InstallerType", [this](const YAML::Node& value) { m_p_installer->InstallerType = ManifestInstaller::ConvertToInstallerTypeEnum(value.as<std::string>()); } }, - { "UpdateBehavior", [this](const YAML::Node& value) { m_p_installer->UpdateBehavior = ManifestInstaller::ConvertToUpdateBehaviorEnum(value.as<std::string>()); } }, - { "PackageFamilyName", [this](const YAML::Node& value) { m_p_installer->PackageFamilyName = value.as<std::string>(); }, false, "[-.A-Za-z0-9]+_[A-Za-z0-9]{13}" }, - { "ProductCode", [this](const YAML::Node& value) { m_p_installer->ProductCode = value.as<std::string>(); } }, - { "Switches", [this](const YAML::Node& value) { *m_p_switchesNode = value; } }, - }; - - SwitchesFieldInfos = - { - { "Custom", [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::Custom] = value.as<std::string>(); } }, - { "Silent", [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::Silent] = value.as<std::string>(); } }, - { "SilentWithProgress", [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::SilentWithProgress] = value.as<std::string>(); } }, - { "Interactive", [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::Interactive] = value.as<std::string>(); } }, - { "Language", [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::Language] = value.as<std::string>(); } }, - { "Log", [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::Log] = value.as<std::string>(); } }, - { "InstallLocation", [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::InstallLocation] = value.as<std::string>(); } }, - { "Update", [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::Update] = value.as<std::string>(); } }, - }; - - LocalizationFieldInfos = - { - { "Language", [this](const YAML::Node& value) { m_p_localization->Language = value.as<std::string>(); }, true }, - { "Description", [this](const YAML::Node& value) { m_p_localization->Description = value.as<std::string>(); } }, - { "Homepage", [this](const YAML::Node& value) { m_p_localization->Homepage = value.as<std::string>(); } }, - { "LicenseUrl", [this](const YAML::Node& value) { m_p_localization->LicenseUrl = value.as<std::string>(); } }, - }; - - // Store extension - if (manifestVer.HasExtension(s_MSStoreExtension)) - { - InstallerFieldInfos.emplace_back("ProductId", [this](const YAML::Node& value) { m_p_installer->ProductId = value.as<std::string>(); }); - } - } - - Manifest YamlParser::CreateFromPath(const std::filesystem::path& inputFile, bool fullValidation, bool throwOnWarning) - { - Manifest manifest; - std::vector<ValidationError> errors; - - try - { - YAML::Node rootNode = YAML::Load(inputFile); - YamlParser parser; - errors = parser.ParseManifest(rootNode, manifest, fullValidation); - } - catch (const ManifestException&) - { - // Prevent ManifestException from being wrapped in another ManifestException - throw; - } - catch (const std::exception& e) - { - THROW_EXCEPTION_MSG(ManifestException(), e.what()); - } + if (fullValidation && manifestType == ManifestTypeEnum::Merged) + { + errors.emplace_back(ValidationError::MessageFieldValueWithFile(ManifestError::FieldValueNotSupported, "ManifestType", manifestTypeStr, firstYamlManifest.FileName)); + } - if (!errors.empty()) - { - ManifestException ex{ std::move(errors) }; + if (!schemaValidationOnly && manifestType != ManifestTypeEnum::Merged && manifestType != ManifestTypeEnum::Singleton) + { + errors.emplace_back(ValidationError::MessageWithFile(ManifestError::IncompleteMultiFileManifest, firstYamlManifest.FileName)); + } + } + else + { + firstYamlManifest.ManifestType = ManifestTypeEnum::Preview; + } + } - if (throwOnWarning || !ex.IsWarningOnly()) + if (!errors.empty()) { + ManifestException ex{ std::move(errors) }; THROW_EXCEPTION(ex); } + + return manifestVersion; } - return manifest; - } + // Find a unique required manifest from the input in multi manifest case + const YAML::Node& FindUniqueRequiredDocFromMultiFileManifest(const std::vector<YamlManifestInfo>& input, ManifestTypeEnum manifestType) + { + auto iter = std::find_if(input.begin(), input.end(), + [=](auto const& s) + { + return s.ManifestType == manifestType; + }); - Manifest YamlParser::Create(const std::string& input, bool fullValidation, bool throwOnWarning) - { - Manifest manifest; - std::vector<ValidationError> errors; + THROW_HR_IF(E_UNEXPECTED, iter == input.end()); - try - { - YAML::Node rootNode = YAML::Load(input); - YamlParser parser; - errors = parser.ParseManifest(rootNode, manifest, fullValidation); - } - catch (const ManifestException&) - { - // Prevent ManifestException from being wrapped in another ManifestException - throw; - } - catch (const std::exception& e) - { - THROW_EXCEPTION_MSG(ManifestException(), e.what()); + return iter->Root; } - if (!errors.empty()) + // 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) { - ManifestException ex{ std::move(errors) }; + THROW_HR_IF(E_UNEXPECTED, !input.IsMap()); + THROW_HR_IF(E_UNEXPECTED, !destination.IsMap()); - if (throwOnWarning || !ex.IsWarningOnly()) + const std::vector<std::string> FieldsToIgnore = { "PackageIdentifier", "PackageVersion", "ManifestType", "ManifestVersion" }; + + for (auto const& keyValuePair : input.Mapping()) { - THROW_EXCEPTION(ex); + // We only support string type as key in our manifest + if (std::find(FieldsToIgnore.begin(), FieldsToIgnore.end(), keyValuePair.first.as<std::string>()) == FieldsToIgnore.end()) + { + YAML::Node key = keyValuePair.first; + YAML::Node value = keyValuePair.second; + destination.AddMappingNode(std::move(key), std::move(value)); + } } } - return manifest; - } - - std::vector<ValidationError> YamlParser::ParseManifest(const YAML::Node& rootNode, Manifest& manifest, bool fullValidation) - { - // Detects empty files with a better error. - if (!rootNode.IsMap()) + YAML::Node MergeMultiFileManifest(const std::vector<YamlManifestInfo>& input) { - THROW_EXCEPTION_MSG(ManifestException(APPINSTALLER_CLI_ERROR_INVALID_MANIFEST), "The manifest does not contain a valid root."); - } + // Starts with installer manifest + YAML::Node result = FindUniqueRequiredDocFromMultiFileManifest(input, ManifestTypeEnum::Installer); - // Detect manifest version first to determine expected fields - // Use index to access ManifestVersion directly. If there're duplicates or other general errors, it'll be detected in later - // processing of iterating the whole manifest. - if (rootNode["ManifestVersion"sv]) - { - auto manifestVersionValue = rootNode["ManifestVersion"sv].as<std::string>(); - manifest.ManifestVersion = ManifestVer(manifestVersionValue); - } - else - { - manifest.ManifestVersion = ManifestVer(s_DefaultManifestVersion); - } + // Copy default locale manifest content into manifest root + YAML::Node defaultLocaleManifest = FindUniqueRequiredDocFromMultiFileManifest(input, ManifestTypeEnum::DefaultLocale); + MergeOneManifestToMultiFileManifest(defaultLocaleManifest, result); - // Check manifest version is supported - if (manifest.ManifestVersion.Major() > s_MaxSupportedMajorVersion) - { - THROW_EXCEPTION_MSG(ManifestException(APPINSTALLER_CLI_ERROR_UNSUPPORTED_MANIFESTVERSION), "Unsupported ManifestVersion: %S", manifest.ManifestVersion.ToString().c_str()); - } + // Copy additional locale manifests + YAML::Node localizations{ YAML::Node::Type::Sequence, "", YAML::Mark() }; + for (const auto& entry : input) + { + if (entry.ManifestType == ManifestTypeEnum::Locale) + { + YAML::Node localization{ YAML::Node::Type::Mapping, "", YAML::Mark() }; + MergeOneManifestToMultiFileManifest(entry.Root, localization); + localizations.AddSequenceNode(std::move(localization)); + } + } - PrepareManifestFieldInfos(manifest.ManifestVersion); + if (localizations.size() > 0) + { + YAML::Node key{ YAML::Node::Type::Scalar, "", YAML::Mark() }; + key.SetScalar("Localization"); + result.AddMappingNode(std::move(key), std::move(localizations)); + } - // Populate root fields - YAML::Node switchesNode; - YAML::Node installersNode; - YAML::Node localizationsNode; - m_p_switchesNode = &switchesNode; - m_p_installersNode = &installersNode; - m_p_localizationsNode = &localizationsNode; - m_p_manifest = &manifest; - auto resultErrors = ValidateAndProcessFields(rootNode, RootFieldInfos, fullValidation); + result["ManifestType"sv].SetScalar("merged"); - // Populate root switches - if (!switchesNode.IsNull()) - { - m_p_switches = &manifest.Switches; - auto errors = ValidateAndProcessFields(switchesNode, SwitchesFieldInfos, fullValidation); - std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); + return result; } - // Populate installers - for (std::size_t i = 0; i < installersNode.size(); i++) + void EmitYamlNode(const YAML::Node& input, YAML::Emitter& emitter) { - YAML::Node installerNode = installersNode[i]; - ManifestInstaller installer; - YAML::Node installerSwitchesNode; - - // Populate defaults - installer.InstallerType = manifest.InstallerType; - installer.UpdateBehavior = manifest.UpdateBehavior; - installer.Scope = ManifestInstaller::ScopeEnum::User; - - m_p_installer = &installer; - m_p_switchesNode = &installerSwitchesNode; - auto errors = ValidateAndProcessFields(installerNode, InstallerFieldInfos, fullValidation); - std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); - - // Copy in system reference strings from the root if not set in the installer and appropriate - if (installer.PackageFamilyName.empty() && ManifestInstaller::DoesInstallerTypeUsePackageFamilyName(installer.InstallerType)) + if (input.IsMap()) { - installer.PackageFamilyName = manifest.PackageFamilyName; + emitter << YAML::BeginMap; + for (auto const& keyValuePair : input.Mapping()) + { + emitter << YAML::Key; + EmitYamlNode(keyValuePair.first, emitter); + emitter << YAML::Value; + EmitYamlNode(keyValuePair.second, emitter); + } + emitter << YAML::EndMap; } - - if (installer.ProductCode.empty() && ManifestInstaller::DoesInstallerTypeUseProductCode(installer.InstallerType)) + else if (input.IsSequence()) + { + emitter << YAML::BeginSeq; + for (auto const& value : input.Sequence()) + { + EmitYamlNode(value, emitter); + } + emitter << YAML::EndSeq; + } + else if (input.IsScalar()) { - installer.ProductCode = manifest.ProductCode; + emitter << input.as<std::string>(); } - - // Populate default known switches - installer.Switches = GetDefaultKnownSwitches(installer.InstallerType); - - // Override with switches from manifest root if applicable - for (auto const& keyValuePair : manifest.Switches) + else if (input.IsNull()) { - installer.Switches[keyValuePair.first] = keyValuePair.second; + emitter << ""; } - - // Override with switches from installer declaration if applicable - if (!installerSwitchesNode.IsNull()) + else { - m_p_switches = &installer.Switches; - auto switchesErrors = ValidateAndProcessFields(installerSwitchesNode, SwitchesFieldInfos, fullValidation); - std::move(switchesErrors.begin(), switchesErrors.end(), std::inserter(resultErrors, resultErrors.end())); + THROW_HR(E_UNEXPECTED); } - - manifest.Installers.emplace_back(std::move(installer)); } - // Populate localization fields - if (!localizationsNode.IsNull()) + void OutputYamlDoc(const YAML::Node& input, const std::filesystem::path& out) { - for (std::size_t i = 0; i < localizationsNode.size(); i++) - { - YAML::Node localizationNode = localizationsNode[i]; - ManifestLocalization localization; + THROW_HR_IF(E_UNEXPECTED, !input.IsMap()); - // Populates default values from root first - localization.Description = manifest.Description; - localization.Homepage = manifest.Homepage; - localization.LicenseUrl = manifest.LicenseUrl; + YAML::Emitter emitter; + EmitYamlNode(input, emitter); - m_p_localization = &localization; - auto errors = ValidateAndProcessFields(localizationNode, LocalizationFieldInfos, fullValidation); - std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); - manifest.Localization.emplace_back(std::move(localization)); - } + std::filesystem::create_directories(out.parent_path()); + std::ofstream outFileStream(out); + emitter.Emit(outFileStream); + outFileStream.close(); } - // Extra semantic validations after basic validation and field population - if (fullValidation) + std::vector<ValidationError> ParseManifestImpl( + std::vector<YamlManifestInfo>& input, + Manifest& manifest, + bool fullValidation, + const std::filesystem::path& mergedManifestPath, + bool schemaValidationOnly) { - auto errors = ValidateManifest(manifest); - std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); - } + THROW_HR_IF_MSG(E_INVALIDARG, input.size() == 0, "No manifest file found"); + THROW_HR_IF_MSG(E_INVALIDARG, schemaValidationOnly && !mergedManifestPath.empty(), "Manifest cannot be merged if only schema validation is performed"); + THROW_HR_IF_MSG(E_INVALIDARG, input.size() == 1 && !mergedManifestPath.empty(), "Manifest cannot be merged from a single manifest"); - return resultErrors; - } + auto manifestVersion = ValidateInput(input, fullValidation, schemaValidationOnly); - std::vector<ValidationError> YamlParser::ValidateAndProcessFields( - const YAML::Node& rootNode, - const std::vector<ManifestFieldInfo>& fieldInfos, - bool fullValidation) - { - std::vector<ValidationError> errors; + std::vector<ValidationError> resultErrors; - if (rootNode.size() == 0 || !rootNode.IsMap()) - { - errors.emplace_back(ManifestError::InvalidRootNode, "", "", rootNode.Mark().line, rootNode.Mark().column); - return errors; - } + if (fullValidation || schemaValidationOnly) + { + resultErrors = ValidateAgainstSchema(input, manifestVersion); + } - // Keeps track of already processed fields. Used to check duplicate fields or missing required fields. - std::set<std::string> processedFields; + if (schemaValidationOnly) + { + return resultErrors; + } - for (auto const& keyValuePair : rootNode.Mapping()) - { - std::string key = keyValuePair.first.as<std::string>(); - const YAML::Node& valueNode = keyValuePair.second; + // Merge manifests in multi file manifest case + const YAML::Node& manifestDoc = (input.size() > 1) ? MergeMultiFileManifest(input) : input[0].Root; - // We'll do case insensitive search first and validate correct case later. - auto fieldIter = std::find_if(fieldInfos.begin(), fieldInfos.end(), - [&](auto const& s) - { - return Utility::CaseInsensitiveEquals(s.Name, key); - }); + auto errors = ManifestYamlPopulator::PopulateManifest(manifestDoc, manifest, manifestVersion, fullValidation); + std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); - if (fieldIter != fieldInfos.end()) + // Extra semantic validations after basic validation and field population + if (fullValidation) { - const ManifestFieldInfo& fieldInfo = *fieldIter; + errors = ValidateManifest(manifest); + std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); + } - // Make sure the found key is in Pascal Case - if (key != fieldInfo.Name) - { - errors.emplace_back(ManifestError::FieldIsNotPascalCase, key, "", keyValuePair.first.Mark().line, keyValuePair.first.Mark().column); - } + // Output merged manifest if requested + if (!mergedManifestPath.empty()) + { + OutputYamlDoc(manifestDoc, mergedManifestPath); + } - // Make sure it's not a duplicate key - if (!processedFields.insert(fieldInfo.Name).second) - { - errors.emplace_back(ManifestError::FieldDuplicate, fieldInfo.Name, "", keyValuePair.first.Mark().line, keyValuePair.first.Mark().column); - } + return resultErrors; + } + } - // Validate non empty value is provided for required fields - if (fieldInfo.Required) - { - if (!valueNode.IsDefined() || valueNode.IsNull() || // Should be defined and not null - (valueNode.IsScalar() && valueNode.as<std::string>().empty()) || // Scalar type should have content - ((valueNode.IsMap() || valueNode.IsSequence()) && valueNode.size() == 0)) // Map or sequence type should have size greater than 0 - { - errors.emplace_back(ManifestError::RequiredFieldEmpty, fieldInfo.Name, "", valueNode.Mark().line, valueNode.Mark().column); - } - } + Manifest CreateFromPath( + const std::filesystem::path& inputPath, + bool fullValidation, + bool throwOnWarning, + const std::filesystem::path& mergedManifestPath, + bool schemaValidationOnly) + { + std::vector<YamlManifestInfo> docList; - // Validate value against regex if applicable - if (fullValidation && !fieldInfo.RegEx.empty()) + try + { + if (std::filesystem::is_directory(inputPath)) + { + for (const auto& file : std::filesystem::directory_iterator(inputPath)) { - std::string value = valueNode.as<std::string>(); - std::regex pattern{ fieldInfo.RegEx }; - if (!std::regex_match(value, pattern)) - { - errors.emplace_back(ManifestError::InvalidFieldValue, fieldInfo.Name, value, valueNode.Mark().line, valueNode.Mark().column); - continue; - } - } + THROW_HR_IF_MSG(HRESULT_FROM_WIN32(ERROR_DIRECTORY_NOT_SUPPORTED), std::filesystem::is_directory(file.path()), "Subdirectory not supported in manifest path"); - if (!valueNode.IsNull()) - { - fieldInfo.ProcessFunc(valueNode); + YamlManifestInfo doc; + doc.Root = YAML::Load(file.path()); + doc.FileName = file.path().filename().u8string(); + docList.emplace_back(std::move(doc)); } } else { - // For full validation, also reports unrecognized fields as warning - if (fullValidation) - { - errors.emplace_back(ManifestError::FieldUnknown, key, "", keyValuePair.first.Mark().line, keyValuePair.first.Mark().column, ValidationError::Level::Warning); - } + YamlManifestInfo doc; + doc.Root = YAML::Load(inputPath); + doc.FileName = inputPath.filename().u8string(); + docList.emplace_back(std::move(doc)); } } + catch (const std::exception& e) + { + THROW_EXCEPTION_MSG(ManifestException(), e.what()); + } + + return ParseManifest(docList, fullValidation, throwOnWarning, mergedManifestPath, schemaValidationOnly); + } + + Manifest Create( + const std::string& input, + bool fullValidation, + bool throwOnWarning, + const std::filesystem::path& mergedManifestPath, + bool schemaValidationOnly) + { + std::vector<YamlManifestInfo> docList; - // Make sure required fields are provided - for (auto const& fieldInfo : fieldInfos) + try { - if (fieldInfo.Required && processedFields.find(fieldInfo.Name) == processedFields.end()) - { - errors.emplace_back(ManifestError::RequiredFieldMissing, fieldInfo.Name); - } + YamlManifestInfo doc; + doc.Root = YAML::Load(input); + docList.emplace_back(std::move(doc)); + } + catch (const std::exception& e) + { + THROW_EXCEPTION_MSG(ManifestException(), e.what()); } - return errors; + return ParseManifest(docList, fullValidation, throwOnWarning, mergedManifestPath, schemaValidationOnly); } - std::map<ManifestInstaller::InstallerSwitchType, ManifestInstaller::string_t> YamlParser::GetDefaultKnownSwitches( - ManifestInstaller::InstallerTypeEnum installerType) + Manifest ParseManifest( + std::vector<YamlManifestInfo>& input, + bool fullValidation, + bool throwOnWarning, + const std::filesystem::path& mergedManifestPath, + bool schemaValidationOnly) { - switch (installerType) + Manifest manifest; + std::vector<ValidationError> errors; + + try { - case ManifestInstaller::InstallerTypeEnum::Burn: - case ManifestInstaller::InstallerTypeEnum::Wix: - case ManifestInstaller::InstallerTypeEnum::Msi: - return - { - {ManifestInstaller::InstallerSwitchType::Silent, ManifestInstaller::string_t("/quiet")}, - {ManifestInstaller::InstallerSwitchType::SilentWithProgress, ManifestInstaller::string_t("/passive")}, - {ManifestInstaller::InstallerSwitchType::Log, ManifestInstaller::string_t("/log \"" + std::string(ARG_TOKEN_LOGPATH) + "\"")}, - {ManifestInstaller::InstallerSwitchType::InstallLocation, ManifestInstaller::string_t("TARGETDIR=\"" + std::string(ARG_TOKEN_INSTALLPATH) + "\"")}, - {ManifestInstaller::InstallerSwitchType::Update, ManifestInstaller::string_t("REINSTALL=ALL REINSTALLMODE=vamus")} - }; - case ManifestInstaller::InstallerTypeEnum::Nullsoft: - return - { - {ManifestInstaller::InstallerSwitchType::Silent, ManifestInstaller::string_t("/S")}, - {ManifestInstaller::InstallerSwitchType::SilentWithProgress, ManifestInstaller::string_t("/S")}, - {ManifestInstaller::InstallerSwitchType::InstallLocation, ManifestInstaller::string_t("/D=" + std::string(ARG_TOKEN_INSTALLPATH))} - }; - case ManifestInstaller::InstallerTypeEnum::Inno: - return + errors = ParseManifestImpl(input, manifest, fullValidation, mergedManifestPath, schemaValidationOnly); + } + catch (const ManifestException&) + { + // Prevent ManifestException from being wrapped in another ManifestException + throw; + } + catch (const std::exception& e) + { + THROW_EXCEPTION_MSG(ManifestException(), e.what()); + } + + if (!errors.empty()) + { + ManifestException ex{ std::move(errors) }; + + if (throwOnWarning || !ex.IsWarningOnly()) { - {ManifestInstaller::InstallerSwitchType::Silent, ManifestInstaller::string_t("/VERYSILENT")}, - {ManifestInstaller::InstallerSwitchType::SilentWithProgress, ManifestInstaller::string_t("/SILENT")}, - {ManifestInstaller::InstallerSwitchType::Log, ManifestInstaller::string_t("/LOG=\"" + std::string(ARG_TOKEN_LOGPATH) + "\"")}, - {ManifestInstaller::InstallerSwitchType::InstallLocation, ManifestInstaller::string_t("/DIR=\"" + std::string(ARG_TOKEN_INSTALLPATH) + "\"")} - }; - default: - return {}; + THROW_EXCEPTION(ex); + } } + + return manifest; } } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/AppInstallerStrings.h b/src/AppInstallerCommonCore/Public/AppInstallerStrings.h @@ -122,6 +122,7 @@ namespace AppInstaller::Utility // Removes whitespace from the beginning and end of the string. std::string& Trim(std::string& str); + std::string Trim(std::string&& str); // Removes whitespace from the beginning and end of the string. std::wstring& Trim(std::wstring& str); diff --git a/src/AppInstallerCommonCore/Public/winget/Manifest.h b/src/AppInstallerCommonCore/Public/winget/Manifest.h @@ -2,7 +2,6 @@ // Licensed under the MIT License. #pragma once #include <AppInstallerStrings.h> -#include <AppInstallerVersions.h> #include <winget/ManifestInstaller.h> #include <winget/ManifestLocalization.h> @@ -10,90 +9,39 @@ namespace AppInstaller::Manifest { - // ManifestVer is inherited from Utility::Version and is a more restricted version. - // ManifestVer is used to specify the version of app manifest itself. - // ManifestVer is a 3 part version in the format of [0-65535].[0-65535].[0-65535] - // and optionally a following tag in the format of -[SomeString] for experimental purpose. - struct ManifestVer : public Utility::Version - { - ManifestVer() = default; - - ManifestVer(std::string_view version); - - uint64_t Major() const { return m_parts.size() > 0 ? m_parts[0].Integer : 0; } - uint64_t Minor() const { return m_parts.size() > 1 ? m_parts[1].Integer : 0; } - uint64_t Patch() const { return m_parts.size() > 2 ? m_parts[2].Integer : 0; } - - bool HasExtension() const; - - bool HasExtension(std::string_view extension) const; - - private: - std::vector<Version> m_extensions; - }; - // Representation of the parsed manifest file. struct Manifest { using string_t = Utility::NormalizedString; - // Required string_t Id; - // Required - string_t Name; - - // Required string_t Version; - // Required - string_t Publisher; - - string_t AppMoniker; - string_t Channel; - string_t Author; - - string_t License; + string_t Moniker; - string_t MinOSVersion; - - // Comma separated values - std::vector<string_t> Tags; - - // Comma separated values - std::vector<string_t> Commands; - - // Comma separated values - std::vector<string_t> Protocols; - - // Comma separated values - std::vector<string_t> FileExtensions; - - ManifestInstaller::InstallerTypeEnum InstallerType = ManifestInstaller::InstallerTypeEnum::Unknown; - - // Default is Install if not specified - ManifestInstaller::UpdateBehaviorEnum UpdateBehavior = ManifestInstaller::UpdateBehaviorEnum::Install; + ManifestVer ManifestVersion; - // Package family name for MSIX packaged installers. - string_t PackageFamilyName; + ManifestInstaller DefaultInstallerInfo; - // Product code for ARP (Add/Remove Programs) installers. - string_t ProductCode; + std::vector<ManifestInstaller> Installers; - string_t Description; + ManifestLocalization DefaultLocalization; - string_t Homepage; + std::vector<ManifestLocalization> Localizations; - string_t LicenseUrl; + ManifestLocalization CurrentLocalization; - ManifestVer ManifestVersion; + // ApplyLocale will update the CurrentLocalization according to the specified locale + // If locale is empty, user setting locale will be used + void ApplyLocale(const std::string& locale = {}); - std::map<ManifestInstaller::InstallerSwitchType, string_t> Switches; - - std::vector<ManifestInstaller> Installers; + // Get all tags across localizations + std::vector<string_t> GetAggregatedTags() const; - std::vector<ManifestLocalization> Localization; + // Get all commands across installers + std::vector<string_t> GetAggregatedCommands() const; }; } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <AppInstallerStrings.h> +#include <AppInstallerVersions.h> + +#include <map> + +namespace AppInstaller::Manifest +{ + using string_t = Utility::NormalizedString; + using namespace std::string_view_literals; + + // The maximum supported major version known about by this code. + constexpr uint64_t s_MaxSupportedMajorVersion = 1; + + // The default manifest version assigned to manifests without a ManifestVersion field. + constexpr std::string_view s_DefaultManifestVersion = "0.1.0"sv; + + // V1 manifest version for GA + constexpr std::string_view s_ManifestVersionV1 = "1.0.0"sv; + + // The manifest extension for the MS Store + constexpr std::string_view s_MSStoreExtension = "msstore"sv; + + // ManifestVer is inherited from Utility::Version and is a more restricted version. + // ManifestVer is used to specify the version of app manifest itself. + // ManifestVer is a 3 part version in the format of [0-65535].[0-65535].[0-65535] + // and optionally a following tag in the format of -[SomeString] for experimental purpose. + struct ManifestVer : public Utility::Version + { + ManifestVer() = default; + + ManifestVer(std::string_view version); + + uint64_t Major() const { return m_parts.size() > 0 ? m_parts[0].Integer : 0; } + uint64_t Minor() const { return m_parts.size() > 1 ? m_parts[1].Integer : 0; } + uint64_t Patch() const { return m_parts.size() > 2 ? m_parts[2].Integer : 0; } + + bool HasExtension() const; + + bool HasExtension(std::string_view extension) const; + + private: + std::vector<Version> m_extensions; + }; + + enum class InstallerTypeEnum + { + Unknown, + Inno, + Wix, + Msi, + Nullsoft, + Zip, + Msix, + Exe, + Burn, + MSStore, + }; + + enum class UpdateBehaviorEnum + { + Unknown, + Install, + UninstallPrevious, + }; + + enum class InstallerSwitchType + { + Custom, + Silent, + SilentWithProgress, + Interactive, + Language, + Log, + InstallLocation, + Update + }; + + enum class ScopeEnum + { + Unknown, + User, + Machine, + }; + + enum class PlatformEnum + { + Unknown, + Universal, + Desktop, + }; + + enum class ManifestTypeEnum + { + Singleton, + Version, + Installer, + DefaultLocale, + Locale, + Merged, + Preview + }; + + struct PackageDependency + { + string_t Id; + string_t MinVersion; + }; + + struct Dependency + { + std::vector<string_t> WindowsFeatures; + std::vector<string_t> WindowsLibraries; + std::vector<PackageDependency> PackageDependencies; + std::vector<string_t> ExternalDependencies; + }; + + + InstallerTypeEnum ConvertToInstallerTypeEnum(const std::string& in); + + UpdateBehaviorEnum ConvertToUpdateBehaviorEnum(const std::string& in); + + ScopeEnum ConvertToScopeEnum(const std::string& in); + + PlatformEnum ConvertToPlatformEnum(const std::string& in); + + ManifestTypeEnum ConvertToManifestTypeEnum(const std::string& in); + + std::string_view InstallerTypeToString(InstallerTypeEnum installerType); + + std::string_view ScopeToString(ScopeEnum scope); + + // Gets a value indicating whether the given installer type uses the PackageFamilyName system reference. + bool DoesInstallerTypeUsePackageFamilyName(InstallerTypeEnum installerType); + + // Gets a value indicating whether the given installer type uses the ProductCode system reference. + bool DoesInstallerTypeUseProductCode(InstallerTypeEnum installerType); + + // Checks whether 2 installer types are compatible. E.g. inno and exe are update compatible + bool IsInstallerTypeCompatible(InstallerTypeEnum type1, InstallerTypeEnum type2); + + // Get a list of default switches for known installer types + std::map<InstallerSwitchType, Utility::NormalizedString> GetDefaultKnownSwitches(InstallerTypeEnum installerType); +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h b/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h @@ -3,6 +3,7 @@ #pragma once #include <AppInstallerArchitecture.h> #include <AppInstallerStrings.h> +#include <winget\ManifestCommon.h> #include <map> #include <string> @@ -19,100 +20,57 @@ namespace AppInstaller::Manifest { using string_t = Utility::NormalizedString; - enum class InstallerTypeEnum - { - Unknown, - Inno, - Wix, - Msi, - Nullsoft, - Zip, - Msix, - Exe, - Burn, - MSStore, - }; - - enum class UpdateBehaviorEnum - { - Unknown, - Install, - UninstallPrevious, - }; - - enum class InstallerSwitchType - { - Custom, - Silent, - SilentWithProgress, - Interactive, - Language, - Log, - InstallLocation, - Update - }; - - enum class ScopeEnum - { - Unknown, - User, - Machine, - }; - - // Required. Values: x86, x64, arm, arm64, all. AppInstaller::Utility::Architecture Arch; - // Required string_t Url; - // Required std::vector<BYTE> Sha256; // Optional. Only used by appx/msix type. If provided, Appinstaller will // validate appx/msix signature and perform streaming install. std::vector<BYTE> SignatureSha256; - // Empty means default - string_t Language; - - // Name TBD - ScopeEnum Scope; - // Store Product Id string_t ProductId; - // Package family name for MSIX packaged installers. - string_t PackageFamilyName; + string_t Locale; - // Product code for ARP (Add/Remove Programs) installers. - string_t ProductCode; + std::vector<PlatformEnum> Platform; + + string_t MinOSVersion; // If present, has more precedence than root - InstallerTypeEnum InstallerType; + InstallerTypeEnum InstallerType = InstallerTypeEnum::Unknown; - // Default is Install if not specified - UpdateBehaviorEnum UpdateBehavior; + ScopeEnum Scope = ScopeEnum::User; + + std::vector<ScopeEnum> InstallModes; // If present, has more precedence than root std::map<InstallerSwitchType, string_t> Switches; - static InstallerTypeEnum ConvertToInstallerTypeEnum(const std::string& in); + std::vector<int> InstallerSuccessCodes; + + UpdateBehaviorEnum UpdateBehavior = UpdateBehaviorEnum::Install; - static UpdateBehaviorEnum ConvertToUpdateBehaviorEnum(const std::string& in); + std::vector<string_t> Commands; - static ScopeEnum ConvertToScopeEnum(const std::string& in); + std::vector<string_t> Protocols; - static std::string_view InstallerTypeToString(InstallerTypeEnum installerType); + std::vector<string_t> FileExtensions; - static std::string_view ScopeToString(ScopeEnum scope); + // Package family name for MSIX packaged installers. + string_t PackageFamilyName; + + // Product code for ARP (Add/Remove Programs) installers. + string_t ProductCode; - // Gets a value indicating whether the given installer type uses the PackageFamilyName system reference. - static bool DoesInstallerTypeUsePackageFamilyName(InstallerTypeEnum installerType); + // For msix only + std::vector<string_t> Capabilities; - // Gets a value indicating whether the given installer type uses the ProductCode system reference. - static bool DoesInstallerTypeUseProductCode(InstallerTypeEnum installerType); + // For msix only + std::vector<string_t> RestrictedCapabilities; - // Checks whether 2 installer types are compatible. E.g. inno and exe are update compatible - static bool IsInstallerTypeCompatible(InstallerTypeEnum type1, InstallerTypeEnum type2); + Dependency Dependencies; }; } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestLocalization.h b/src/AppInstallerCommonCore/Public/winget/ManifestLocalization.h @@ -3,20 +3,91 @@ #pragma once #include <AppInstallerStrings.h> +#include <variant> + namespace AppInstaller::Manifest { - class ManifestLocalization + using string_t = Utility::NormalizedString; + + enum class Localization : size_t + { + Publisher, + PublisherUrl, + PublisherSupportUrl, + PrivacyUrl, + Author, + PackageName, + PackageUrl, + License, + LicenseUrl, + Copyright, + CopyrightUrl, + ShortDescription, + Description, + Tags, + Max + }; + + namespace details + { + template <Localization L> + struct LocalizationMapping + { + using value_t = string_t; + }; + + template <> + struct LocalizationMapping<Localization::Tags> + { + using value_t = std::vector<string_t>; + }; + + // Used to deduce the LocalizationVariant type; making a variant that includes std::monostate and all LocalizationMapping types. + template <size_t... I> + inline auto Deduce(std::index_sequence<I...>) { return std::variant<std::monostate, LocalizationMapping<static_cast<Localization>(I)>::value_t...>{}; } + + // Holds data of any type listed in a LocalizationMapping. + using LocalizationVariant = decltype(Deduce(std::make_index_sequence<static_cast<size_t>(Localization::Max)>())); + + // Gets the index into the variant for the given Localization. + constexpr inline size_t LocalizationIndex(Localization l) { return static_cast<size_t>(l) + 1; } + } + + struct ManifestLocalization { - public: - using string_t = Utility::NormalizedString; + string_t Locale; - // Required - string_t Language; + // Adds a value to the Localization data, or overwrites an existing entry. + template <Localization L> + void Add(typename details::LocalizationMapping<L>::value_t&& v) + { + m_data[L].emplace<details::LocalizationIndex(L)>(std::forward<typename details::LocalizationMapping<L>::value_t>(v)); + } + template <Localization L> + void Add(const typename details::LocalizationMapping<L>::value_t& v) + { + m_data[L].emplace<details::LocalizationIndex(L)>(v); + } - string_t Description; + // Return a value indicating whether the given localization type exists. + bool Contains(Localization l) { return (m_data.find(l) != m_data.end()); } - string_t Homepage; + // Gets the localization value if exists, otherwise empty for easier access + template <Localization L> + typename details::LocalizationMapping<L>::value_t Get() const + { + auto itr = m_data.find(L); + if (itr == m_data.end()) + { + return {}; + } + else + { + return std::get<details::LocalizationIndex(L)>(itr->second); + } + } - string_t LicenseUrl; + private: + std::map<Localization, details::LocalizationVariant> m_data; }; } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestSchemaValidation.h b/src/AppInstallerCommonCore/Public/winget/ManifestSchemaValidation.h @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ManifestCommon.h" +#include "ManifestValidation.h" + +#include <json.h> + +namespace AppInstaller::Manifest::YamlParser +{ + // Forward declarations + struct YamlManifestInfo; + + // Load an embedded resource from binary and return as std::string + std::string LoadResourceAsString(PCWSTR resourceName, PCWSTR resourceType); + + // Load manifest schema as parsed json doc + Json::Value LoadSchemaDoc(const ManifestVer& manifestVersion, ManifestTypeEnum manifestType); + + // Validate a list of individual manifests against schema + std::vector<ValidationError> ValidateAgainstSchema( + const std::vector<YamlManifestInfo>& manifestList, + const ManifestVer& manifestVersion); +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h b/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h @@ -31,6 +31,13 @@ namespace AppInstaller::Manifest const char* const DuplicateInstallerEntry = "Duplicate installer entry found."; const char* const InstallerTypeDoesNotSupportPackageFamilyName = "The specified installer type does not support PackageFamilyName."; const char* const InstallerTypeDoesNotSupportProductCode = "The specified installer type does not support ProductCode."; + const char* const IncompleteMultiFileManifest = "The multi file manifest is incomplete."; + const char* const InconsistentMultiFileManifestFieldValue = "The multi file manifest has inconsistent field values."; + const char* const DuplicateMultiFileManifestType = "The multi file manifest should contain only one file with the particular ManifestType."; + const char* const DuplicateMultiFileManifestLocale = "The multi file manifest contains duplicate PackageLocale."; + const char* const UnsupportedMultiFileManifestType = "The multi file manifest should not contain file with the particular ManifestType."; + const char* const InconsistentMultiFileManifestDefaultLocale = "DefaultLocale value in version manifest does not match PackageLocale value in defaultLocale manifest."; + const char* const FieldFailedToProcess = "Failed to process field."; } struct ValidationError @@ -48,6 +55,7 @@ namespace AppInstaller::Manifest size_t Line = 0; size_t Column = 0; Level ErrorLevel = Level::Error; + std::string FileName; ValidationError(std::string message) : Message(std::move(message)) {} @@ -69,6 +77,20 @@ namespace AppInstaller::Manifest ValidationError(std::string message, std::string field, std::string value, size_t line, size_t column, Level level) : Message(std::move(message)), Field(std::move(field)), Value(std::move(value)), Line(line), Column(column), ErrorLevel(level) {} + + static ValidationError MessageWithFile(std::string message, std::string file) + { + ValidationError error{ message }; + error.FileName = file; + return error; + } + + static ValidationError MessageFieldValueWithFile(std::string message, std::string field, std::string value, std::string file) + { + ValidationError error{ message, field, value }; + error.FileName = file; + return error; + } }; struct ManifestException : public wil::ResultException @@ -121,6 +143,10 @@ namespace AppInstaller::Manifest { m_manifestErrorMessage += " Line: " + std::to_string(error.Line) + ", Column: " + std::to_string(error.Column); } + if (!error.FileName.empty()) + { + m_manifestErrorMessage += " File: " + error.FileName; + } m_manifestErrorMessage += '\n'; } } diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestYamlParser.h b/src/AppInstallerCommonCore/Public/winget/ManifestYamlParser.h @@ -3,61 +3,46 @@ #pragma once #include <winget/ManifestValidation.h> #include <winget/Manifest.h> +#include <winget/Yaml.h> #include <filesystem> -namespace AppInstaller::Manifest +namespace AppInstaller::Manifest::YamlParser { - struct YamlParser + struct YamlManifestInfo { - // fullValidation: Bool to set if manifest creation should perform extra validation that client does not need. - // e.g. Channel should be null. Client code does not need this check to work properly. - // throwOnWarning: Bool to indicate if an exception should be thrown with only warnings detected in the manifest. - static Manifest CreateFromPath(const std::filesystem::path& inputFile, bool fullValidation = false, bool throwOnWarning = false); + // Root node of a yaml manifest file + YAML::Node Root; - static Manifest Create(const std::string& input, bool fullValidation = false, bool throwOnWarning = false); + // File name of the manifest file if applicable for error reporting + std::string FileName; - private: - // These pointers are referenced in the processing functions in manifest field info table. - YAML::Node* m_p_installersNode = nullptr; - YAML::Node* m_p_switchesNode = nullptr; - YAML::Node* m_p_localizationsNode = nullptr; - AppInstaller::Manifest::Manifest* m_p_manifest = nullptr; - AppInstaller::Manifest::ManifestInstaller* m_p_installer = nullptr; - std::map<ManifestInstaller::InstallerSwitchType, Utility::NormalizedString>* m_p_switches = nullptr; - AppInstaller::Manifest::ManifestLocalization* m_p_localization = nullptr; - - // This struct contains individual app manifest field info - struct ManifestFieldInfo - { - ManifestFieldInfo(std::string name, std::function<void(const YAML::Node&)> func, bool required = false, std::string regex = {}) : - Name(std::move(name)), ProcessFunc(func), Required(required), RegEx(std::move(regex)) {} - - std::string Name; - std::function<void(const YAML::Node&)> ProcessFunc; - bool Required = false; - std::string RegEx = {}; - }; - - std::vector<ManifestFieldInfo> RootFieldInfos; - std::vector<ManifestFieldInfo> InstallerFieldInfos; - std::vector<ManifestFieldInfo> SwitchesFieldInfos; - std::vector<ManifestFieldInfo> LocalizationFieldInfos; - - std::vector<ValidationError> ParseManifest(const YAML::Node& rootNode, Manifest& manifest, bool fullValidation); - - static std::map<ManifestInstaller::InstallerSwitchType, Utility::NormalizedString> GetDefaultKnownSwitches( - ManifestInstaller::InstallerTypeEnum installerType); - - // This method takes YAML root node and list of manifest field info. - // Yaml-cpp does not support case insensitive search and it allows duplicate keys. If duplicate keys exist, - // the value is undefined. So in this method, we will iterate through the node map and process each individual - // pair ourselves. This also helps with generating aggregated error rather than throwing on first failure. - static std::vector<ValidationError> ValidateAndProcessFields( - const YAML::Node& rootNode, - const std::vector<ManifestFieldInfo>& fieldInfos, - bool fullValidation); - - void PrepareManifestFieldInfos(const ManifestVer& manifestVer); + ManifestTypeEnum ManifestType; }; + + // fullValidation: Bool to set if manifest creation should perform extra validation that client does not need. + // e.g. Channel should be null. Client code does not need this check to work properly. + // throwOnWarning: Bool to indicate if an exception should be thrown with only warnings detected in the manifest. + // mergedManifestPath: Output file for merged manifest after processing a multi file manifest + // schemaValidationOnly: Bool to indicate if only schema validation should be performed + Manifest CreateFromPath( + const std::filesystem::path& inputPath, + bool fullValidation = false, + bool throwOnWarning = false, + const std::filesystem::path& mergedManifestPath = {}, + bool schemaValidationOnly = false); + + Manifest Create( + const std::string& input, + bool fullValidation = false, + bool throwOnWarning = false, + const std::filesystem::path& mergedManifestPath = {}, + bool schemaValidationOnly = false); + + Manifest ParseManifest( + std::vector<YamlManifestInfo>& input, + bool fullValidation = false, + bool throwOnWarning = false, + const std::filesystem::path& mergedManifestPath = {}, + bool schemaValidationOnly = false); } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestYamlPopulator.h b/src/AppInstallerCommonCore/Public/winget/ManifestYamlPopulator.h @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <winget/Manifest.h> +#include <winget/ManifestValidation.h> +#include <winget/Yaml.h> + +namespace AppInstaller::Manifest +{ + struct ManifestYamlPopulator + { + static std::vector<ValidationError> PopulateManifest(const YAML::Node& rootNode, Manifest& manifest, const ManifestVer& manifestVersion, bool fullValidation); + + private: + + bool m_fullValidation = false; + bool m_isMergedManifest = false; + + // Struct mapping a manifest field to its population logic + struct FieldProcessInfo + { + FieldProcessInfo(std::string name, std::function<std::vector<ValidationError>(const YAML::Node&)> func) : + Name(std::move(name)), ProcessFunc(func) {} + + std::string Name; + std::function<std::vector<ValidationError>(const YAML::Node&)> ProcessFunc; + }; + + std::vector<FieldProcessInfo> RootFieldInfos; + std::vector<FieldProcessInfo> InstallerFieldInfos; + std::vector<FieldProcessInfo> SwitchesFieldInfos; + std::vector<FieldProcessInfo> DependenciesFieldInfos; + std::vector<FieldProcessInfo> PackageDependenciesFieldInfos; + std::vector<FieldProcessInfo> LocalizationFieldInfos; + + // These pointers are referenced in the processing functions in manifest field process info table. + AppInstaller::Manifest::Manifest* m_p_manifest = nullptr; + AppInstaller::Manifest::ManifestInstaller* m_p_installer = nullptr; + std::map<InstallerSwitchType, Utility::NormalizedString>* m_p_switches = nullptr; + AppInstaller::Manifest::Dependency* m_p_dependency = nullptr; + AppInstaller::Manifest::PackageDependency* m_p_packageDependency = nullptr; + AppInstaller::Manifest::ManifestLocalization* m_p_localization = nullptr; + + // 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> GetDependenciesFieldProcessInfo(const ManifestVer& manifestVersion); + std::vector<FieldProcessInfo> GetPackageDependenciesFieldProcessInfo(const ManifestVer& manifestVersion); + std::vector<FieldProcessInfo> GetLocalizationFieldProcessInfo(const ManifestVer& manifestVersion, bool forRootFields = false); + + // 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, + // the value is undefined. So in this method, we will iterate through the node map and process each individual + // pair ourselves. This also helps with generating aggregated error rather than throwing on first failure. + std::vector<ValidationError> ValidateAndProcessFields( + const YAML::Node& rootNode, + const std::vector<FieldProcessInfo>& fieldInfos); + + std::vector<ValidationError> ProcessPackageDependenciesNode(const YAML::Node& rootNode, std::vector<PackageDependency>& packageDependencies); + + std::vector<ValidationError> PopulateManifestInternal(const YAML::Node& rootNode, Manifest& manifest, const ManifestVer& manifestVersion, bool fullValidation); + }; +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/Yaml.h b/src/AppInstallerCommonCore/Public/winget/Yaml.h @@ -140,6 +140,7 @@ namespace AppInstaller::YAML // The workers for the as function. std::string as_dispatch(std::string*) const; int64_t as_dispatch(int64_t*) const; + int as_dispatch(int*) const; bool as_dispatch(bool*) const; Type m_type; @@ -195,6 +196,9 @@ namespace AppInstaller::YAML // Gets the result of the emitter; can only be retrieved once. std::string str(); + // Gets the result of the emitter to out stream; can only be retrieved once. + void Emit(std::ostream& out); + private: // Appends the given node to the current container if applicable. void AppendNode(int id); diff --git a/src/AppInstallerCommonCore/Yaml.cpp b/src/AppInstallerCommonCore/Yaml.cpp @@ -244,6 +244,12 @@ namespace AppInstaller::YAML return std::stoll(m_scalar); } + int Node::as_dispatch(int*) const + { + // To allow HResult representation + return static_cast<int>(std::stoll(m_scalar, 0, 0)); + } + bool Node::as_dispatch(bool*) const { if (Utility::CaseInsensitiveEquals(m_scalar, "true")) @@ -418,6 +424,14 @@ namespace AppInstaller::YAML return stream.str(); } + void Emitter::Emit(std::ostream& out) + { + Wrapper::Emitter emitter(out); + + emitter.Dump(*m_document); + emitter.Flush(); + } + void Emitter::AppendNode(int id) { if (!m_containers.empty()) diff --git a/src/AppInstallerCommonCore/pch.h b/src/AppInstallerCommonCore/pch.h @@ -17,6 +17,16 @@ #define YAML_DECLARE_STATIC #include <yaml.h> +#include <json.h> + +#pragma warning( push ) +#pragma warning ( disable : 4458 4100 4702 ) +#include <valijson/schema.hpp> +#include <valijson/schema_parser.hpp> +#include <valijson/validator.hpp> +#include <valijson/adapters/jsoncpp_adapter.hpp> +#pragma warning( pop ) + #include <algorithm> #include <chrono> #include <cwctype> diff --git a/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp b/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp @@ -5,16 +5,16 @@ namespace AppInstaller::Repository::Microsoft { - Registry::Key ARPHelper::GetARPKey(Manifest::ManifestInstaller::ScopeEnum scope, Utility::Architecture architecture) const + Registry::Key ARPHelper::GetARPKey(Manifest::ScopeEnum scope, Utility::Architecture architecture) const { HKEY rootKey = NULL; switch (scope) { - case Manifest::ManifestInstaller::ScopeEnum::User: + case Manifest::ScopeEnum::User: rootKey = HKEY_CURRENT_USER; break; - case Manifest::ManifestInstaller::ScopeEnum::Machine: + case Manifest::ScopeEnum::Machine: rootKey = HKEY_LOCAL_MACHINE; break; default: @@ -38,7 +38,7 @@ namespace AppInstaller::Repository::Microsoft switch (architecture) { case Utility::Architecture::X86: - if (scope == Manifest::ManifestInstaller::ScopeEnum::Machine) + if (scope == Manifest::ScopeEnum::Machine) { access |= KEY_WOW64_32KEY; isValid = true; @@ -62,7 +62,7 @@ namespace AppInstaller::Repository::Microsoft switch (architecture) { case Utility::Architecture::X86: - if (scope == Manifest::ManifestInstaller::ScopeEnum::Machine) + if (scope == Manifest::ScopeEnum::Machine) { #ifdef _ARM_ // Not accessible if this is an ARM process @@ -172,7 +172,7 @@ namespace AppInstaller::Repository::Microsoft } } - void ARPHelper::PopulateIndexFromARP(SQLiteIndex& index, Manifest::ManifestInstaller::ScopeEnum scope) const + void ARPHelper::PopulateIndexFromARP(SQLiteIndex& index, Manifest::ScopeEnum scope) const { for (auto architecture : Utility::GetApplicableArchitectures()) { @@ -180,7 +180,7 @@ namespace AppInstaller::Repository::Microsoft if (arpRootKey) { - PopulateIndexFromKey(index, arpRootKey, Manifest::ManifestInstaller::ScopeToString(scope), Utility::ToString(architecture)); + PopulateIndexFromKey(index, arpRootKey, Manifest::ScopeToString(scope), Utility::ToString(architecture)); } } } @@ -194,7 +194,7 @@ namespace AppInstaller::Repository::Microsoft std::string productCode = arpEntry.Name(); Manifest::Manifest manifest; - manifest.Tags = { "ARP" }; + manifest.DefaultLocalization.Add<Manifest::Localization::Tags>({ "ARP" }); // Use the key name as the Id, as it is supposed to be unique. // TODO: We probably want something better here, like constructing the value as @@ -226,8 +226,9 @@ namespace AppInstaller::Repository::Microsoft AICLI_LOG(Repo, Verbose, << "Skipping " << productCode << " because DisplayName is not a REG_SZ value"); continue; } - manifest.Name = displayName->GetValue<Registry::Value::Type::String>(); - if (manifest.Name.empty()) + auto displayNameValue = displayName->GetValue<Registry::Value::Type::String>(); + manifest.DefaultLocalization.Add<Manifest::Localization::PackageName>(displayNameValue); + if (displayNameValue.empty()) { AICLI_LOG(Repo, Verbose, << "Skipping " << productCode << " because DisplayName is empty"); continue; @@ -244,7 +245,7 @@ namespace AppInstaller::Repository::Microsoft auto publisher = arpKey[Publisher]; if (publisher && publisher->GetType() == Registry::Value::Type::String) { - manifest.Publisher = publisher->GetValue<Registry::Value::Type::String>(); + manifest.DefaultLocalization.Add<Manifest::Localization::Publisher>(publisher->GetValue<Registry::Value::Type::String>()); } // TODO: If we want to keep the constructed manifest around to allow for `show` type commands @@ -274,7 +275,7 @@ namespace AppInstaller::Repository::Microsoft if (!manifestIdOpt) { - Logging::Telemetry().LogDuplicateARPEntry(addHr, scope, architecture, productCode, manifest.Name); + Logging::Telemetry().LogDuplicateARPEntry(addHr, scope, architecture, productCode, manifest.DefaultLocalization.Get<Manifest::Localization::PackageName>()); continue; } @@ -297,14 +298,14 @@ namespace AppInstaller::Repository::Microsoft // Pick up WindowsInstaller to determine if this is an MSI install. // TODO: Could also determine Inno (and maybe other types) through detecting other keys here. - auto installedType = Manifest::ManifestInstaller::InstallerTypeEnum::Exe; + auto installedType = Manifest::InstallerTypeEnum::Exe; if (GetBoolValue(arpKey, WindowsInstaller)) { - installedType = Manifest::ManifestInstaller::InstallerTypeEnum::Msi; + installedType = Manifest::InstallerTypeEnum::Msi; } - index.SetMetadataByManifestId(manifestId, PackageVersionMetadata::InstalledType, Manifest::ManifestInstaller::InstallerTypeToString(installedType)); + index.SetMetadataByManifestId(manifestId, PackageVersionMetadata::InstalledType, Manifest::InstallerTypeToString(installedType)); } } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.h b/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.h @@ -50,7 +50,7 @@ namespace AppInstaller::Repository::Microsoft // Gets the registry key associated with the given scope and architecture on this platform. // May return an empty key if there is no valid location (bad combination or not found). - Registry::Key GetARPKey(Manifest::ManifestInstaller::ScopeEnum scope, Utility::Architecture architecture) const; + Registry::Key GetARPKey(Manifest::ScopeEnum scope, Utility::Architecture architecture) const; // Returns true IFF the value exists and contains a non-zero DWORD. static bool GetBoolValue(const Registry::Key& arpKey, const std::wstring& name); @@ -67,7 +67,7 @@ namespace AppInstaller::Repository::Microsoft // Populates the index with the ARP entries from the given scope (machine/user). // Handles all of the architectures for the given scope. - void PopulateIndexFromARP(SQLiteIndex& index, Manifest::ManifestInstaller::ScopeEnum scope) const; + void PopulateIndexFromARP(SQLiteIndex& index, Manifest::ScopeEnum scope) const; // Populates the index with the ARP entries from the given key. // This entry point is primarily to allow unit tests to operate of arbitrary keys; diff --git a/src/AppInstallerRepositoryCore/Microsoft/PredefinedInstalledSourceFactory.cpp b/src/AppInstallerRepositoryCore/Microsoft/PredefinedInstalledSourceFactory.cpp @@ -33,7 +33,7 @@ namespace AppInstaller::Repository::Microsoft // Add one installer for storing the package family name. manifest.Installers.emplace_back(); // Every package will have the same tags currently. - manifest.Tags = { "msix" }; + manifest.DefaultLocalization.Add<Manifest::Localization::Tags>({ "msix" }); // Fields in the index but not populated: // AppMoniker - Not sure what we would put. @@ -55,11 +55,17 @@ namespace AppInstaller::Repository::Microsoft manifest.Id = familyName; + bool isPackageNameSet = false; // Attempt to get the DisplayName. Since this will retrieve the localized value, it has a chance to fail. // Rather than completely skip this package in that case, we will simply fall back to using the package name below. try { - manifest.Name = Utility::ConvertToUTF8(package.DisplayName()); + auto displayName = Utility::ConvertToUTF8(package.DisplayName()); + if (!displayName.empty()) + { + manifest.DefaultLocalization.Add<Manifest::Localization::PackageName>(displayName); + isPackageNameSet = true; + } } catch (const winrt::hresult_error& hre) { @@ -71,9 +77,9 @@ namespace AppInstaller::Repository::Microsoft AICLI_LOG(Repo, Info, << "Unknown exception thrown when getting DisplayName for " << familyName); } - if (manifest.Name.empty()) + if (!isPackageNameSet) { - manifest.Name = Utility::ConvertToUTF8(packageId.Name()); + manifest.DefaultLocalization.Add<Manifest::Localization::PackageName>(Utility::ConvertToUTF8(packageId.Name())); } std::ostringstream strstr; @@ -88,7 +94,7 @@ namespace AppInstaller::Repository::Microsoft auto manifestId = index.AddManifest(manifest, std::filesystem::path{ packageId.FamilyName().c_str() }); index.SetMetadataByManifestId(manifestId, PackageVersionMetadata::InstalledType, - Manifest::ManifestInstaller::InstallerTypeToString(Manifest::ManifestInstaller::InstallerTypeEnum::Msix)); + Manifest::InstallerTypeToString(Manifest::InstallerTypeEnum::Msix)); } } @@ -119,7 +125,7 @@ namespace AppInstaller::Repository::Microsoft arpHelper = ARPHelper(); } - arpHelper->PopulateIndexFromARP(index, Manifest::ManifestInstaller::ScopeEnum::Machine); + arpHelper->PopulateIndexFromARP(index, Manifest::ScopeEnum::Machine); } if (filter == PredefinedInstalledSourceFactory::Filter::None || filter == PredefinedInstalledSourceFactory::Filter::ARP_User) @@ -129,7 +135,7 @@ namespace AppInstaller::Repository::Microsoft arpHelper = ARPHelper(); } - arpHelper->PopulateIndexFromARP(index, Manifest::ManifestInstaller::ScopeEnum::User); + arpHelper->PopulateIndexFromARP(index, Manifest::ScopeEnum::User); } if (filter == PredefinedInstalledSourceFactory::Filter::None || filter == PredefinedInstalledSourceFactory::Filter::MSIX) diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface_1_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface_1_0.cpp @@ -185,8 +185,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 // Ensure that all of the 1:1 data exists. SQLite::rowid_t idId = IdTable::EnsureExists(connection, manifest.Id, true); - SQLite::rowid_t nameId = NameTable::EnsureExists(connection, manifest.Name); - SQLite::rowid_t monikerId = MonikerTable::EnsureExists(connection, manifest.AppMoniker); + SQLite::rowid_t nameId = NameTable::EnsureExists(connection, manifest.DefaultLocalization.Get<Manifest::Localization::PackageName>()); + SQLite::rowid_t monikerId = MonikerTable::EnsureExists(connection, manifest.Moniker); SQLite::rowid_t versionId = VersionTable::EnsureExists(connection, manifest.Version); SQLite::rowid_t channelId = ChannelTable::EnsureExists(connection, manifest.Channel); @@ -201,8 +201,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 }); // Add all of the 1:N data. - TagsTable::EnsureExistsAndInsert(connection, manifest.Tags, manifestId); - CommandsTable::EnsureExistsAndInsert(connection, manifest.Commands, manifestId); + TagsTable::EnsureExistsAndInsert(connection, manifest.GetAggregatedTags(), manifestId); + CommandsTable::EnsureExistsAndInsert(connection, manifest.GetAggregatedCommands(), manifestId); savepoint.Commit(); @@ -243,15 +243,16 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 indexModified = true; } - if (nameInIndex != manifest.Name) + auto packageName = manifest.DefaultLocalization.Get<Manifest::Localization::PackageName>(); + if (nameInIndex != packageName) { - UpdateManifestValueById<NameTable>(connection, manifest.Name, manifestId); + UpdateManifestValueById<NameTable>(connection, packageName, manifestId); indexModified = true; } - if (monikerInIndex != manifest.AppMoniker) + if (monikerInIndex != manifest.Moniker) { - UpdateManifestValueById<MonikerTable>(connection, manifest.AppMoniker, manifestId); + UpdateManifestValueById<MonikerTable>(connection, manifest.Moniker, manifestId); indexModified = true; } @@ -273,8 +274,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 } // Update all 1:N tables as necessary - indexModified = TagsTable::UpdateIfNeededByManifestId(connection, manifest.Tags, manifestId) || indexModified; - indexModified = CommandsTable::UpdateIfNeededByManifestId(connection, manifest.Commands, manifestId) || indexModified; + indexModified = TagsTable::UpdateIfNeededByManifestId(connection, manifest.GetAggregatedTags(), manifestId) || indexModified; + indexModified = CommandsTable::UpdateIfNeededByManifestId(connection, manifest.GetAggregatedCommands(), manifestId) || indexModified; savepoint.Commit(); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/NameTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/NameTable.h @@ -18,5 +18,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 } // The table for Name. + // TODO: Currently only indexing name from default locale, might need to be OneToMany table using NameTable = OneToOneTable<details::NameTableInfo>; } diff --git a/src/ManifestSchema/ManifestSchema.h b/src/ManifestSchema/ManifestSchema.h @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once + +#define MANIFESTSCHEMA_RESOURCE_TYPE 200 + +#define IDX_MANIFEST_SCHEMA_PREVIEW 201 +#define IDX_MANIFEST_SCHEMA_V1_SINGLETON 202 +#define IDX_MANIFEST_SCHEMA_V1_VERSION 203 +#define IDX_MANIFEST_SCHEMA_V1_INSTALLER 204 +#define IDX_MANIFEST_SCHEMA_V1_DEFAULTLOCALE 205 +#define IDX_MANIFEST_SCHEMA_V1_LOCALE 206 diff --git a/src/ManifestSchema/ManifestSchema.rc b/src/ManifestSchema/ManifestSchema.rc @@ -0,0 +1,71 @@ +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" +#include "ManifestSchema.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE 9, 1 + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + +///////////////////////////////////////////////////////////////////////////// +// +// Manifest schema +// +IDX_MANIFEST_SCHEMA_PREVIEW MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\preview\\manifest.0.1.0.json" +IDX_MANIFEST_SCHEMA_V1_SINGLETON MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\v1.0.0\\manifest.singleton.1.0.0.json" +IDX_MANIFEST_SCHEMA_V1_VERSION MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\v1.0.0\\manifest.version.1.0.0.json" +IDX_MANIFEST_SCHEMA_V1_INSTALLER MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\v1.0.0\\manifest.installer.1.0.0.json" +IDX_MANIFEST_SCHEMA_V1_DEFAULTLOCALE MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\v1.0.0\\manifest.defaultLocale.1.0.0.json" +IDX_MANIFEST_SCHEMA_V1_LOCALE MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\v1.0.0\\manifest.locale.1.0.0.json" diff --git a/src/ManifestSchema/ManifestSchema.vcxitems b/src/ManifestSchema/ManifestSchema.vcxitems @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup Label="Globals"> + <MSBuildAllProjects Condition="'$(MSBuildVersion)' == '' Or '$(MSBuildVersion)' < '16.0'">$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects> + <HasSharedItems>true</HasSharedItems> + <ItemsProjectGuid>{7d05f64d-ce5a-42aa-a2c1-e91458f061cf}</ItemsProjectGuid> + </PropertyGroup> + <ItemDefinitionGroup> + <ClCompile> + <AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(MSBuildThisFileDirectory)</AdditionalIncludeDirectories> + </ClCompile> + </ItemDefinitionGroup> + <ItemGroup> + <ProjectCapability Include="SourceItemsFromImports" /> + </ItemGroup> + <ItemGroup> + <ClInclude Include="$(MSBuildThisFileDirectory)ManifestSchema.h" /> + <ClInclude Include="$(MSBuildThisFileDirectory)resource.h" /> + </ItemGroup> + <ItemGroup> + <ResourceCompile Include="$(MSBuildThisFileDirectory)ManifestSchema.rc" /> + </ItemGroup> + <ItemGroup> + <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\preview\manifest.0.1.0.json" /> + <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.0.0\manifest.defaultLocale.1.0.0.json" /> + <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.0.0\manifest.installer.1.0.0.json" /> + <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.0.0\manifest.locale.1.0.0.json" /> + <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.0.0\manifest.singleton.1.0.0.json" /> + <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.0.0\manifest.version.1.0.0.json" /> + </ItemGroup> +</Project>+ \ No newline at end of file diff --git a/src/ManifestSchema/ManifestSchema.vcxitems.filters b/src/ManifestSchema/ManifestSchema.vcxitems.filters @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <ItemGroup> + <Filter Include="schema"> + <UniqueIdentifier>{d6998b42-8ce1-440a-ad12-8b505a284d7b}</UniqueIdentifier> + </Filter> + </ItemGroup> + <ItemGroup> + <ClInclude Include="$(MSBuildThisFileDirectory)ManifestSchema.h" /> + <ClInclude Include="$(MSBuildThisFileDirectory)resource.h" /> + </ItemGroup> + <ItemGroup> + <ResourceCompile Include="$(MSBuildThisFileDirectory)ManifestSchema.rc" /> + </ItemGroup> + <ItemGroup> + <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\preview\manifest.0.1.0.json"> + <Filter>schema</Filter> + </None> + <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.0.0\manifest.defaultLocale.1.0.0.json"> + <Filter>schema</Filter> + </None> + <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.0.0\manifest.installer.1.0.0.json"> + <Filter>schema</Filter> + </None> + <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.0.0\manifest.locale.1.0.0.json"> + <Filter>schema</Filter> + </None> + <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.0.0\manifest.singleton.1.0.0.json"> + <Filter>schema</Filter> + </None> + <None Include="$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests\v1.0.0\manifest.version.1.0.0.json"> + <Filter>schema</Filter> + </None> + </ItemGroup> +</Project>+ \ No newline at end of file diff --git a/src/ManifestSchema/resource.h b/src/ManifestSchema/resource.h @@ -0,0 +1,14 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by ManifestSchema.rc + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 101 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/src/Valijson/Valijson.vcxitems b/src/Valijson/Valijson.vcxitems @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup Label="Globals"> + <MSBuildAllProjects Condition="'$(MSBuildVersion)' == '' Or '$(MSBuildVersion)' < '16.0'">$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects> + <HasSharedItems>true</HasSharedItems> + <ItemsProjectGuid>{358bc478-0624-4ad1-a933-0422b5292af8}</ItemsProjectGuid> + </PropertyGroup> + <ItemDefinitionGroup> + <ClCompile> + <AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(MSBuildThisFileDirectory)valijson\include</AdditionalIncludeDirectories> + </ClCompile> + </ItemDefinitionGroup> + <ItemGroup> + <ProjectCapability Include="SourceItemsFromImports" /> + </ItemGroup> + <ItemGroup> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\adapters\adapter.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\adapters\basic_adapter.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\adapters\frozen_value.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\adapters\json11_adapter.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\adapters\jsoncpp_adapter.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\adapters\nlohmann_json_adapter.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\adapters\picojson_adapter.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\adapters\poco_json_adapter.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\adapters\property_tree_adapter.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\adapters\qtjson_adapter.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\adapters\rapidjson_adapter.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\adapters\std_string_adapter.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\constraints\basic_constraint.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\constraints\concrete_constraints.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\constraints\constraint.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\constraints\constraint_visitor.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\constraint_builder.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\internal\custom_allocator.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\internal\debug.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\internal\json_pointer.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\internal\json_reference.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\internal\optional.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\internal\uri.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\schema.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\schema_parser.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\subschema.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\utils\file_utils.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\utils\json11_utils.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\utils\jsoncpp_utils.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\utils\nlohmann_json_utils.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\utils\picojson_utils.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\utils\poco_json_utils.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\utils\property_tree_utils.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\utils\qtjson_utils.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\utils\rapidjson_utils.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\utils\utf8_utils.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\validation_results.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\validation_visitor.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\validator.hpp" /> + </ItemGroup> +</Project>+ \ No newline at end of file diff --git a/src/Valijson/Valijson.vcxitems.filters b/src/Valijson/Valijson.vcxitems.filters @@ -0,0 +1,122 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <ItemGroup> + <Filter Include="adapters"> + <UniqueIdentifier>{7a0239af-b4b5-48ba-a16c-4759e2fe7b43}</UniqueIdentifier> + </Filter> + <Filter Include="constraints"> + <UniqueIdentifier>{f503d3b0-5fd9-4ef4-9db9-5e48fe68db82}</UniqueIdentifier> + </Filter> + <Filter Include="internal"> + <UniqueIdentifier>{bce3512d-8612-4a2e-a2ad-7156b0d2f607}</UniqueIdentifier> + </Filter> + <Filter Include="utils"> + <UniqueIdentifier>{e6b3c438-739c-4dbc-bfd0-f0dd8ff97467}</UniqueIdentifier> + </Filter> + </ItemGroup> + <ItemGroup> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\adapters\adapter.hpp"> + <Filter>adapters</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\adapters\basic_adapter.hpp"> + <Filter>adapters</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\adapters\frozen_value.hpp"> + <Filter>adapters</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\adapters\json11_adapter.hpp"> + <Filter>adapters</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\adapters\jsoncpp_adapter.hpp"> + <Filter>adapters</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\adapters\nlohmann_json_adapter.hpp"> + <Filter>adapters</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\adapters\picojson_adapter.hpp"> + <Filter>adapters</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\adapters\poco_json_adapter.hpp"> + <Filter>adapters</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\adapters\property_tree_adapter.hpp"> + <Filter>adapters</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\adapters\qtjson_adapter.hpp"> + <Filter>adapters</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\adapters\rapidjson_adapter.hpp"> + <Filter>adapters</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\adapters\std_string_adapter.hpp"> + <Filter>adapters</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\constraints\basic_constraint.hpp"> + <Filter>constraints</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\constraints\concrete_constraints.hpp"> + <Filter>constraints</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\constraints\constraint.hpp"> + <Filter>constraints</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\constraints\constraint_visitor.hpp"> + <Filter>constraints</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\internal\custom_allocator.hpp"> + <Filter>internal</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\internal\debug.hpp"> + <Filter>internal</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\internal\json_pointer.hpp"> + <Filter>internal</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\internal\json_reference.hpp"> + <Filter>internal</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\internal\optional.hpp"> + <Filter>internal</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\internal\uri.hpp"> + <Filter>internal</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\utils\file_utils.hpp"> + <Filter>utils</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\utils\json11_utils.hpp"> + <Filter>utils</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\utils\jsoncpp_utils.hpp"> + <Filter>utils</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\utils\nlohmann_json_utils.hpp"> + <Filter>utils</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\utils\picojson_utils.hpp"> + <Filter>utils</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\utils\poco_json_utils.hpp"> + <Filter>utils</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\utils\property_tree_utils.hpp"> + <Filter>utils</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\utils\qtjson_utils.hpp"> + <Filter>utils</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\utils\rapidjson_utils.hpp"> + <Filter>utils</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\utils\utf8_utils.hpp"> + <Filter>utils</Filter> + </ClInclude> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\constraint_builder.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\schema.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\schema_parser.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\subschema.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\validation_results.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\validation_visitor.hpp" /> + <ClInclude Include="$(MSBuildThisFileDirectory)valijson\include\valijson\validator.hpp" /> + </ItemGroup> +</Project>+ \ No newline at end of file diff --git a/src/WinGetUtil/Exports.cpp b/src/WinGetUtil/Exports.cpp @@ -198,6 +198,34 @@ extern "C" } CATCH_RETURN() + WINGET_UTIL_API WinGetValidateManifestV2( + WINGET_STRING inputPath, + BOOL* succeeded, + WINGET_STRING_OUT* message, + WINGET_STRING mergedManifestPath, + WinGetValidateManifestOption option) try + { + THROW_HR_IF(E_INVALIDARG, !inputPath); + THROW_HR_IF(E_INVALIDARG, !succeeded); + + try + { + (void)YamlParser::CreateFromPath(inputPath, true, true, mergedManifestPath, option == WinGetValidateManifestOption::SchemaValidationOnly); + *succeeded = TRUE; + } + catch (const ManifestException& e) + { + *succeeded = e.IsWarningOnly(); + if (message) + { + *message = ::SysAllocString(ConvertToUTF16(e.GetManifestErrorMessage()).c_str()); + } + } + + return S_OK; + } + CATCH_RETURN() + WINGET_UTIL_API WinGetDownload( WINGET_STRING url, WINGET_STRING filePath, diff --git a/src/WinGetUtil/WinGetUtil.h b/src/WinGetUtil/WinGetUtil.h @@ -17,6 +17,12 @@ extern "C" #define WINGET_SQLITE_INDEX_VERSION_LATEST ((UINT32)-1) + enum WinGetValidateManifestOption + { + Default, + SchemaValidationOnly + }; + // Initializes the logging infrastructure. WINGET_UTIL_API WinGetLoggingInit( WINGET_STRING logPath); @@ -80,6 +86,17 @@ extern "C" BOOL* succeeded, WINGET_STRING_OUT* message); + // Validates a given manifest. Returns a bool for validation result and + // a string representing validation errors if validation failed. + // If mergedManifestPath is provided, this method will write a merged manifest + // to the location specified by mergedManifestPath + WINGET_UTIL_API WinGetValidateManifestV2( + WINGET_STRING inputPath, + BOOL* succeeded, + WINGET_STRING_OUT* message, + WINGET_STRING mergedManifestPath, + WinGetValidateManifestOption option); + // Downloads a file to the given path, returning the SHA 256 hash of the file. WINGET_UTIL_API WinGetDownload( WINGET_STRING url, diff --git a/src/WinGetUtil/WinGetUtil.vcxproj b/src/WinGetUtil/WinGetUtil.vcxproj @@ -70,6 +70,7 @@ </ImportGroup> <ImportGroup Label="Shared"> <Import Project="..\binver\binver.vcxitems" Label="Shared" /> + <Import Project="..\ManifestSchema\ManifestSchema.vcxitems" Label="Shared" /> </ImportGroup> <ImportGroup Label="PropertySheets"> <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> diff --git a/src/WinGetYamlFuzzing/WinGetYamlFuzzing.vcxproj b/src/WinGetYamlFuzzing/WinGetYamlFuzzing.vcxproj @@ -25,6 +25,7 @@ <ImportGroup Label="ExtensionSettings"> </ImportGroup> <ImportGroup Label="Shared"> + <Import Project="..\ManifestSchema\ManifestSchema.vcxitems" Label="Shared" /> </ImportGroup> <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Fuzzing|x64'"> <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />