diff --git a/src/AppInstallerCLICore/Commands/ValidateCommand.cpp b/src/AppInstallerCLICore/Commands/ValidateCommand.cpp
index b569fb9239..ad23874be6 100644
--- a/src/AppInstallerCLICore/Commands/ValidateCommand.cpp
+++ b/src/AppInstallerCLICore/Commands/ValidateCommand.cpp
@@ -46,6 +46,7 @@ namespace AppInstaller::CLI
{
ManifestValidateOption validateOption;
validateOption.FullValidation = true;
+ validateOption.SchemaHeaderValidationAsWarning = true;
validateOption.ThrowOnWarning = !(context.Args.Contains(Execution::Args::Type::IgnoreWarnings));
auto manifest = YamlParser::CreateFromPath(inputFile, validateOption);
diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestGoodManifestV1_10-SchemaHeader.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestGoodManifestV1_10-SchemaHeader.yaml
new file mode 100644
index 0000000000..199dc578ca
--- /dev/null
+++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestGoodManifestV1_10-SchemaHeader.yaml
@@ -0,0 +1,16 @@
+# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.10.0.schema.json
+PackageIdentifier: AppInstallerCliTest.SchemaHeader
+PackageVersion: 1.0.0.0
+PackageLocale: en-US
+PackageName: AppInstaller Test Schema Header
+Publisher: Microsoft Corporation
+License: Test
+ShortDescription: This manifest with schema header
+
+Installers:
+ - Architecture: x86
+ InstallerUrl: https://ThisIsNotUsed
+ InstallerType: msi
+ InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B
+ManifestType: singleton
+ManifestVersion: 1.10.0
diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestWarningManifestV1_10-SchemaHeaderInvalid.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestWarningManifestV1_10-SchemaHeaderInvalid.yaml
new file mode 100644
index 0000000000..305aed7d8d
--- /dev/null
+++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestWarningManifestV1_10-SchemaHeaderInvalid.yaml
@@ -0,0 +1,16 @@
+# yaml-language-server= $schema=https://aka.ms/winget-manifest.singleton.1.10.0.schema.json
+PackageIdentifier: AppInstallerCliTest.SchemaHeaderInvalid
+PackageVersion: 1.0.0.0
+PackageLocale: en-US
+PackageName: AppInstaller Test Schema Header Invalid
+Publisher: Microsoft Corporation
+License: Test
+ShortDescription: This manifest has an invalid schema header
+
+Installers:
+ - Architecture: x86
+ InstallerUrl: https://ThisIsNotUsed
+ InstallerType: msi
+ InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B
+ManifestType: singleton
+ManifestVersion: 1.10.0
diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestWarningManifestV1_10-SchemaHeaderManifestTypeMismatch.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestWarningManifestV1_10-SchemaHeaderManifestTypeMismatch.yaml
new file mode 100644
index 0000000000..ce370d7dd3
--- /dev/null
+++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestWarningManifestV1_10-SchemaHeaderManifestTypeMismatch.yaml
@@ -0,0 +1,16 @@
+# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.10.0.schema.json
+PackageIdentifier: AppInstallerCliTest.SchemaHeaderManifestTypeMismatch
+PackageVersion: 1.0.0.0
+PackageLocale: en-US
+PackageName: AppInstaller Test Schema Header ManifestType Mismatch
+Publisher: Microsoft Corporation
+License: Test
+ShortDescription: This manifest has a mismatched ManisfestType in the schema header
+
+Installers:
+ - Architecture: x86
+ InstallerUrl: https://ThisIsNotUsed
+ InstallerType: msi
+ InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B
+ManifestType: singleton
+ManifestVersion: 1.10.0
diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestWarningManifestV1_10-SchemaHeaderNotFound.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestWarningManifestV1_10-SchemaHeaderNotFound.yaml
new file mode 100644
index 0000000000..2e42d9a7b7
--- /dev/null
+++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestWarningManifestV1_10-SchemaHeaderNotFound.yaml
@@ -0,0 +1,15 @@
+PackageIdentifier: AppInstallerCliTest.SchemaHeaderNotFound
+PackageVersion: 1.0.0.0
+PackageLocale: en-US
+PackageName: AppInstaller Test Schema Header Not Found
+Publisher: Microsoft Corporation
+License: Test
+ShortDescription: This manifest has a missing schema header
+
+Installers:
+ - Architecture: x86
+ InstallerUrl: https://ThisIsNotUsed
+ InstallerType: msi
+ InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B
+ManifestType: singleton
+ManifestVersion: 1.10.0
diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestWarningManifestV1_10-SchemaHeaderURLPatternMismatch.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestWarningManifestV1_10-SchemaHeaderURLPatternMismatch.yaml
new file mode 100644
index 0000000000..2f6fe41a52
--- /dev/null
+++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestWarningManifestV1_10-SchemaHeaderURLPatternMismatch.yaml
@@ -0,0 +1,16 @@
+# yaml-language-server: $schema=https://aka.ms/winget-manifest-invalid.singleton.1.10.0.schema.json
+PackageIdentifier: AppInstallerCliTest.SchemaHeaderURLPatternMismatch
+PackageVersion: 1.0.0.0
+PackageLocale: en-US
+PackageName: AppInstaller Test Schema Header URL Pattern Mismatch
+Publisher: Microsoft Corporation
+License: Test
+ShortDescription: This manifest has a mismatched schema header URL pattern
+
+Installers:
+ - Architecture: x86
+ InstallerUrl: https://ThisIsNotUsed
+ InstallerType: msi
+ InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B
+ManifestType: singleton
+ManifestVersion: 1.10.0
diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestWarningManifestV1_10-SchemaHeaderVersionMismatch.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestWarningManifestV1_10-SchemaHeaderVersionMismatch.yaml
new file mode 100644
index 0000000000..630bb21182
--- /dev/null
+++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestWarningManifestV1_10-SchemaHeaderVersionMismatch.yaml
@@ -0,0 +1,16 @@
+# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.9.0.schema.json
+PackageIdentifier: AppInstallerCliTest.SchemaHeaderVersionMismatch
+PackageVersion: 1.0.0.0
+PackageLocale: en-US
+PackageName: AppInstaller Test Schema Header ManifestVersion Mismatch
+Publisher: Microsoft Corporation
+License: Test
+ShortDescription: This manifest has a mismatched ManisfestVersion in the schema header
+
+Installers:
+ - Architecture: x86
+ InstallerUrl: https://ThisIsNotUsed
+ InstallerType: msi
+ InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B
+ManifestType: singleton
+ManifestVersion: 1.10.0
diff --git a/src/AppInstallerCLIE2ETests/ValidateCommand.cs b/src/AppInstallerCLIE2ETests/ValidateCommand.cs
index 5f1f7b89b5..4893568dbc 100644
--- a/src/AppInstallerCLIE2ETests/ValidateCommand.cs
+++ b/src/AppInstallerCLIE2ETests/ValidateCommand.cs
@@ -80,5 +80,47 @@ public void ValidateManifestDoesNotExist()
Assert.AreEqual(Constants.ErrorCode.ERROR_PATH_NOT_FOUND, result.ExitCode);
Assert.True(result.StdOut.Contains("Path does not exist"));
}
+
+ ///
+ /// Test validate manifest with invalid schema and expect warnings.
+ ///
+ [Test]
+ public void ValidateManifestV1_10_SchemaHeaderExpectWarnings()
+ {
+ var result = TestCommon.RunAICLICommand("validate", TestCommon.GetTestDataFile("Manifests\\TestWarningManifestV1_10-SchemaHeaderNotFound.yaml"));
+ Assert.AreEqual(Constants.ErrorCode.ERROR_MANIFEST_VALIDATION_WARNING, result.ExitCode);
+ Assert.True(result.StdOut.Contains("Manifest validation succeeded with warnings."));
+ Assert.True(result.StdOut.Contains("Manifest Warning: Schema header not found."));
+
+ result = TestCommon.RunAICLICommand("validate", TestCommon.GetTestDataFile("Manifests\\TestWarningManifestV1_10-SchemaHeaderInvalid.yaml"));
+ Assert.AreEqual(Constants.ErrorCode.ERROR_MANIFEST_VALIDATION_WARNING, result.ExitCode);
+ Assert.True(result.StdOut.Contains("Manifest validation succeeded with warnings."));
+ Assert.True(result.StdOut.Contains("Manifest Warning: The schema header is invalid. Please verify that the schema header is present and formatted correctly."));
+
+ result = TestCommon.RunAICLICommand("validate", TestCommon.GetTestDataFile("Manifests\\TestWarningManifestV1_10-SchemaHeaderURLPatternMismatch.yaml"));
+ Assert.AreEqual(Constants.ErrorCode.ERROR_MANIFEST_VALIDATION_WARNING, result.ExitCode);
+ Assert.True(result.StdOut.Contains("Manifest validation succeeded with warnings."));
+ Assert.True(result.StdOut.Contains("Manifest Warning: The schema header URL does not match the expected pattern"));
+
+ result = TestCommon.RunAICLICommand("validate", TestCommon.GetTestDataFile("Manifests\\TestWarningManifestV1_10-SchemaHeaderManifestTypeMismatch.yaml"));
+ Assert.AreEqual(Constants.ErrorCode.ERROR_MANIFEST_VALIDATION_WARNING, result.ExitCode);
+ Assert.True(result.StdOut.Contains("Manifest validation succeeded with warnings."));
+ Assert.True(result.StdOut.Contains("Manifest Warning: The manifest type in the schema header does not match the ManifestType property value in the manifest."));
+
+ result = TestCommon.RunAICLICommand("validate", TestCommon.GetTestDataFile("Manifests\\TestWarningManifestV1_10-SchemaHeaderVersionMismatch.yaml"));
+ Assert.AreEqual(Constants.ErrorCode.ERROR_MANIFEST_VALIDATION_WARNING, result.ExitCode);
+ Assert.True(result.StdOut.Contains("Manifest validation succeeded with warnings."));
+ Assert.True(result.StdOut.Contains("Manifest Warning: The manifest version in the schema header does not match the ManifestVersion property value in the manifest."));
+ }
+
+ ///
+ /// Test validate manifest with valid schema header.
+ ///
+ [Test]
+ public void ValidateManifestV1_10_SchemaHeaderExpectNoWarning()
+ {
+ var result = TestCommon.RunAICLICommand("validate", TestCommon.GetTestDataFile("Manifests\\TestGoodManifestV1_10-SchemaHeader.yaml"));
+ Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode);
+ }
}
}
\ No newline at end of file
diff --git a/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilManifest.cs b/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilManifest.cs
index 0cb5e42b0c..ab0d76a223 100644
--- a/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilManifest.cs
+++ b/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilManifest.cs
@@ -1,4 +1,4 @@
-// -----------------------------------------------------------------------------
+// -----------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
//
@@ -80,6 +80,85 @@ public void WinGetUtil_ValidateManifest_Success(WinGetUtilWrapper.CreateManifest
// Close manifest
WinGetUtilWrapper.WinGetCloseManifest(manifestHandle);
- }
- }
+ }
+
+ ///
+ /// Test validate manifest with schema header.
+ ///
+ /// Create manifest options.
+ [Test]
+ [TestCase(WinGetUtilWrapper.CreateManifestOption.NoValidation)]
+ [TestCase(WinGetUtilWrapper.CreateManifestOption.SchemaAndSemanticValidation)]
+ public void WinGetUtil_ValidateManifest_V1_10_WithSchemaHeader_Success(WinGetUtilWrapper.CreateManifestOption createManifestOption)
+ {
+ string manifestsFilePath = TestCommon.GetTestDataFile(@"Manifests\TestGoodManifestV1_10-SchemaHeader.yaml");
+
+ // Create manifest
+ WinGetUtilWrapper.WinGetCreateManifest(
+ manifestsFilePath,
+ out bool succeeded,
+ out IntPtr manifestHandle,
+ out string createFailureMessage,
+ string.Empty,
+ createManifestOption);
+
+ Assert.True(succeeded);
+ Assert.AreNotEqual(IntPtr.Zero, manifestHandle);
+ Assert.IsNull(createFailureMessage);
+
+ // Close manifest
+ WinGetUtilWrapper.WinGetCloseManifest(manifestHandle);
+ }
+
+ ///
+ /// Test validate manifest with schema header for failure scenarios.
+ ///
+ /// Create manifest options.
+ [Test]
+ [TestCase(WinGetUtilWrapper.CreateManifestOption.SchemaAndSemanticValidation)]
+ public void WinGetUtil_ValidateManifest_V1_10_WithSchemaHeader_Failure(WinGetUtilWrapper.CreateManifestOption createManifestOption)
+ {
+ // Schema header not found
+ string manifestsFilePath = TestCommon.GetTestDataFile(@"Manifests\TestWarningManifestV1_10-SchemaHeaderNotFound.yaml");
+ string expectedError = "Manifest Error: Schema header not found.";
+ ValidateSchemaHeaderFailure(manifestsFilePath, createManifestOption, expectedError);
+
+ // Schema header invalid
+ manifestsFilePath = TestCommon.GetTestDataFile(@"Manifests\TestWarningManifestV1_10-SchemaHeaderInvalid.yaml");
+ expectedError = "Manifest Error: The schema header is invalid. Please verify that the schema header is present and formatted correctly.";
+ ValidateSchemaHeaderFailure(manifestsFilePath, createManifestOption, expectedError);
+
+ // Schema header URL pattern mismatch
+ manifestsFilePath = TestCommon.GetTestDataFile(@"Manifests\TestWarningManifestV1_10-SchemaHeaderURLPatternMismatch.yaml");
+ expectedError = "Manifest Error: The schema header URL does not match the expected pattern.";
+ ValidateSchemaHeaderFailure(manifestsFilePath, createManifestOption, expectedError);
+
+ // Schema header manifest type mismatch
+ manifestsFilePath = TestCommon.GetTestDataFile(@"Manifests\TestWarningManifestV1_10-SchemaHeaderManifestTypeMismatch.yaml");
+ expectedError = "Manifest Error: The manifest type in the schema header does not match the ManifestType property value in the manifest.";
+ ValidateSchemaHeaderFailure(manifestsFilePath, createManifestOption, expectedError);
+
+ // Schema header version mismatch
+ manifestsFilePath = TestCommon.GetTestDataFile(@"Manifests\TestWarningManifestV1_10-SchemaHeaderVersionMismatch.yaml");
+ expectedError = "Manifest Error: The manifest version in the schema header does not match the ManifestVersion property value in the manifest.";
+ ValidateSchemaHeaderFailure(manifestsFilePath, createManifestOption, expectedError);
+ }
+
+ private static void ValidateSchemaHeaderFailure(string manifestsFilePath, WinGetUtilWrapper.CreateManifestOption createManifestOption, string expectedError)
+ {
+ // Create manifest
+ WinGetUtilWrapper.WinGetCreateManifest(
+ manifestsFilePath,
+ out bool succeeded,
+ out IntPtr manifestHandle,
+ out string createFailureMessage,
+ string.Empty,
+ createManifestOption);
+
+ Assert.False(succeeded);
+ Assert.AreEqual(IntPtr.Zero, manifestHandle);
+ Assert.IsNotNull(createFailureMessage);
+ Assert.IsTrue(createFailureMessage.Contains(expectedError));
+ }
+ }
}
diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
index 5bf26983b7..538c2f6367 100644
--- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
+++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
@@ -1033,6 +1033,21 @@
true
+
+ true
+
+
+ true
+
+
+ true
+
+
+ true
+
+
+ true
+
@@ -1076,4 +1091,4 @@
-
+
\ No newline at end of file
diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters
index 5895890272..5da81fec15 100644
--- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters
+++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters
@@ -1044,5 +1044,20 @@
TestData
+
+ TestData
+
+
+ TestData
+
+
+ TestData
+
+
+ TestData
+
+
+ TestData
+
\ No newline at end of file
diff --git a/src/AppInstallerCLITests/TestData/ManifestV1_10-Bad-SchemaHeaderInvalid.yaml b/src/AppInstallerCLITests/TestData/ManifestV1_10-Bad-SchemaHeaderInvalid.yaml
new file mode 100644
index 0000000000..305aed7d8d
--- /dev/null
+++ b/src/AppInstallerCLITests/TestData/ManifestV1_10-Bad-SchemaHeaderInvalid.yaml
@@ -0,0 +1,16 @@
+# yaml-language-server= $schema=https://aka.ms/winget-manifest.singleton.1.10.0.schema.json
+PackageIdentifier: AppInstallerCliTest.SchemaHeaderInvalid
+PackageVersion: 1.0.0.0
+PackageLocale: en-US
+PackageName: AppInstaller Test Schema Header Invalid
+Publisher: Microsoft Corporation
+License: Test
+ShortDescription: This manifest has an invalid schema header
+
+Installers:
+ - Architecture: x86
+ InstallerUrl: https://ThisIsNotUsed
+ InstallerType: msi
+ InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B
+ManifestType: singleton
+ManifestVersion: 1.10.0
diff --git a/src/AppInstallerCLITests/TestData/ManifestV1_10-Bad-SchemaHeaderManifestTypeMismatch.yaml b/src/AppInstallerCLITests/TestData/ManifestV1_10-Bad-SchemaHeaderManifestTypeMismatch.yaml
new file mode 100644
index 0000000000..ce370d7dd3
--- /dev/null
+++ b/src/AppInstallerCLITests/TestData/ManifestV1_10-Bad-SchemaHeaderManifestTypeMismatch.yaml
@@ -0,0 +1,16 @@
+# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.10.0.schema.json
+PackageIdentifier: AppInstallerCliTest.SchemaHeaderManifestTypeMismatch
+PackageVersion: 1.0.0.0
+PackageLocale: en-US
+PackageName: AppInstaller Test Schema Header ManifestType Mismatch
+Publisher: Microsoft Corporation
+License: Test
+ShortDescription: This manifest has a mismatched ManisfestType in the schema header
+
+Installers:
+ - Architecture: x86
+ InstallerUrl: https://ThisIsNotUsed
+ InstallerType: msi
+ InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B
+ManifestType: singleton
+ManifestVersion: 1.10.0
diff --git a/src/AppInstallerCLITests/TestData/ManifestV1_10-Bad-SchemaHeaderManifestVersionMismatch.yaml b/src/AppInstallerCLITests/TestData/ManifestV1_10-Bad-SchemaHeaderManifestVersionMismatch.yaml
new file mode 100644
index 0000000000..630bb21182
--- /dev/null
+++ b/src/AppInstallerCLITests/TestData/ManifestV1_10-Bad-SchemaHeaderManifestVersionMismatch.yaml
@@ -0,0 +1,16 @@
+# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.9.0.schema.json
+PackageIdentifier: AppInstallerCliTest.SchemaHeaderVersionMismatch
+PackageVersion: 1.0.0.0
+PackageLocale: en-US
+PackageName: AppInstaller Test Schema Header ManifestVersion Mismatch
+Publisher: Microsoft Corporation
+License: Test
+ShortDescription: This manifest has a mismatched ManisfestVersion in the schema header
+
+Installers:
+ - Architecture: x86
+ InstallerUrl: https://ThisIsNotUsed
+ InstallerType: msi
+ InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B
+ManifestType: singleton
+ManifestVersion: 1.10.0
diff --git a/src/AppInstallerCLITests/TestData/ManifestV1_10-Bad-SchemaHeaderNotFound.yaml b/src/AppInstallerCLITests/TestData/ManifestV1_10-Bad-SchemaHeaderNotFound.yaml
new file mode 100644
index 0000000000..2e42d9a7b7
--- /dev/null
+++ b/src/AppInstallerCLITests/TestData/ManifestV1_10-Bad-SchemaHeaderNotFound.yaml
@@ -0,0 +1,15 @@
+PackageIdentifier: AppInstallerCliTest.SchemaHeaderNotFound
+PackageVersion: 1.0.0.0
+PackageLocale: en-US
+PackageName: AppInstaller Test Schema Header Not Found
+Publisher: Microsoft Corporation
+License: Test
+ShortDescription: This manifest has a missing schema header
+
+Installers:
+ - Architecture: x86
+ InstallerUrl: https://ThisIsNotUsed
+ InstallerType: msi
+ InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B
+ManifestType: singleton
+ManifestVersion: 1.10.0
diff --git a/src/AppInstallerCLITests/TestData/ManifestV1_10-Bad-SchemaHeaderURLPatternMismatch.yaml b/src/AppInstallerCLITests/TestData/ManifestV1_10-Bad-SchemaHeaderURLPatternMismatch.yaml
new file mode 100644
index 0000000000..2f6fe41a52
--- /dev/null
+++ b/src/AppInstallerCLITests/TestData/ManifestV1_10-Bad-SchemaHeaderURLPatternMismatch.yaml
@@ -0,0 +1,16 @@
+# yaml-language-server: $schema=https://aka.ms/winget-manifest-invalid.singleton.1.10.0.schema.json
+PackageIdentifier: AppInstallerCliTest.SchemaHeaderURLPatternMismatch
+PackageVersion: 1.0.0.0
+PackageLocale: en-US
+PackageName: AppInstaller Test Schema Header URL Pattern Mismatch
+Publisher: Microsoft Corporation
+License: Test
+ShortDescription: This manifest has a mismatched schema header URL pattern
+
+Installers:
+ - Architecture: x86
+ InstallerUrl: https://ThisIsNotUsed
+ InstallerType: msi
+ InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B
+ManifestType: singleton
+ManifestVersion: 1.10.0
diff --git a/src/AppInstallerCLITests/TestData/ManifestV1_10-Singleton.yaml b/src/AppInstallerCLITests/TestData/ManifestV1_10-Singleton.yaml
index bf13ddc447..3b68b17acb 100644
--- a/src/AppInstallerCLITests/TestData/ManifestV1_10-Singleton.yaml
+++ b/src/AppInstallerCLITests/TestData/ManifestV1_10-Singleton.yaml
@@ -1,3 +1,4 @@
+# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.10.0.schema.json
PackageIdentifier: microsoft.msixsdk
PackageVersion: 1.7.32
PackageLocale: en-US
diff --git a/src/AppInstallerCLITests/TestData/MultiFileManifestV1_10/ManifestV1_10-MultiFile-DefaultLocale.yaml b/src/AppInstallerCLITests/TestData/MultiFileManifestV1_10/ManifestV1_10-MultiFile-DefaultLocale.yaml
index 6dd4c32c51..3a35bbbe7a 100644
--- a/src/AppInstallerCLITests/TestData/MultiFileManifestV1_10/ManifestV1_10-MultiFile-DefaultLocale.yaml
+++ b/src/AppInstallerCLITests/TestData/MultiFileManifestV1_10/ManifestV1_10-MultiFile-DefaultLocale.yaml
@@ -1,3 +1,4 @@
+# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.10.0.schema.json
PackageIdentifier: microsoft.msixsdk
PackageVersion: 1.7.32
PackageLocale: en-US
diff --git a/src/AppInstallerCLITests/TestData/MultiFileManifestV1_10/ManifestV1_10-MultiFile-Installer.yaml b/src/AppInstallerCLITests/TestData/MultiFileManifestV1_10/ManifestV1_10-MultiFile-Installer.yaml
index b7c1958495..e241eb09a9 100644
--- a/src/AppInstallerCLITests/TestData/MultiFileManifestV1_10/ManifestV1_10-MultiFile-Installer.yaml
+++ b/src/AppInstallerCLITests/TestData/MultiFileManifestV1_10/ManifestV1_10-MultiFile-Installer.yaml
@@ -1,3 +1,4 @@
+# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.10.0.schema.json
PackageIdentifier: microsoft.msixsdk
PackageVersion: 1.7.32
InstallerLocale: en-US
diff --git a/src/AppInstallerCLITests/TestData/MultiFileManifestV1_10/ManifestV1_10-MultiFile-Locale.yaml b/src/AppInstallerCLITests/TestData/MultiFileManifestV1_10/ManifestV1_10-MultiFile-Locale.yaml
index 65e42dbd3e..76a79a934d 100644
--- a/src/AppInstallerCLITests/TestData/MultiFileManifestV1_10/ManifestV1_10-MultiFile-Locale.yaml
+++ b/src/AppInstallerCLITests/TestData/MultiFileManifestV1_10/ManifestV1_10-MultiFile-Locale.yaml
@@ -1,3 +1,4 @@
+# yaml-language-server: $schema=https://aka.ms/winget-manifest.locale.1.10.0.schema.json
PackageIdentifier: microsoft.msixsdk
PackageVersion: 1.7.32
PackageLocale: en-GB
diff --git a/src/AppInstallerCLITests/TestData/MultiFileManifestV1_10/ManifestV1_10-MultiFile-Version.yaml b/src/AppInstallerCLITests/TestData/MultiFileManifestV1_10/ManifestV1_10-MultiFile-Version.yaml
index b31d77874b..1ae085fdc7 100644
--- a/src/AppInstallerCLITests/TestData/MultiFileManifestV1_10/ManifestV1_10-MultiFile-Version.yaml
+++ b/src/AppInstallerCLITests/TestData/MultiFileManifestV1_10/ManifestV1_10-MultiFile-Version.yaml
@@ -1,3 +1,4 @@
+# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.10.0.schema.json
PackageIdentifier: microsoft.msixsdk
PackageVersion: 1.7.32
DefaultLocale: en-US
diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp
index 0bbe8243cc..4e536fc6b7 100644
--- a/src/AppInstallerCLITests/YamlManifest.cpp
+++ b/src/AppInstallerCLITests/YamlManifest.cpp
@@ -1846,6 +1846,27 @@ TEST_CASE("ManifestArpVersionRange", "[ManifestValidation]")
REQUIRE(arpRangeMultiArp.GetMaxVersion().ToString() == "13.0");
}
+TEST_CASE("ManifestV1_10_SchemaHeaderValidations", "[ManifestValidation]")
+{
+ ManifestValidateOption validateOption;
+ validateOption.FullValidation = true;
+
+ // Schema header not found
+ REQUIRE_THROWS_MATCHES(YamlParser::CreateFromPath(TestDataFile("ManifestV1_10-Bad-SchemaHeaderNotFound.yaml"),validateOption), ManifestException, ManifestExceptionMatcher("Schema header not found"));
+
+ // Schema header not valid
+ REQUIRE_THROWS_MATCHES(YamlParser::CreateFromPath(TestDataFile("ManifestV1_10-Bad-SchemaHeaderInvalid.yaml"), validateOption), ManifestException, ManifestExceptionMatcher("The schema header is invalid. Please verify that the schema header is present and formatted correctly."));
+
+ // Schema header URL does not match the expected schema URL
+ REQUIRE_THROWS_MATCHES(YamlParser::CreateFromPath(TestDataFile("ManifestV1_10-Bad-SchemaHeaderURLPatternMismatch.yaml"), validateOption), ManifestException, ManifestExceptionMatcher("The schema header URL does not match the expected pattern."));
+
+ // Schema header ManifestType does not match the expected value
+ REQUIRE_THROWS_MATCHES(YamlParser::CreateFromPath(TestDataFile("ManifestV1_10-Bad-SchemaHeaderManifestTypeMismatch.yaml"), validateOption), ManifestException, ManifestExceptionMatcher("The manifest type in the schema header does not match the ManifestType property value in the manifest."));
+
+ // Schema header version does not match the expected version
+ REQUIRE_THROWS_MATCHES(YamlParser::CreateFromPath(TestDataFile("ManifestV1_10-Bad-SchemaHeaderManifestVersionMismatch.yaml"), validateOption), ManifestException, ManifestExceptionMatcher("The manifest version in the schema header does not match the ManifestVersion property value in the manifest."));
+}
+
TEST_CASE("ShadowManifest", "[ShadowManifest]")
{
ManifestValidateOption validateOption;
diff --git a/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp
index d25182d0a0..a7078dbbf4 100644
--- a/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp
+++ b/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp
@@ -24,7 +24,7 @@ namespace AppInstaller::Manifest::YamlParser
};
// List of fields that use non string scalar types
- const std::map ManifestFieldTypes=
+ const std::map ManifestFieldTypes =
{
{ "InstallerSuccessCodes"sv, YamlScalarType::Int },
{ "InstallerAbortsTerminal"sv, YamlScalarType::Bool },
@@ -98,6 +98,173 @@ namespace AppInstaller::Manifest::YamlParser
return result;
}
+
+ std::vector ParseSchemaHeaderString(const YamlManifestInfo& manifestInfo, const ValidationError::Level& errorLevel, std::string& schemaHeaderUrlString)
+ {
+ std::vector errors;
+ std::string schemaHeader = manifestInfo.DocumentSchemaHeader.SchemaHeader;
+
+ // Remove the leading '#' and any leading/trailing whitespaces
+ if (schemaHeader[0] == '#')
+ {
+ schemaHeader = schemaHeader.substr(1); // Remove the leading '#'
+ schemaHeader = Utility::Trim(schemaHeader); // Trim leading/trailing whitespaces
+ }
+
+ // Parse the schema header string as YAML string to get the schema header URL
+ try
+ {
+ auto root = YAML::Load(schemaHeader);
+
+ if (root.IsNull() || (!root.IsNull() && !root.IsDefined()))
+ {
+ errors.emplace_back(ValidationError::MessageContextValueLineLevelWithFile(ManifestError::InvalidSchemaHeader, "", schemaHeader, manifestInfo.DocumentSchemaHeader.Mark.line, manifestInfo.DocumentSchemaHeader.Mark.column, errorLevel, manifestInfo.FileName));
+ }
+ else
+ {
+ schemaHeaderUrlString = root[YAML::DocumentSchemaHeader::YamlLanguageServerKey].as();
+ }
+ }
+ catch (const YAML::Exception&)
+ {
+ errors.emplace_back(ValidationError::MessageContextValueLineLevelWithFile(ManifestError::InvalidSchemaHeader, "", schemaHeader, manifestInfo.DocumentSchemaHeader.Mark.line, manifestInfo.DocumentSchemaHeader.Mark.column, errorLevel, manifestInfo.FileName));
+ }
+ catch (const std::exception&)
+ {
+ errors.emplace_back(ValidationError::MessageContextValueLineLevelWithFile(ManifestError::InvalidSchemaHeader, "", schemaHeader, manifestInfo.DocumentSchemaHeader.Mark.line, manifestInfo.DocumentSchemaHeader.Mark.column, errorLevel, manifestInfo.FileName));
+ }
+
+ return errors;
+ }
+
+ bool ParseSchemaHeaderUrl(const std::string& schemaHeaderValue, std::string& schemaType, std::string& schemaVersion)
+ {
+ // Use regex to match the pattern of @"winget-manifest\.(?\w+)\.(?[\d\.]+)\.schema\.json$"
+ std::regex schemaUrlPattern(R"(winget-manifest\.(\w+)\.([\d\.]+)\.schema\.json$)");
+ std::smatch match;
+
+ if (std::regex_search(schemaHeaderValue, match, schemaUrlPattern))
+ {
+ schemaType = match[1].str();
+ schemaVersion = match[2].str();
+ return true;
+ }
+
+ return false;
+ }
+
+ std::vector ValidateSchemaHeaderType(const std::string& headerManifestType, const ManifestTypeEnum& expectedManifestType, const YamlManifestInfo& manifestInfo, ValidationError::Level errorLevel)
+ {
+ std::vector errors;
+ ManifestTypeEnum actualManifestType = ConvertToManifestTypeEnum(headerManifestType);
+ size_t schemaHeaderTypeIndex = manifestInfo.DocumentSchemaHeader.SchemaHeader.find(headerManifestType) + 1;
+
+ if (actualManifestType != expectedManifestType)
+ {
+ errors.emplace_back(ValidationError::MessageContextValueLineLevelWithFile(ManifestError::SchemaHeaderManifestTypeMismatch, "", headerManifestType, manifestInfo.DocumentSchemaHeader.Mark.line, schemaHeaderTypeIndex, errorLevel, manifestInfo.FileName));
+ }
+
+ return errors;
+ }
+
+ std::vector ValidateSchemaHeaderVersion(const std::string& headerManifestVersion, const ManifestVer& expectedManifestVersion, const YamlManifestInfo& manifestInfo, ValidationError::Level errorLevel)
+ {
+ std::vector errors;
+ ManifestVer actualHeaderVersion(headerManifestVersion);
+ size_t schemaHeaderVersionIndex = manifestInfo.DocumentSchemaHeader.SchemaHeader.find(headerManifestVersion) + 1;
+
+ if (actualHeaderVersion != expectedManifestVersion)
+ {
+ errors.emplace_back(ValidationError::MessageContextValueLineLevelWithFile(ManifestError::SchemaHeaderManifestVersionMismatch, "", headerManifestVersion, manifestInfo.DocumentSchemaHeader.Mark.line, schemaHeaderVersionIndex, errorLevel, manifestInfo.FileName));
+ }
+
+ return errors;
+ }
+
+ bool IsValidSchemaHeaderUrl(const std::string& schemaHeaderUrlString, const YamlManifestInfo& manifestInfo, const ManifestVer& manifestVersion)
+ {
+ // Load the schema file to compare the schema header URL with the schema ID in the schema file
+ Json::Value schemaFile = LoadSchemaDoc(manifestVersion, manifestInfo.ManifestType);
+
+ if (schemaFile.isMember("$id"))
+ {
+ std::string schemaId = schemaFile["$id"].asString();
+
+ // Prefix schema ID with "schema=" to match the schema header URL pattern and compare it with the schema header URL
+ schemaId = "$schema=" + schemaId;
+
+ if (Utility::CaseInsensitiveEquals(schemaId, schemaHeaderUrlString))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ValidationError GetSchemaHeaderUrlPatternMismatchError(const std::string& schemaHeaderUrlString, const YamlManifestInfo& manifestInfo, const ValidationError::Level& errorLevel)
+ {
+ size_t schemaHeaderUrlIndex = manifestInfo.DocumentSchemaHeader.SchemaHeader.find(schemaHeaderUrlString) + 1;
+
+ return ValidationError::MessageContextValueLineLevelWithFile(ManifestError::SchemaHeaderUrlPatternMismatch, "", manifestInfo.DocumentSchemaHeader.SchemaHeader, manifestInfo.DocumentSchemaHeader.Mark.line, schemaHeaderUrlIndex, errorLevel, manifestInfo.FileName);
+ }
+
+ std::vector ValidateSchemaHeaderUrl(const YamlManifestInfo& manifestInfo, const ManifestVer& manifestVersion, const ValidationError::Level& errorLevel)
+ {
+ std::vector errors;
+
+ std::string schemaHeaderUrlString;
+ // Parse the schema header string to get the schema header URL
+ auto parserErrors = ParseSchemaHeaderString(manifestInfo, errorLevel, schemaHeaderUrlString);
+ std::move(parserErrors.begin(), parserErrors.end(), std::inserter(errors, errors.end()));
+
+ if (!errors.empty())
+ {
+ return errors;
+ }
+
+ std::string manifestTypeString;
+ std::string manifestVersionString;
+
+ // Parse the schema header URL to get the manifest type and version
+ if (ParseSchemaHeaderUrl(schemaHeaderUrlString, manifestTypeString, manifestVersionString))
+ {
+ auto headerManifestTypeErrors = ValidateSchemaHeaderType(manifestTypeString, manifestInfo.ManifestType, manifestInfo, errorLevel);
+ std::move(headerManifestTypeErrors.begin(), headerManifestTypeErrors.end(), std::inserter(errors, errors.end()));
+
+ auto headerManifestVersionErrors = ValidateSchemaHeaderVersion(manifestVersionString, manifestVersion, manifestInfo, errorLevel);
+ std::move(headerManifestVersionErrors.begin(), headerManifestVersionErrors.end(), std::inserter(errors, errors.end()));
+
+ // Finally, match the entire schema header URL with the schema ID in the schema file to ensure the URL domain matches the schema definition file.
+ if (!IsValidSchemaHeaderUrl(schemaHeaderUrlString, manifestInfo, manifestVersion))
+ {
+ errors.emplace_back(GetSchemaHeaderUrlPatternMismatchError(schemaHeaderUrlString, manifestInfo, errorLevel));
+ }
+ }
+ else
+ {
+ errors.emplace_back(GetSchemaHeaderUrlPatternMismatchError(schemaHeaderUrlString, manifestInfo, errorLevel));
+ }
+
+ return errors;
+ }
+
+ std::vector ValidateYamlManifestSchemaHeader(const YamlManifestInfo& manifestInfo, const ManifestVer& manifestVersion, ValidationError::Level errorLevel)
+ {
+ std::vector errors;
+ std::string schemaHeaderString;
+
+ if (manifestInfo.DocumentSchemaHeader.SchemaHeader.empty())
+ {
+ errors.emplace_back(ValidationError::MessageLevelWithFile(ManifestError::SchemaHeaderNotFound, errorLevel, manifestInfo.FileName));
+ return errors;
+ }
+
+ auto parserErrors = ValidateSchemaHeaderUrl(manifestInfo, manifestVersion, errorLevel);
+ std::move(parserErrors.begin(), parserErrors.end(), std::inserter(errors, errors.end()));
+
+ return errors;
+ }
}
Json::Value LoadSchemaDoc(const ManifestVer& manifestVersion, ManifestTypeEnum manifestType)
@@ -134,7 +301,7 @@ namespace AppInstaller::Manifest::YamlParser
{ ManifestTypeEnum::DefaultLocale, IDX_MANIFEST_SCHEMA_V1_7_DEFAULTLOCALE },
{ ManifestTypeEnum::Locale, IDX_MANIFEST_SCHEMA_V1_7_LOCALE },
};
- }
+ }
else if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_6 })
{
resourceMap = {
@@ -251,4 +418,25 @@ namespace AppInstaller::Manifest::YamlParser
return errors;
}
+
+ std::vector ValidateYamlManifestsSchemaHeader(const std::vector& manifestList, const ManifestVer& manifestVersion, bool treatErrorAsWarning)
+ {
+ std::vector errors;
+ ValidationError::Level errorLevel = treatErrorAsWarning ? ValidationError::Level::Warning : ValidationError::Level::Error;
+
+ // Read the manifest schema header and ensure it exists
+ for (const auto& entry : manifestList)
+ {
+ if (entry.ManifestType == ManifestTypeEnum::Shadow)
+ {
+ // There's no schema for a shadow manifest.
+ continue;
+ }
+
+ auto schemaHeaderErrors = ValidateYamlManifestSchemaHeader(entry, manifestVersion, errorLevel);
+ std::move(schemaHeaderErrors.begin(), schemaHeaderErrors.end(), std::inserter(errors, errors.end()));
+ }
+
+ return errors;
+ }
}
diff --git a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp
index d045a88292..cb42ea9c94 100644
--- a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp
+++ b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp
@@ -63,7 +63,12 @@ namespace AppInstaller::Manifest
{ AppInstaller::Manifest::ManifestError::ArpValidationError, "Arp Validation Error."sv },
{ AppInstaller::Manifest::ManifestError::SchemaError, "Schema Error."sv },
{ AppInstaller::Manifest::ManifestError::MsixSignatureHashFailed, "Failed to calculate MSIX signature hash.Please verify that the input file is a valid, signed MSIX."sv },
- { AppInstaller::Manifest::ManifestError::ShadowManifestNotAllowed, "Shadow manifest is not allowed." }
+ { AppInstaller::Manifest::ManifestError::ShadowManifestNotAllowed, "Shadow manifest is not allowed." },
+ { AppInstaller::Manifest::ManifestError::SchemaHeaderNotFound, "Schema header not found." },
+ { AppInstaller::Manifest::ManifestError::InvalidSchemaHeader , "The schema header is invalid. Please verify that the schema header is present and formatted correctly."sv },
+ { AppInstaller::Manifest::ManifestError::SchemaHeaderManifestTypeMismatch , "The manifest type in the schema header does not match the ManifestType property value in the manifest."sv },
+ { AppInstaller::Manifest::ManifestError::SchemaHeaderManifestVersionMismatch, "The manifest version in the schema header does not match the ManifestVersion property value in the manifest."sv },
+ { AppInstaller::Manifest::ManifestError::SchemaHeaderUrlPatternMismatch, "The schema header URL does not match the expected pattern."sv },
};
return ErrorIdToMessageMap;
diff --git a/src/AppInstallerCommonCore/Manifest/YamlParser.cpp b/src/AppInstallerCommonCore/Manifest/YamlParser.cpp
index 0223e79c0b..86bbd23d84 100644
--- a/src/AppInstallerCommonCore/Manifest/YamlParser.cpp
+++ b/src/AppInstallerCommonCore/Manifest/YamlParser.cpp
@@ -479,6 +479,14 @@ namespace AppInstaller::Manifest::YamlParser
{
errors = ValidateManifest(manifest);
std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end()));
+
+ // Validate the schema header for manifest version 1.10 and above
+ if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_10 })
+ {
+ // Validate the schema header.
+ errors = ValidateYamlManifestsSchemaHeader(input, manifestVersion, validateOption.SchemaHeaderValidationAsWarning);
+ std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end()));
+ }
}
if (validateOption.InstallerValidation)
@@ -518,18 +526,22 @@ namespace AppInstaller::Manifest::YamlParser
{
THROW_HR_IF_MSG(HRESULT_FROM_WIN32(ERROR_DIRECTORY_NOT_SUPPORTED), std::filesystem::is_directory(file.path()), "Subdirectory not supported in manifest path");
- YamlManifestInfo doc;
- doc.Root = YAML::Load(file.path());
- doc.FileName = file.path().filename().u8string();
- docList.emplace_back(std::move(doc));
+ YamlManifestInfo manifestInfo;
+ YAML::Document doc = YAML::LoadDocument(file.path());
+ manifestInfo.Root = std::move(doc).GetRoot();
+ manifestInfo.DocumentSchemaHeader = doc.GetSchemaHeader();
+ manifestInfo.FileName = file.path().filename().u8string();
+ docList.emplace_back(std::move(manifestInfo));
}
}
else
{
- YamlManifestInfo doc;
- doc.Root = YAML::Load(inputPath, doc.StreamSha256);
- doc.FileName = inputPath.filename().u8string();
- docList.emplace_back(std::move(doc));
+ YamlManifestInfo manifestInfo;
+ YAML::Document doc = YAML::LoadDocument(inputPath, manifestInfo.StreamSha256);
+ manifestInfo.Root = std::move(doc).GetRoot();
+ manifestInfo.DocumentSchemaHeader = doc.GetSchemaHeader();
+ manifestInfo.FileName = inputPath.filename().u8string();
+ docList.emplace_back(std::move(manifestInfo));
}
}
catch (const std::exception& e)
@@ -549,9 +561,11 @@ namespace AppInstaller::Manifest::YamlParser
try
{
- YamlManifestInfo doc;
- doc.Root = YAML::Load(input);
- docList.emplace_back(std::move(doc));
+ YamlManifestInfo manifestInfo;
+ YAML::Document doc = YAML::LoadDocument(input);
+ manifestInfo.Root = std::move(doc).GetRoot();
+ manifestInfo.DocumentSchemaHeader = doc.GetSchemaHeader();
+ docList.emplace_back(std::move(manifestInfo));
}
catch (const std::exception& e)
{
@@ -595,4 +609,4 @@ namespace AppInstaller::Manifest::YamlParser
return manifest;
}
-}
\ No newline at end of file
+}
diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h
index dc6b83bc2b..2e06654839 100644
--- a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h
+++ b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h
@@ -63,6 +63,7 @@ namespace AppInstaller::Manifest
bool FullValidation = false;
bool ThrowOnWarning = false;
bool AllowShadowManifest = false;
+ bool SchemaHeaderValidationAsWarning = false;
};
// ManifestVer is inherited from Utility::Version and is a more restricted version.
diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestSchemaValidation.h b/src/AppInstallerCommonCore/Public/winget/ManifestSchemaValidation.h
index 7e6d2a68c6..a170caf4bd 100644
--- a/src/AppInstallerCommonCore/Public/winget/ManifestSchemaValidation.h
+++ b/src/AppInstallerCommonCore/Public/winget/ManifestSchemaValidation.h
@@ -18,4 +18,10 @@ namespace AppInstaller::Manifest::YamlParser
std::vector ValidateAgainstSchema(
const std::vector& manifestList,
const ManifestVer& manifestVersion);
-}
\ No newline at end of file
+
+ // Validate the schema header of a list of manifests
+ std::vector ValidateYamlManifestsSchemaHeader(
+ const std::vector& manifestList,
+ const ManifestVer& manifestVersion,
+ bool treatErrorAsWarning = true);
+}
diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h b/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h
index 6ecc54b619..e1c958230e 100644
--- a/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h
+++ b/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h
@@ -65,7 +65,12 @@ namespace AppInstaller::Manifest
WINGET_DEFINE_RESOURCE_STRINGID(ScopeNotSupported);
WINGET_DEFINE_RESOURCE_STRINGID(ShadowManifestNotAllowed);
WINGET_DEFINE_RESOURCE_STRINGID(SingleManifestPackageHasDependencies);
- WINGET_DEFINE_RESOURCE_STRINGID(UnsupportedMultiFileManifestType);
+ WINGET_DEFINE_RESOURCE_STRINGID(UnsupportedMultiFileManifestType);
+ WINGET_DEFINE_RESOURCE_STRINGID(SchemaHeaderNotFound);
+ WINGET_DEFINE_RESOURCE_STRINGID(InvalidSchemaHeader);
+ WINGET_DEFINE_RESOURCE_STRINGID(SchemaHeaderManifestTypeMismatch);
+ WINGET_DEFINE_RESOURCE_STRINGID(SchemaHeaderManifestVersionMismatch);
+ WINGET_DEFINE_RESOURCE_STRINGID(SchemaHeaderUrlPatternMismatch);
}
struct ValidationError
@@ -134,6 +139,20 @@ namespace AppInstaller::Manifest
error.FileName = file;
return error;
}
+
+ static ValidationError MessageLevelWithFile(AppInstaller::StringResource::StringId message, Level level, std::string file)
+ {
+ ValidationError error{ message, level };
+ error.FileName = file;
+ return error;
+ }
+
+ static ValidationError MessageContextValueLineLevelWithFile(AppInstaller::StringResource::StringId message, std::string context, std::string value, size_t line, size_t column , Level level , std::string file)
+ {
+ ValidationError error{ message, context, value, line, column, level };
+ error.FileName = file;
+ return error;
+ }
};
struct ManifestException : public wil::ResultException
@@ -232,4 +251,4 @@ namespace AppInstaller::Manifest
std::vector ValidateManifest(const Manifest& manifest, bool fullValidation = true);
std::vector ValidateManifestLocalization(const ManifestLocalization& localization, bool treatErrorAsWarning = false);
std::vector ValidateManifestInstallers(const Manifest& manifest, bool treatErrorAsWarning = false);
-}
\ No newline at end of file
+}
diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestYamlParser.h b/src/AppInstallerCommonCore/Public/winget/ManifestYamlParser.h
index 0334fa7622..509b3bf86b 100644
--- a/src/AppInstallerCommonCore/Public/winget/ManifestYamlParser.h
+++ b/src/AppInstallerCommonCore/Public/winget/ManifestYamlParser.h
@@ -5,7 +5,6 @@
#include
#include
#include
-
#include
namespace AppInstaller::Manifest::YamlParser
@@ -18,6 +17,9 @@ namespace AppInstaller::Manifest::YamlParser
// File name of the manifest file if applicable for error reporting
std::string FileName;
+ // Schema header string found in the manifest file
+ YAML::DocumentSchemaHeader DocumentSchemaHeader;
+
// The SHA256 hash of the stream
Utility::SHA256::HashBuffer StreamSha256;
@@ -38,4 +40,4 @@ namespace AppInstaller::Manifest::YamlParser
std::vector& input,
ManifestValidateOption validateOption = {},
const std::filesystem::path& mergedManifestPath = {});
-}
\ No newline at end of file
+}
diff --git a/src/AppInstallerSharedLib/Public/winget/Yaml.h b/src/AppInstallerSharedLib/Public/winget/Yaml.h
index 59e582f123..2e4521430b 100644
--- a/src/AppInstallerSharedLib/Public/winget/Yaml.h
+++ b/src/AppInstallerSharedLib/Public/winget/Yaml.h
@@ -237,12 +237,44 @@ namespace AppInstaller::YAML
Folded,
};
+ // A schema header for a document.
+ struct DocumentSchemaHeader
+ {
+ DocumentSchemaHeader() = default;
+ DocumentSchemaHeader(std::string schemaHeaderString, const Mark& mark) : SchemaHeader(std::move(schemaHeaderString)), Mark(mark) {}
+
+ std::string SchemaHeader;
+ Mark Mark;
+ static constexpr std::string_view YamlLanguageServerKey = "yaml-language-server";
+ };
+
+ struct Document
+ {
+ Document() = default;
+ Document(Node root, DocumentSchemaHeader schemaHeader) : m_root(std::move(root)), m_schemaHeader(std::move(schemaHeader)) {}
+
+ const DocumentSchemaHeader& GetSchemaHeader() const { return m_schemaHeader; }
+
+ // Return r-values for move semantics
+ Node&& GetRoot() && { return std::move(m_root); }
+
+ private:
+ Node m_root;
+ DocumentSchemaHeader m_schemaHeader;
+ };
+
// Forward declaration to allow pImpl in this Emitter.
namespace Wrapper
{
struct Document;
}
+ // Loads from the input; returns the root node of the first document.
+ Document LoadDocument(std::string_view input);
+ Document LoadDocument(const std::string& input);
+ Document LoadDocument(const std::filesystem::path& input);
+ Document LoadDocument(const std::filesystem::path& input, Utility::SHA256::HashBuffer& hashOut);
+
// A YAML emitter.
struct Emitter
{
diff --git a/src/AppInstallerSharedLib/Yaml.cpp b/src/AppInstallerSharedLib/Yaml.cpp
index a263cf7253..1326256ac9 100644
--- a/src/AppInstallerSharedLib/Yaml.cpp
+++ b/src/AppInstallerSharedLib/Yaml.cpp
@@ -99,6 +99,35 @@ namespace AppInstaller::YAML
return Node::TagType::Unknown;
}
+
+ DocumentSchemaHeader ExtractSchemaHeaderFromYaml( const std::string& yamlDocument, size_t rootNodeLine)
+ {
+ std::istringstream input(yamlDocument);
+ std::string line;
+ size_t currentLine = 1;
+
+ // Search for the schema header string in the comments before the root node.
+ while (currentLine < rootNodeLine && std::getline(input, line))
+ {
+ std::string comment = Utility::Trim(line);
+
+ // Check if the line is a comment
+ if (!comment.empty() && comment[0] == '#')
+ {
+ size_t pos = line.find(DocumentSchemaHeader::YamlLanguageServerKey);
+
+ // Check if the comment contains the schema header string
+ if (pos != std::string::npos)
+ {
+ return DocumentSchemaHeader(std::move(comment), YAML::Mark{ currentLine, pos});
+ }
+ }
+
+ currentLine++;
+ }
+
+ return {};
+ }
}
Exception::Exception(Type type) :
@@ -601,6 +630,66 @@ namespace AppInstaller::YAML
return Load(input, &hashOut);
}
+ Document LoadDocument(std::string_view input)
+ {
+ Wrapper::Parser parser(input);
+ Wrapper::Document document = parser.Load();
+
+ if (document.HasRoot())
+ {
+ const Node root = document.GetRoot();
+ const DocumentSchemaHeader schemaHeader = ExtractSchemaHeaderFromYaml(parser.GetEncodedInput(), root.Mark().line);
+
+ return { root, schemaHeader };
+ }
+ else
+ {
+ // Return an empty root and schema header.
+ return {};
+ }
+ }
+
+ Document LoadDocument(const std::string& input)
+ {
+ return LoadDocument(static_cast(input));
+ }
+
+ Document LoadDocument(std::istream& input, Utility::SHA256::HashBuffer* hashOut)
+ {
+ Wrapper::Parser parser(input, hashOut);
+ Wrapper::Document document = parser.Load();
+
+ if (document.HasRoot())
+ {
+ const Node root = document.GetRoot();
+ const DocumentSchemaHeader schemaHeader = ExtractSchemaHeaderFromYaml(parser.GetEncodedInput(), root.Mark().line);
+
+ return { root, schemaHeader };
+ }
+ else
+ {
+ // Return an empty root and schema header.
+ return {};
+ }
+ }
+
+ Document LoadDocument(const std::filesystem::path& input, Utility::SHA256::HashBuffer* hashOut)
+ {
+ std::ifstream stream(input, std::ios_base::in | std::ios_base::binary);
+ THROW_LAST_ERROR_IF(stream.fail());
+ return LoadDocument(stream, hashOut);
+ }
+
+ Document LoadDocument(const std::filesystem::path& input)
+ {
+ return LoadDocument(input, nullptr);
+ }
+
+ Document LoadDocument(const std::filesystem::path& input, Utility::SHA256::HashBuffer& hashOut)
+ {
+ return LoadDocument(input, &hashOut);
+ }
+
Emitter::Emitter() :
m_document(std::make_unique(true))
{
diff --git a/src/AppInstallerSharedLib/YamlWrapper.h b/src/AppInstallerSharedLib/YamlWrapper.h
index d87141f56e..6fdc5f9f7d 100644
--- a/src/AppInstallerSharedLib/YamlWrapper.h
+++ b/src/AppInstallerSharedLib/YamlWrapper.h
@@ -83,6 +83,9 @@ namespace AppInstaller::YAML::Wrapper
// Loads the next document from the input, if one exists.
Document Load();
+ // Retrieves the input that was used to create the parser with the correct encoding scheme.
+ const std::string& GetEncodedInput() const { return m_input; }
+
private:
// Determines the type of encoding in use, transforming the input as necessary.
void PrepareInput();