winget-cli

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

commit c70d0f2bcb8498f7ec06c12a7e4a102611b8e107
parent 96955098040dd8f6a45f0607abaea58cd6c2d922
Author: JohnMcPMS <johnmcp@microsoft.com>
Date:   Fri, 18 Sep 2020 15:36:49 -0700

Add system reference values to manifest parsing (#574)

Adds the PackageFamilyName and ProductCode values to parsing by the manifest.  These can be at the root level, where they will be inherited.  They can also be provided per installer, but they must match the InstallerType or it will be an error.

Also makes major changes to the way the ManifestVersion is handled.  For now, the version is largely ignored (except it must be major version 0 if provided).  A concept of extensions is added, which are defined as the hyphen separated values after the primary version.  These can also be versioned, although the code does not exist to check it.  The syntax is:
```
ManifestVersion := Version (- Extension)
Extension := Version (- Extension)
```
where the Version portion of Extension is treated as `Name.Version`.  For instance, `1.0.0-extension` and `1.0.0-extension.2`.

Additionally:
- Fixes an issue with the version sorting where additional version characters would sort higher than those without (ex. 1.0 will now be greater than 1.0-alpha)
Diffstat:
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 12++++++++++++
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 9+++++++++
Msrc/AppInstallerCLITests/TestData/Manifest-Bad-InvalidManifestVersionValue.yaml | 2+-
Asrc/AppInstallerCLITests/TestData/Manifest-Bad-PackageFamilyNameOnMSI.yaml | 12++++++++++++
Asrc/AppInstallerCLITests/TestData/Manifest-Bad-ProductCodeOnMSIX.yaml | 12++++++++++++
Asrc/AppInstallerCLITests/TestData/Manifest-Good-SystemReferenceComplex.yaml | 32++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/TestData/Manifest-Good.yaml | 2++
Msrc/AppInstallerCLITests/YamlManifest.cpp | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/Manifest/Manifest.cpp | 58++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/AppInstallerCommonCore/Manifest/ManifestInstaller.cpp | 17+++++++++++++++++
Msrc/AppInstallerCommonCore/Manifest/ManifestValidation.cpp | 11+++++++++++
Msrc/AppInstallerCommonCore/Manifest/YamlParser.cpp | 142+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/AppInstallerCommonCore/Public/AppInstallerVersions.h | 4++++
Msrc/AppInstallerCommonCore/Public/winget/Manifest.h | 15+++++++++++++--
Msrc/AppInstallerCommonCore/Public/winget/ManifestInstaller.h | 6++++++
Msrc/AppInstallerCommonCore/Public/winget/ManifestValidation.h | 2++
Msrc/AppInstallerCommonCore/Public/winget/ManifestYamlParser.h | 9+++------
Msrc/AppInstallerCommonCore/Versions.cpp | 19+++++++++++++++++--
18 files changed, 334 insertions(+), 86 deletions(-)

diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -409,6 +409,18 @@ <CopyFileToFolders Include="TestData\Manifest-Encoding-UTF8.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Bad-PackageFamilyNameOnMSI.yaml"> + <DeploymentContent>true</DeploymentContent> + <FileType>Document</FileType> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Bad-ProductCodeOnMSIX.yaml"> + <DeploymentContent>true</DeploymentContent> + <FileType>Document</FileType> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Good-SystemReferenceComplex.yaml"> + <DeploymentContent>true</DeploymentContent> + <FileType>Document</FileType> + </CopyFileToFolders> </ItemGroup> <ItemGroup> <ProjectReference Include="..\AppInstallerCLICore\AppInstallerCLICore.vcxproj"> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -300,5 +300,14 @@ <CopyFileToFolders Include="TestData\Manifest-Encoding-UTF16LE.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Good-SystemReferenceComplex.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Bad-PackageFamilyNameOnMSI.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Bad-ProductCodeOnMSIX.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> </ItemGroup> </Project> \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/Manifest-Bad-InvalidManifestVersionValue.yaml b/src/AppInstallerCLITests/TestData/Manifest-Bad-InvalidManifestVersionValue.yaml @@ -9,4 +9,4 @@ Installers: - Arch: x86 Url: https://rubengustorage.blob.core.windows.net/publiccontainer/msixsdk-x86.zip Sha256: 98B67758CEAFFCBB3FE47838FD0A8D7BD581C2650842D6B2B0E0D49A23270CCD -ManifestVersion: 1.1.0-invalid +ManifestVersion: 0.1.0-invalid diff --git a/src/AppInstallerCLITests/TestData/Manifest-Bad-PackageFamilyNameOnMSI.yaml b/src/AppInstallerCLITests/TestData/Manifest-Bad-PackageFamilyNameOnMSI.yaml @@ -0,0 +1,12 @@ +# Bad manifest. Invalid Arch value +Id: microsoft.msixsdk +Name: MSIX SDK +Version: 1.7.32 +Publisher: Microsoft +License: Test +Installers: + - Arch: x86 + InstallerType: MSI + PackageFamilyName: Foo_123456789abcd + Url: https://rubengustorage.blob.core.windows.net/publiccontainer/msixsdk-x86.zip + Sha256: 98B67758CEAFFCBB3FE47838FD0A8D7BD581C2650842D6B2B0E0D49A23270CCD diff --git a/src/AppInstallerCLITests/TestData/Manifest-Bad-ProductCodeOnMSIX.yaml b/src/AppInstallerCLITests/TestData/Manifest-Bad-ProductCodeOnMSIX.yaml @@ -0,0 +1,12 @@ +# Bad manifest. Invalid Arch value +Id: microsoft.msixsdk +Name: MSIX SDK +Version: 1.7.32 +Publisher: Microsoft +License: Test +Installers: + - Arch: x86 + InstallerType: MSIX + ProductCode: "{FOO}" + Url: https://rubengustorage.blob.core.windows.net/publiccontainer/msixsdk-x86.zip + Sha256: 98B67758CEAFFCBB3FE47838FD0A8D7BD581C2650842D6B2B0E0D49A23270CCD diff --git a/src/AppInstallerCLITests/TestData/Manifest-Good-SystemReferenceComplex.yaml b/src/AppInstallerCLITests/TestData/Manifest-Good-SystemReferenceComplex.yaml @@ -0,0 +1,32 @@ +Id: microsoft.sysrefcomp +Name: Complex System Reference +Version: 1.7.32 +Publisher: Microsoft +License: MIT License +Description: A complex system reference test file. +InstallerType: Zip +PackageFamilyName: Microsoft.DesktopAppInstaller_8wekyb3d8bbwe +ProductCode: "{Foo}" +Installers: + - InstallerType: Zip + Arch: x86 + Url: https://rubengustorage.blob.core.windows.net/publiccontainer/msixsdkx86.zip + Sha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + - InstallerType: MSIX + Arch: x86 + Url: https://rubengustorage.blob.core.windows.net/publiccontainer/msixsdkx86.zip + Sha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + - InstallerType: MSI + Arch: x86 + Url: https://rubengustorage.blob.core.windows.net/publiccontainer/msixsdkx86.zip + Sha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + - InstallerType: MSIX + Arch: x64 + Url: https://rubengustorage.blob.core.windows.net/publiccontainer/msixsdkx86.zip + Sha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + PackageFamilyName: Override_8wekyb3d8bbwe + - InstallerType: MSI + Arch: x64 + Url: https://rubengustorage.blob.core.windows.net/publiccontainer/msixsdkx86.zip + Sha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + ProductCode: "Override" diff --git a/src/AppInstallerCLITests/TestData/Manifest-Good.yaml b/src/AppInstallerCLITests/TestData/Manifest-Good.yaml @@ -17,6 +17,8 @@ FileExtensions: "appx,appxbundle,msix,msixbundle" # InstallerType and Switches CAN have a "default" value # on the root. An installer can override them. InstallerType: Zip +PackageFamilyName: Microsoft.DesktopAppInstaller_8wekyb3d8bbwe +ProductCode: "{Foo}" Switches: Custom: /custom SilentWithProgress: /silentwithprogress diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp @@ -49,6 +49,8 @@ TEST_CASE("ReadGoodManifestAndVerifyContents", "[ManifestValidation]") 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}"); // default switches auto switches = manifest.Switches; @@ -69,6 +71,8 @@ TEST_CASE("ReadGoodManifestAndVerifyContents", "[ManifestValidation]") REQUIRE(installer1.Language == "en-US"); REQUIRE(installer1.InstallerType == ManifestInstaller::InstallerTypeEnum::Zip); REQUIRE(installer1.Scope == "user"); + REQUIRE(installer1.PackageFamilyName == ""); + REQUIRE(installer1.ProductCode == ""); auto installer1Switches = installer1.Switches; REQUIRE(installer1Switches.at(ManifestInstaller::InstallerSwitchType::Custom) == "/c"); @@ -86,6 +90,8 @@ TEST_CASE("ReadGoodManifestAndVerifyContents", "[ManifestValidation]") REQUIRE(installer2.Language == "en-US"); REQUIRE(installer2.InstallerType == ManifestInstaller::InstallerTypeEnum::Zip); REQUIRE(installer2.Scope == "user"); + REQUIRE(installer2.PackageFamilyName == ""); + REQUIRE(installer2.ProductCode == ""); // Installer2 does not declare switches, it inherits switches from package default. auto installer2Switches = installer2.Switches; @@ -227,6 +233,8 @@ TEST_CASE("ReadBadManifests", "[ManifestValidation]") { "Manifest-Bad-VersionMissing.yaml", "Required field missing. Field: Version" }, { "Manifest-Bad-InvalidManifestVersionValue.yaml", "Invalid field value. Field: ManifestVersion" }, { "InstallFlowTest_MSStore.yaml", "Field value is not supported. Field: InstallerType Value: MSStore" }, + { "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" }, }; for (auto const& testCase : TestCases) @@ -255,3 +263,51 @@ TEST_CASE("ManifestEncoding", "[ManifestValidation]") REQUIRE(manifest.Name == u8"MSIX SDK\xA9"); } } + +TEST_CASE("ComplexSystemReference", "[ManifestValidation]") +{ + Manifest manifest = YamlParser::CreateFromPath(TestDataFile("Manifest-Good-SystemReferenceComplex.yaml")); + + REQUIRE(manifest.Installers.size() == 5); + + // Zip installer does not inherit + REQUIRE(manifest.Installers[0].InstallerType == ManifestInstaller::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].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].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].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].Arch == Architecture::X64); + REQUIRE(manifest.Installers[4].PackageFamilyName == ""); + REQUIRE(manifest.Installers[4].ProductCode == "Override"); +} + +TEST_CASE("ManifestVersionExtensions", "[ManifestValidation]") +{ + REQUIRE(!ManifestVer("1.0.0"sv).HasExtension("msstore")); + REQUIRE(!ManifestVer("1.0.0-other"sv).HasExtension("msstore")); + REQUIRE(!ManifestVer("1.0.0-other-other2"sv).HasExtension("msstore")); + + REQUIRE(ManifestVer("1.0.0-msstore"sv).HasExtension("msstore")); + REQUIRE(ManifestVer("1.0.0-msstore.2"sv).HasExtension("msstore")); + REQUIRE(ManifestVer("1.0.0-other-msstore.2"sv).HasExtension("msstore")); + REQUIRE(ManifestVer("1.0.0-msstore.2-other"sv).HasExtension("msstore")); +} diff --git a/src/AppInstallerCommonCore/Manifest/Manifest.cpp b/src/AppInstallerCommonCore/Manifest/Manifest.cpp @@ -6,10 +6,34 @@ namespace AppInstaller::Manifest { - ManifestVer::ManifestVer(std::string version, bool fullValidation) : Version(std::move(version), ".") + 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; @@ -18,8 +42,16 @@ namespace AppInstaller::Manifest { for (size_t i = 0; i < m_parts.size(); i++) { - if (!m_parts[i].Other.empty() && - (i < 2 || fullValidation)) + 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; @@ -30,13 +62,27 @@ namespace AppInstaller::Manifest if (!validationSuccess) { std::vector<ValidationError> errors; - errors.emplace_back(ManifestError::InvalidFieldValue, "ManifestVersion", m_version); + errors.emplace_back(ManifestError::InvalidFieldValue, "ManifestVersion", std::string{ version }); THROW_EXCEPTION(ManifestException(std::move(errors))); } } - bool ManifestVer::HasTag() const + bool ManifestVer::HasExtension() const { - return m_parts.size() == 3 && !m_parts[2].Other.empty(); + 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; } } diff --git a/src/AppInstallerCommonCore/Manifest/ManifestInstaller.cpp b/src/AppInstallerCommonCore/Manifest/ManifestInstaller.cpp @@ -89,4 +89,21 @@ namespace AppInstaller::Manifest return result; } + + 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 + ); + } } diff --git a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp @@ -80,6 +80,17 @@ namespace AppInstaller::Manifest resultErrors.emplace_back(ManifestError::InvalidFieldValue, "InstallerType"); } + // Validate system reference strings if they are set at the installer level + if (!installer.PackageFamilyName.empty() && !ManifestInstaller::DoesInstallerTypeUsePackageFamilyName(installer.InstallerType)) + { + resultErrors.emplace_back(ManifestError::InstallerTypeDoesNotSupportPackageFamilyName, "InstallerType", ManifestInstaller::InstallerTypeToString(installer.InstallerType)); + } + + if (!installer.ProductCode.empty() && !ManifestInstaller::DoesInstallerTypeUseProductCode(installer.InstallerType)) + { + resultErrors.emplace_back(ManifestError::InstallerTypeDoesNotSupportProductCode, "InstallerType", ManifestInstaller::InstallerTypeToString(installer.InstallerType)); + } + if (installer.InstallerType == ManifestInstaller::InstallerTypeEnum::MSStore) { // MSStore type is not supported in community repo diff --git a/src/AppInstallerCommonCore/Manifest/YamlParser.cpp b/src/AppInstallerCommonCore/Manifest/YamlParser.cpp @@ -9,6 +9,15 @@ namespace AppInstaller::Manifest { namespace { + // The maximum supported major version known about by this code. + constexpr uint64_t s_MaxSupportedMajorVersion = 0; + + // The default manifest version assigned to manifests without a ManifestVersion field. + constexpr std::string_view s_DefaultManifestVersion = "0.1.0"sv; + + // The manifest extension for the MS Store + constexpr std::string_view s_MSStoreExtension = "msstore"sv; + std::vector<Manifest::string_t> SplitMultiValueField(const std::string& input) { if (input.empty()) @@ -41,91 +50,76 @@ namespace AppInstaller::Manifest void YamlParser::PrepareManifestFieldInfos(const ManifestVer& manifestVer) { + // Initially supported fields RootFieldInfos = { - { "ManifestVersion", PreviewManifestVersion, [](const YAML::Node&) { /* ManifestVersion already processed */ }, false, + { "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", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->Id = value.as<std::string>(); Utility::Trim(m_p_manifest->Id); }, true, "^[\\S]+\\.[\\S]+$" }, - { "Name", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->Name = value.as<std::string>(); Utility::Trim(m_p_manifest->Name); }, true }, - { "Version", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->Version = value.as<std::string>(); Utility::Trim(m_p_manifest->Version); }, true, + { "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", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->Publisher = value.as<std::string>(); }, true }, - { "AppMoniker", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->AppMoniker = value.as<std::string>(); Utility::Trim(m_p_manifest->AppMoniker); } }, - { "Channel", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->Channel = value.as<std::string>(); Utility::Trim(m_p_manifest->Channel); } }, - { "Author", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->Author = value.as<std::string>(); } }, - { "License", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->License = value.as<std::string>(); } }, - { "MinOSVersion", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->MinOSVersion = value.as<std::string>(); Utility::Trim(m_p_manifest->MinOSVersion); }, false, + { "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", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->Tags = SplitMultiValueField(value.as<std::string>()); } }, - { "Commands", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->Commands = SplitMultiValueField(value.as<std::string>()); } }, - { "Protocols", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->Protocols = SplitMultiValueField(value.as<std::string>()); } }, - { "FileExtensions", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->FileExtensions = SplitMultiValueField(value.as<std::string>()); } }, - { "InstallerType", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->InstallerType = ManifestInstaller::ConvertToInstallerTypeEnum(value.as<std::string>()); } }, - { "Description", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->Description = value.as<std::string>(); } }, - { "Homepage", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->Homepage = value.as<std::string>(); } }, - { "LicenseUrl", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_manifest->LicenseUrl = value.as<std::string>(); } }, - { "Switches", PreviewManifestVersion, [this](const YAML::Node& value) { *m_p_switchesNode = value; } }, - { "Installers", PreviewManifestVersion, [this](const YAML::Node& value) { *m_p_installersNode = value; }, true }, - { "Localization", PreviewManifestVersion, [this](const YAML::Node& value) { *m_p_localizationsNode = value; } }, + { "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>()); } }, + { "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", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_installer->Arch = Utility::ConvertToArchitectureEnum(value.as<std::string>()); }, true }, - { "Url", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_installer->Url = value.as<std::string>(); } }, - { "Sha256", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_installer->Sha256 = Utility::SHA256::ConvertToBytes(value.as<std::string>()); }, false, "^[A-Fa-f0-9]{64}$" }, - { "SignatureSha256", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_installer->SignatureSha256 = Utility::SHA256::ConvertToBytes(value.as<std::string>()); }, false, "^[A-Fa-f0-9]{64}$" }, - { "Language", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_installer->Language = value.as<std::string>(); } }, - { "Scope", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_installer->Scope = value.as<std::string>(); } }, - { "InstallerType", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_installer->InstallerType = ManifestInstaller::ConvertToInstallerTypeEnum(value.as<std::string>()); } }, - { "ProductId", PreviewManifestVersionMSStore, [this](const YAML::Node& value) { m_p_installer->ProductId = value.as<std::string>(); } }, - { "Switches", PreviewManifestVersion, [this](const YAML::Node& value) { *m_p_switchesNode = value; } }, + { "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 = value.as<std::string>(); } }, + { "InstallerType", [this](const YAML::Node& value) { m_p_installer->InstallerType = ManifestInstaller::ConvertToInstallerTypeEnum(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", PreviewManifestVersion, [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::Custom] = value.as<std::string>(); } }, - { "Silent", PreviewManifestVersion, [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::Silent] = value.as<std::string>(); } }, - { "SilentWithProgress", PreviewManifestVersion, [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::SilentWithProgress] = value.as<std::string>(); } }, - { "Interactive", PreviewManifestVersion, [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::Interactive] = value.as<std::string>(); } }, - { "Language", PreviewManifestVersion, [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::Language] = value.as<std::string>(); } }, - { "Log", PreviewManifestVersion, [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::Log] = value.as<std::string>(); } }, - { "InstallLocation", PreviewManifestVersion, [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::InstallLocation] = value.as<std::string>(); } }, + { "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>(); } }, }; LocalizationFieldInfos = { - { "Language", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_localization->Language = value.as<std::string>(); }, true }, - { "Description", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_localization->Description = value.as<std::string>(); } }, - { "Homepage", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_localization->Homepage = value.as<std::string>(); } }, - { "LicenseUrl", PreviewManifestVersion, [this](const YAML::Node& value) { m_p_localization->LicenseUrl = value.as<std::string>(); } }, + { "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>(); } }, }; - FilterManifestFieldInfos(RootFieldInfos, manifestVer); - FilterManifestFieldInfos(InstallerFieldInfos, manifestVer); - FilterManifestFieldInfos(SwitchesFieldInfos, manifestVer); - FilterManifestFieldInfos(LocalizationFieldInfos, manifestVer); - } - - void YamlParser::FilterManifestFieldInfos( - std::vector<ManifestFieldInfo>& source, - const ManifestVer& manifestVer) - { - auto it = std::remove_if(source.begin(), source.end(), - [&](ManifestFieldInfo field) - { - if (field.VerIntroduced.HasTag()) - { - // Tagged version should have exact match - return field.VerIntroduced != manifestVer; - } - else - { - return manifestVer < field.VerIntroduced; - } - }); - source.erase(it, source.end()); + // 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) @@ -207,19 +201,18 @@ namespace AppInstaller::Manifest // Detect manifest version first to determine expected fields // Use index to access ManifestVersion directly. If there're duplicates or other general errors, it'll be detected in later // processing of iterating the whole manifest. - // Todo: make ManifestVersion required when all manifests in our repo have been updated to contain a ManifestVersion if (rootNode["ManifestVersion"sv]) { auto manifestVersionValue = rootNode["ManifestVersion"sv].as<std::string>(); - manifest.ManifestVersion = ManifestVer(manifestVersionValue, false); + manifest.ManifestVersion = ManifestVer(manifestVersionValue); } else { - manifest.ManifestVersion = PreviewManifestVersion; + manifest.ManifestVersion = ManifestVer(s_DefaultManifestVersion); } // Check manifest version is supported - if (manifest.ManifestVersion.Major() > MaxSupportedMajorVersion) + if (manifest.ManifestVersion.Major() > s_MaxSupportedMajorVersion) { THROW_EXCEPTION_MSG(ManifestException(APPINSTALLER_CLI_ERROR_UNSUPPORTED_MANIFESTVERSION), "Unsupported ManifestVersion: %S", manifest.ManifestVersion.ToString().c_str()); } @@ -260,6 +253,17 @@ namespace AppInstaller::Manifest 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)) + { + installer.PackageFamilyName = manifest.PackageFamilyName; + } + + if (installer.ProductCode.empty() && ManifestInstaller::DoesInstallerTypeUseProductCode(installer.InstallerType)) + { + installer.ProductCode = manifest.ProductCode; + } + // Populate default known switches installer.Switches = GetDefaultKnownSwitches(installer.InstallerType); diff --git a/src/AppInstallerCommonCore/Public/AppInstallerVersions.h b/src/AppInstallerCommonCore/Public/AppInstallerVersions.h @@ -20,6 +20,7 @@ namespace AppInstaller::Utility // if both sides have no more parts, return equal // else if one side has no more parts, it is less // else if integers not equal, return comparison of integers + // else if only one side has a non-empty string part, it is less // else if string parts not equal, return comparison of strings struct Version { @@ -32,6 +33,9 @@ namespace AppInstaller::Utility Version(std::string(version), splitChars) {} Version(std::string&& version, std::string_view splitChars = DefaultSplitChars); + // Resets the version's value to the input. + void Assign(std::string&& version, std::string_view splitChars = DefaultSplitChars); + // Gets the full version string used to construct the Version. const std::string& ToString() const { return m_version; } diff --git a/src/AppInstallerCommonCore/Public/winget/Manifest.h b/src/AppInstallerCommonCore/Public/winget/Manifest.h @@ -18,13 +18,18 @@ namespace AppInstaller::Manifest { ManifestVer() = default; - ManifestVer(std::string version, bool fullValidation); + 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 HasTag() const; + bool HasExtension() const; + + bool HasExtension(std::string_view extension) const; + + private: + std::vector<Version> m_extensions; }; // Representation of the parsed manifest file. @@ -68,6 +73,12 @@ namespace AppInstaller::Manifest ManifestInstaller::InstallerTypeEnum InstallerType = ManifestInstaller::InstallerTypeEnum::Unknown; + // Package family name for MSIX packaged installers. + string_t PackageFamilyName; + + // Product code for ARP (Add/Remove Programs) installers. + string_t ProductCode; + string_t Description; string_t Homepage; diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h b/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h @@ -81,5 +81,11 @@ namespace AppInstaller::Manifest static InstallerTypeEnum ConvertToInstallerTypeEnum(const std::string& in); static std::string InstallerTypeToString(InstallerTypeEnum installerType); + + // Gets a value indicating whether the given installer type uses the PackageFamilyName system reference. + static bool DoesInstallerTypeUsePackageFamilyName(InstallerTypeEnum installerType); + + // Gets a value indicating whether the given installer type uses the ProductCode system reference. + static bool DoesInstallerTypeUseProductCode(InstallerTypeEnum installerType); }; } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h b/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h @@ -29,6 +29,8 @@ namespace AppInstaller::Manifest const char* const FieldNotSupported = "Field is not supported."; const char* const FieldValueNotSupported = "Field value is not supported."; const char* const DuplicateInstallerEntry = "Duplicate installer entry found."; + const char* const InstallerTypeDoesNotSupportPackageFamilyName = "The specified installer type does not support PackageFamilyName."; + const char* const InstallerTypeDoesNotSupportProductCode = "The specified installer type does not support ProductCode."; } struct ValidationError diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestYamlParser.h b/src/AppInstallerCommonCore/Public/winget/ManifestYamlParser.h @@ -8,10 +8,6 @@ namespace AppInstaller::Manifest { - static const uint64_t MaxSupportedMajorVersion = 1; - static const ManifestVer PreviewManifestVersion = ManifestVer("0.1.0", false); - static const ManifestVer PreviewManifestVersionMSStore = ManifestVer("0.2.0-msstore", false); - struct YamlParser { // fullValidation: Bool to set if manifest creation should perform extra validation that client does not need. @@ -34,8 +30,10 @@ namespace AppInstaller::Manifest // 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; - ManifestVer VerIntroduced; std::function<void(const YAML::Node&)> ProcessFunc; bool Required = false; std::string RegEx = {}; @@ -61,6 +59,5 @@ namespace AppInstaller::Manifest bool fullValidation); void PrepareManifestFieldInfos(const ManifestVer& manifestVer); - void FilterManifestFieldInfos(std::vector<ManifestFieldInfo>& source, const ManifestVer& manifestVer); }; } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Versions.cpp b/src/AppInstallerCommonCore/Versions.cpp @@ -5,9 +5,14 @@ namespace AppInstaller::Utility { - Version::Version(std::string&& version, std::string_view splitChars) : - m_version(std::move(version)) + Version::Version(std::string&& version, std::string_view splitChars) { + Assign(std::move(version), splitChars); + } + + void Version::Assign(std::string&& version, std::string_view splitChars) + { + m_version = std::move(version); size_t pos = 0; while (pos < m_version.length()) @@ -129,6 +134,16 @@ namespace AppInstaller::Utility { return false; } + else if (Other.empty()) + { + // If this Other is empty, it is at least >= + return false; + } + else if (!Other.empty() && other.Other.empty()) + { + // If the other Other is empty and this is not, this is less. + return true; + } else if (Other < other.Other) { return true;