diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index 4051f4c994..cfa2a0bbe5 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -707,6 +707,9 @@ true + + true + true diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters index f29961ab30..187dafc12a 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -660,6 +660,9 @@ TestData + + TestData + TestData diff --git a/src/AppInstallerCLITests/TestData/Manifest-Good-InstallerUrl-BlockScalar.yaml b/src/AppInstallerCLITests/TestData/Manifest-Good-InstallerUrl-BlockScalar.yaml new file mode 100644 index 0000000000..18d2ed7620 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/Manifest-Good-InstallerUrl-BlockScalar.yaml @@ -0,0 +1,17 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.1.0.schema.json + +PackageIdentifier: microsoft.msixsdk +PackageName: MSIX SDK +PackageVersion: 1.07.32-beta +Publisher: Microsoft +ShortDescription: description +License: Test +PackageLocale: en-US +Installers: + - Architecture: x86 + InstallerUrl: > + https://rubengustorage.blob.core.windows.net/publiccontainer/msixsdk-x86.msi + InstallerSha256: 98B67758CEAFFCBB3FE47838FD0A8D7BD581C2650842D6B2B0E0D49A23270CCD + InstallerType: msi +ManifestType: singleton +ManifestVersion: 1.1.0 diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp index f893881f83..f78a5fdcb1 100644 --- a/src/AppInstallerCLITests/YamlManifest.cpp +++ b/src/AppInstallerCLITests/YamlManifest.cpp @@ -958,6 +958,15 @@ TEST_CASE("ReadGoodManifestWithSpaces", "[ManifestValidation]") REQUIRE(manifest.DefaultInstallerInfo.FileExtensions == MultiValue{ "appx", "appxbundle", "msix", "msixbundle" }); } +TEST_CASE("ReadGoodManifestInstallerUrlBlockScalar", "[ManifestValidation]") +{ + ManifestValidateOption validateOption = GetTestManifestValidateOption(); + Manifest manifest = YamlParser::CreateFromPath(TestDataFile("Manifest-Good-InstallerUrl-BlockScalar.yaml"), validateOption); + + REQUIRE(manifest.Installers.size() == 1); + REQUIRE(manifest.Installers[0].Url == "https://rubengustorage.blob.core.windows.net/publiccontainer/msixsdk-x86.msi"); +} + TEST_CASE("ReadGoodManifests", "[ManifestValidation]") { ManifestTestCase TestCases[] = @@ -973,6 +982,7 @@ TEST_CASE("ReadGoodManifests", "[ManifestValidation]") { "Manifest-Good-Minimum-InstallerType.yaml" }, { "Manifest-Good-Switches.yaml" }, { "Manifest-Good-DefaultExpectedReturnCodeInInstallerSuccessCodes.yaml" }, + { "Manifest-Good-InstallerUrl-BlockScalar.yaml" }, { "Manifest-Good-InstallerTypeZip-PortableExe.yaml" }, { "Manifest-Good-InstallerTypeZip-PortableExeUppercase.yaml" }, }; diff --git a/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp index b58c2e7a32..fff3806f44 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp @@ -47,6 +47,19 @@ namespace AppInstaller::Manifest::YamlParser return YamlScalarType::String; } + // YAML block scalars with clip/keep chomping retain a trailing line break. + // Schema patterns anchored with `$` fail against that break under current + // MSVC std::regex ECMAScript semantics (LWG-2503), so strip it before validation. + std::string TrimTrailingLineBreaks(std::string value) + { + while (!value.empty() && (value.back() == '\n' || value.back() == '\r')) + { + value.pop_back(); + } + + return value; + } + Json::Value YamlScalarNodeToJson(const YAML::Node& scalarNode, YamlScalarType scalarType) { if (scalarType == YamlScalarType::Int) @@ -59,7 +72,7 @@ namespace AppInstaller::Manifest::YamlParser } else { - return Json::Value(scalarNode.as()); + return Json::Value(TrimTrailingLineBreaks(scalarNode.as())); } } diff --git a/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp b/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp index ad02015068..60df8f56f4 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp @@ -247,7 +247,7 @@ namespace AppInstaller::Manifest std::vector installerOnlyFields = { { "Arch", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { variant_ptr(v)->Arch = Utility::ConvertToArchitectureEnum(value.as()); return {}; } }, - { "Url", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { variant_ptr(v)->Url = value.as(); return {}; } }, + { "Url", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { variant_ptr(v)->Url = Utility::Trim(value.as()); return {}; } }, { "Sha256", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { variant_ptr(v)->Sha256 = Utility::SHA256::ConvertToBytes(value.as()); return {}; } }, { "SignatureSha256", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { variant_ptr(v)->SignatureSha256 = Utility::SHA256::ConvertToBytes(value.as()); return {}; } }, { "Language", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { variant_ptr(v)->Locale = value.as(); return {}; } }, @@ -307,7 +307,7 @@ namespace AppInstaller::Manifest std::vector v1InstallerFields = { { "Architecture", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { variant_ptr(v)->Arch = Utility::ConvertToArchitectureEnum(value.as()); return {}; } }, - { "InstallerUrl", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { variant_ptr(v)->Url = value.as(); return {}; } }, + { "InstallerUrl", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { variant_ptr(v)->Url = Utility::Trim(value.as()); return {}; } }, { "InstallerSha256", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { variant_ptr(v)->Sha256 = Utility::SHA256::ConvertToBytes(value.as()); return {}; } }, { "SignatureSha256", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { variant_ptr(v)->SignatureSha256 = Utility::SHA256::ConvertToBytes(value.as()); return {}; } }, // No custom validation needed at field populating time since we have semantic validation later to block msstore and productId from community repo.