commit b1ac62234040ce63f96a294d23aeb040e0941738 parent f4f8abd5f3232bbe2967e7c01c3c6e7a1071af01 Author: Madhusudhan-MSFT <53235553+Madhusudhan-MSFT@users.noreply.github.com> Date: Fri, 17 Jan 2025 14:04:42 -0800 Yaml manifest schemaheader validation for V.1.10.0 and above (#5126) ### Enable schema header validation for YAML manifests from version 1.10.0 onwards. **YAML Manifest Validation Changes:** 1. Introduce functions to search, parse, and validate schema headers in YAML manifests. 2. Add/Update structs and enums to support schema header validation. 3. Add new error messages and validation options, including treating schema header validation errors as warnings. Schema Header Validation Includes: 1. Validate presence of Manifest Schema Header. 2. Ensure Schema Header format correctness. 3. Verify Schema Header URL Pattern accuracy. 4. Confirm Schema Header Manifest Type matches ManifestType Property. 5. Check Schema Header Manifest Version matches ManifestVersion Property. **WinGet CLI Validation Command Changes:** - Treat schema header validation errors as warnings for 'winget validate --manifest <<path>>' command - Introduced a new option `SchemaHeaderValidationAsWarning` to the `validateOption` object and set it to `true`. - This change treats schema header validation issues as warnings instead of errors, making the validation process more lenient. [NOTE:] 1. SchemaHeaderValidation errors should be treated as warnings for the winget CLI validate command. 2. SchemaHeaderValidation errors should be treated as errors for wingetsvc community manifests. **[Test Coverage:]** - Added new test data files to validate various schema header errors in YAML manifests, including - invalid headers, - type mismatches, - version mismatches, - missing headers, and - URL pattern mismatches. - Added test coverage for WinGet Utils Interop binary and WinGet CLI Validated command **[How Validated:]** - Compiled the AppInstaller end-to-end solution incorporating the changes. - Performed the YAML manifest validation tests, WinGet Utils tests, and WinGet CLI Validate command tests, ensuring all tests passed. - Locally validated the winget CLI validation command with various schema header errors, ensuring they are treated as warnings and that no warnings are reported for a valid manifest. **[Test Results:]** YAML Manifest Validation Test Results:  WinGet Utils Interop Test Results:  Validate Command Test Results:  <!-- To check a checkbox place an "x" between the brackets. e.g: [x] --> - [x] I have signed the [Contributor License Agreement](https://cla.opensource.microsoft.com/microsoft/winget-pkgs). - [x] This pull request is related to an issue. ----- ###### Microsoft Reviewers: [Open in CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/microsoft/winget-cli/pull/5126) --------- Co-authored-by: Madhusudhan Gumbalapura Sudarshan <Madhusudhan.Sudarshan@microsoft.com> Diffstat:
32 files changed, 736 insertions(+), 28 deletions(-)
diff --git 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -80,5 +80,47 @@ namespace AppInstallerCLIE2ETests Assert.AreEqual(Constants.ErrorCode.ERROR_PATH_NOT_FOUND, result.ExitCode); Assert.True(result.StdOut.Contains("Path does not exist")); } + + /// <summary> + /// Test validate manifest with invalid schema and expect warnings. + /// </summary> + [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.")); + } + + /// <summary> + /// Test validate manifest with valid schema header. + /// </summary> + [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 @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="WinGetUtilManifest.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -80,6 +80,85 @@ namespace AppInstallerCLIE2ETests.WinGetUtil // Close manifest WinGetUtilWrapper.WinGetCloseManifest(manifestHandle); - } - } + } + + /// <summary> + /// Test validate manifest with schema header. + /// </summary> + /// <param name="createManifestOption">Create manifest options.</param> + [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); + } + + /// <summary> + /// Test validate manifest with schema header for failure scenarios. + /// </summary> + /// <param name="createManifestOption">Create manifest options.</param> + [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 @@ -1033,6 +1033,21 @@ <CopyFileToFolders Include="TestData\ContainsEscapeControlCode.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\ManifestV1_10-Bad-SchemaHeaderInvalid.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\ManifestV1_10-Bad-SchemaHeaderManifestTypeMismatch.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\ManifestV1_10-Bad-SchemaHeaderManifestVersionMismatch.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\ManifestV1_10-Bad-SchemaHeaderNotFound.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\ManifestV1_10-Bad-SchemaHeaderURLPatternMismatch.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> </ItemGroup> <ItemGroup> <ProjectReference Include="..\AppInstallerCLICore\AppInstallerCLICore.vcxproj"> @@ -1076,4 +1091,4 @@ <Error Condition="!Exists('$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.230706.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.230706.1\build\native\Microsoft.Windows.CppWinRT.props'))" /> <Error Condition="!Exists('$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.230706.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.230706.1\build\native\Microsoft.Windows.CppWinRT.targets'))" /> </Target> -</Project> +</Project>+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -1044,5 +1044,20 @@ <CopyFileToFolders Include="TestData\ContainsEscapeControlCode.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\ManifestV1_10-Bad-SchemaHeaderInvalid.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\ManifestV1_10-Bad-SchemaHeaderManifestTypeMismatch.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\ManifestV1_10-Bad-SchemaHeaderManifestVersionMismatch.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\ManifestV1_10-Bad-SchemaHeaderNotFound.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\ManifestV1_10-Bad-SchemaHeaderURLPatternMismatch.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> </ItemGroup> </Project> \ 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -24,7 +24,7 @@ namespace AppInstaller::Manifest::YamlParser }; // List of fields that use non string scalar types - const std::map<std::string_view, YamlScalarType> ManifestFieldTypes= + const std::map<std::string_view, YamlScalarType> ManifestFieldTypes = { { "InstallerSuccessCodes"sv, YamlScalarType::Int }, { "InstallerAbortsTerminal"sv, YamlScalarType::Bool }, @@ -98,6 +98,173 @@ namespace AppInstaller::Manifest::YamlParser return result; } + + std::vector<ValidationError> ParseSchemaHeaderString(const YamlManifestInfo& manifestInfo, const ValidationError::Level& errorLevel, std::string& schemaHeaderUrlString) + { + std::vector<ValidationError> 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<std::string>(); + } + } + 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\.(?<type>\w+)\.(?<version>[\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<ValidationError> ValidateSchemaHeaderType(const std::string& headerManifestType, const ManifestTypeEnum& expectedManifestType, const YamlManifestInfo& manifestInfo, ValidationError::Level errorLevel) + { + std::vector<ValidationError> 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<ValidationError> ValidateSchemaHeaderVersion(const std::string& headerManifestVersion, const ManifestVer& expectedManifestVersion, const YamlManifestInfo& manifestInfo, ValidationError::Level errorLevel) + { + std::vector<ValidationError> 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<ValidationError> ValidateSchemaHeaderUrl(const YamlManifestInfo& manifestInfo, const ManifestVer& manifestVersion, const ValidationError::Level& errorLevel) + { + std::vector<ValidationError> 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<ValidationError> ValidateYamlManifestSchemaHeader(const YamlManifestInfo& manifestInfo, const ManifestVer& manifestVersion, ValidationError::Level errorLevel) + { + std::vector<ValidationError> 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<ValidationError> ValidateYamlManifestsSchemaHeader(const std::vector<YamlManifestInfo>& manifestList, const ManifestVer& manifestVersion, bool treatErrorAsWarning) + { + std::vector<ValidationError> 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 @@ -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 @@ -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 @@ -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 @@ -18,4 +18,10 @@ namespace AppInstaller::Manifest::YamlParser std::vector<ValidationError> ValidateAgainstSchema( const std::vector<YamlManifestInfo>& manifestList, const ManifestVer& manifestVersion); -}- \ No newline at end of file + + // Validate the schema header of a list of manifests + std::vector<ValidationError> ValidateYamlManifestsSchemaHeader( + const std::vector<YamlManifestInfo>& manifestList, + const ManifestVer& manifestVersion, + bool treatErrorAsWarning = true); +} diff --git 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<ValidationError> ValidateManifest(const Manifest& manifest, bool fullValidation = true); std::vector<ValidationError> ValidateManifestLocalization(const ManifestLocalization& localization, bool treatErrorAsWarning = false); std::vector<ValidationError> 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 @@ -5,7 +5,6 @@ #include <winget/Manifest.h> #include <winget/Yaml.h> #include <AppInstallerSHA256.h> - #include <filesystem> 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<YamlManifestInfo>& 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 @@ -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 @@ -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<std::string_view>(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<Wrapper::Document>(true)) { diff --git 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();