From b74f1846633def6bd0d543012c04239442a5964e Mon Sep 17 00:00:00 2001 From: FOWEPJF255 Date: Sun, 18 Oct 2026 01:04:48 +0800 Subject: [PATCH 1/2] fix: accept YAML block-scalar trailing line breaks in schema patterns --- .../AppInstallerCLITests.vcxproj | 3 +++ .../AppInstallerCLITests.vcxproj.filters | 3 +++ .../Manifest-Good-InstallerUrl-BlockScalar.yaml | 17 +++++++++++++++++ src/AppInstallerCLITests/YamlManifest.cpp | 10 ++++++++++ .../Manifest/ManifestSchemaValidation.cpp | 15 ++++++++++++++- .../Manifest/ManifestYamlPopulator.cpp | 4 ++-- 6 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 src/AppInstallerCLITests/TestData/Manifest-Good-InstallerUrl-BlockScalar.yaml 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. From ff8a49100f8fea064590517d6328bf56754ff4ea Mon Sep 17 00:00:00 2001 From: FOWEPJF255 Date: Fri, 30 Oct 2026 00:55:44 +0800 Subject: [PATCH 2/2] fix: validate schema patterns with multiline regex --- ...anifest-Good-InstallerUrl-BlockScalar.yaml | 2 +- src/AppInstallerCLITests/YamlManifest.cpp | 2 +- .../Manifest/ManifestSchemaValidation.cpp | 15 +---------- .../Manifest/ManifestYamlPopulator.cpp | 4 +-- .../JsonSchemaValidation.cpp | 25 +++++++++++++++++-- 5 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/AppInstallerCLITests/TestData/Manifest-Good-InstallerUrl-BlockScalar.yaml b/src/AppInstallerCLITests/TestData/Manifest-Good-InstallerUrl-BlockScalar.yaml index 18d2ed7620..a36e3ef3c1 100644 --- a/src/AppInstallerCLITests/TestData/Manifest-Good-InstallerUrl-BlockScalar.yaml +++ b/src/AppInstallerCLITests/TestData/Manifest-Good-InstallerUrl-BlockScalar.yaml @@ -10,7 +10,7 @@ PackageLocale: en-US Installers: - Architecture: x86 InstallerUrl: > - https://rubengustorage.blob.core.windows.net/publiccontainer/msixsdk-x86.msi + https://ThisIsNotUsed InstallerSha256: 98B67758CEAFFCBB3FE47838FD0A8D7BD581C2650842D6B2B0E0D49A23270CCD InstallerType: msi ManifestType: singleton diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp index f78a5fdcb1..e00a467d9f 100644 --- a/src/AppInstallerCLITests/YamlManifest.cpp +++ b/src/AppInstallerCLITests/YamlManifest.cpp @@ -964,7 +964,7 @@ TEST_CASE("ReadGoodManifestInstallerUrlBlockScalar", "[ManifestValidation]") 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"); + REQUIRE(manifest.Installers[0].Url == "https://ThisIsNotUsed\n"); } TEST_CASE("ReadGoodManifests", "[ManifestValidation]") diff --git a/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp index fff3806f44..b58c2e7a32 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp @@ -47,19 +47,6 @@ 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) @@ -72,7 +59,7 @@ namespace AppInstaller::Manifest::YamlParser } else { - return Json::Value(TrimTrailingLineBreaks(scalarNode.as())); + return Json::Value(scalarNode.as()); } } diff --git a/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp b/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp index 60df8f56f4..ad02015068 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 = Utility::Trim(value.as()); return {}; } }, + { "Url", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { variant_ptr(v)->Url = 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 = Utility::Trim(value.as()); return {}; } }, + { "InstallerUrl", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { variant_ptr(v)->Url = 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. diff --git a/src/AppInstallerSharedLib/JsonSchemaValidation.cpp b/src/AppInstallerSharedLib/JsonSchemaValidation.cpp index 6cd43450ff..acd339be47 100644 --- a/src/AppInstallerSharedLib/JsonSchemaValidation.cpp +++ b/src/AppInstallerSharedLib/JsonSchemaValidation.cpp @@ -4,8 +4,29 @@ #include "winget/JsonSchemaValidation.h" #include "winget/Resources.h" +#include + namespace AppInstaller::JsonSchema { + namespace + { + struct MultilineRegexEngine + { + explicit MultilineRegexEngine(const std::string& pattern) : + Regex(pattern, std::regex_constants::ECMAScript | std::regex_constants::multiline) + { + } + + static bool search(const std::string& target, const MultilineRegexEngine& expression) + { + return std::regex_search(target, expression.Regex); + } + + private: + std::regex Regex; + }; + } + Json::Value LoadSchemaDoc(std::string_view schemaStr) { Json::Value schemaJson; @@ -35,7 +56,7 @@ namespace AppInstaller::JsonSchema bool Validate(const valijson::Schema& schema, const Json::Value& json, valijson::ValidationResults& results) { - valijson::Validator schemaValidator; + valijson::ValidatorT schemaValidator; valijson::adapters::JsonCppAdapter jsonAdapter(json); return schemaValidator.validate(schema, jsonAdapter, &results); } @@ -59,4 +80,4 @@ namespace AppInstaller::JsonSchema return ss.str(); } -} \ No newline at end of file +}