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();