diff --git a/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj b/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj index 085f7cf63b..ea5e0bd22a 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj +++ b/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj @@ -35,11 +35,7 @@ - - - + diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/JsonSchemaReferenceResolutionException.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/JsonSchemaReferenceResolutionException.cs new file mode 100644 index 0000000000..ba664ccef1 --- /dev/null +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/JsonSchemaReferenceResolutionException.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; + +namespace Microsoft.PowerShell.Commands; + +/// +/// Thrown during evaluation of when an attempt +/// to resolve a $ref or $dynamicRef fails. +/// +internal class JsonSchemaReferenceResolutionException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// The exception that is the cause of the current exception, or a null reference + /// (Nothing in Visual Basic) if no inner exception is specified. + /// + public JsonSchemaReferenceResolutionException(Exception innerException) + : base(message: null, innerException) + { + } +} diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/TestJsonCommand.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/TestJsonCommand.cs index fc416d9772..4a6e11a963 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/TestJsonCommand.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/TestJsonCommand.cs @@ -5,11 +5,11 @@ using System; using System.Globalization; using System.IO; using System.Management.Automation; -using System.Reflection; -using System.Runtime.ExceptionServices; +using System.Net.Http; using System.Security; -using Newtonsoft.Json.Linq; -using NJsonSchema; +using System.Text.Json; +using System.Text.Json.Nodes; +using Json.Schema; namespace Microsoft.PowerShell.Commands { @@ -104,36 +104,50 @@ namespace Microsoft.PowerShell.Commands private bool _isLiteralPath = false; private JsonSchema _jschema; - /// - /// Process all exceptions in the AggregateException. - /// Unwrap TargetInvocationException if any and - /// rethrow inner exception without losing the stack trace. - /// - /// AggregateException to be unwrapped. - /// Return value is unreachable since we always rethrow. - private static bool UnwrapException(Exception e) - { - if (e.InnerException != null && e is TargetInvocationException) - { - ExceptionDispatchInfo.Capture(e.InnerException).Throw(); - } - else - { - ExceptionDispatchInfo.Capture(e).Throw(); - } - - return true; - } - #endregion - #region Protected Members - /// /// Prepare a JSON schema. /// protected override void BeginProcessing() { + // By default, a JSON Schema implementation isn't supposed to automatically fetch content. + // Instead JsonSchema.Net has been set up with a registry so that users can pre-register + // any schemas they may need to resolve. + // However, pre-registering schemas doesn't make sense in the context of a Powershell command, + // and automatically fetching referenced URIs is likely the preferred behavior. To do that, + // this property must be set with a method to retrieve and deserialize the content. + // For more information, see https://json-everything.net/json-schema#automatic-resolution + SchemaRegistry.Global.Fetch = static uri => + { + try + { + string text; + switch (uri.Scheme) + { + case "http": + case "https": + { + using var client = new HttpClient(); + text = client.GetStringAsync(uri).Result; + break; + } + case "file": + var filename = Uri.UnescapeDataString(uri.AbsolutePath); + text = File.ReadAllText(filename); + break; + default: + throw new FormatException(string.Format(TestJsonCmdletStrings.InvalidUriScheme, uri.Scheme)); + } + + return JsonSerializer.Deserialize(text); + } + catch (Exception e) + { + throw new JsonSchemaReferenceResolutionException(e); + } + }; + string resolvedpath = string.Empty; try @@ -142,13 +156,12 @@ namespace Microsoft.PowerShell.Commands { try { - _jschema = JsonSchema.FromJsonAsync(Schema).Result; + _jschema = JsonSchema.FromText(Schema); } - catch (AggregateException ae) + catch (JsonException e) { - // Even if only one exception is thrown, it is still wrapped in an AggregateException exception - // https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/exception-handling-task-parallel-library - ae.Handle(UnwrapException); + Exception exception = new(TestJsonCmdletStrings.InvalidJsonSchema, e); + WriteError(new ErrorRecord(exception, "InvalidJsonSchema", ErrorCategory.InvalidData, Schema)); } } else if (SchemaFile != null) @@ -156,11 +169,12 @@ namespace Microsoft.PowerShell.Commands try { resolvedpath = Context.SessionState.Path.GetUnresolvedProviderPathFromPSPath(SchemaFile); - _jschema = JsonSchema.FromFileAsync(resolvedpath).Result; + _jschema = JsonSchema.FromFile(resolvedpath); } - catch (AggregateException ae) + catch (JsonException e) { - ae.Handle(UnwrapException); + Exception exception = new(TestJsonCmdletStrings.InvalidJsonSchema, e); + WriteError(new ErrorRecord(exception, "InvalidJsonSchema", ErrorCategory.InvalidData, SchemaFile)); } } } @@ -194,6 +208,7 @@ namespace Microsoft.PowerShell.Commands protected override void ProcessRecord() { bool result = true; + string jsonToParse = string.Empty; if (Json != null) @@ -217,45 +232,57 @@ namespace Microsoft.PowerShell.Commands jsonToParse = File.ReadAllText(resolvedPath); } - if (string.IsNullOrWhiteSpace(jsonToParse)) - { - WriteObject(false); - return; - } - try { - var parsedJson = JToken.Parse(jsonToParse); + + var parsedJson = JsonNode.Parse(jsonToParse); if (_jschema != null) { - var errorMessages = _jschema.Validate(parsedJson); - if (errorMessages != null && errorMessages.Count != 0) - { - result = false; - - Exception exception = new(TestJsonCmdletStrings.InvalidJsonAgainstSchema); - - foreach (var message in errorMessages) + var validationResults = _jschema.Validate(parsedJson, new ValidationOptions { OutputFormat = OutputFormat.Basic }); + result = validationResults.IsValid; + if (!result) { - ErrorRecord errorRecord = new(exception, "InvalidJsonAgainstSchema", ErrorCategory.InvalidData, null); - errorRecord.ErrorDetails = new ErrorDetails(message.ToString()); - WriteError(errorRecord); + if (validationResults.Message != null) + { + Exception exception = new(string.Format(TestJsonCmdletStrings.InvalidJsonAgainstSchemaDetailed, validationResults.Message, validationResults.InstanceLocation)); + ErrorRecord errorRecord = new(exception, "InvalidJsonAgainstSchemaDetailed", ErrorCategory.InvalidData, null); + WriteError(errorRecord); + } + + if (validationResults.HasNestedResults) + { + foreach (var nestedResult in validationResults.NestedResults) + { + if (nestedResult.Message == null) + { + continue; + } + + Exception exception = new(string.Format(TestJsonCmdletStrings.InvalidJsonAgainstSchemaDetailed, nestedResult.Message, nestedResult.InstanceLocation)); + ErrorRecord errorRecord = new(exception, "InvalidJsonAgainstSchemaDetailed", ErrorCategory.InvalidData, null); + WriteError(errorRecord); + } + } } - } } } + catch (JsonSchemaReferenceResolutionException jsonExc) + { + result = false; + + Exception exception = new(TestJsonCmdletStrings.InvalidJsonSchema, jsonExc); + WriteError(new ErrorRecord(exception, "InvalidJsonSchema", ErrorCategory.InvalidData, _jschema)); + } catch (Exception exc) { result = false; Exception exception = new(TestJsonCmdletStrings.InvalidJson, exc); - WriteError(new ErrorRecord(exception, "InvalidJson", ErrorCategory.InvalidData, jsonToParse)); + WriteError(new ErrorRecord(exception, "InvalidJson", ErrorCategory.InvalidData, Json)); } WriteObject(result); } - - #endregion } } diff --git a/src/Microsoft.PowerShell.Commands.Utility/resources/TestJsonCmdletStrings.resx b/src/Microsoft.PowerShell.Commands.Utility/resources/TestJsonCmdletStrings.resx index ab105e47fd..eed85d28b3 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/resources/TestJsonCmdletStrings.resx +++ b/src/Microsoft.PowerShell.Commands.Utility/resources/TestJsonCmdletStrings.resx @@ -123,10 +123,13 @@ Cannot parse the JSON. - - The JSON is not valid with the schema. + + The JSON is not valid with the schema: {0} at {1} Can not open JSON schema file: {0} + + URI scheme '{0}' is not supported. Only HTTP(S) and local file system URIs are allowed. + diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Test-Json.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Test-Json.Tests.ps1 index 9e949e0215..a5aa52e029 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Test-Json.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Test-Json.Tests.ps1 @@ -9,62 +9,62 @@ Describe "Test-Json" -Tags "CI" { $missingSchemaJsonPath = Join-Path -Path $assetsPath -ChildPath no_such_file.json $missingJsonPath = Join-Path -Path $assetsPath -ChildPath no_such_file.json - $validSchemaJson = @" + $validSchemaJson = @' { - 'description': 'A person', - 'type': 'object', - 'properties': { - 'name': {'type': 'string'}, - 'hobbies': { - 'type': 'array', - 'items': {'type': 'string'} + "description": "A person", + "type": "object", + "properties": { + "name": {"type": "string"}, + "hobbies": { + "type": "array", + "items": {"type": "string"} } } } -"@ +'@ - $invalidSchemaJson = @" + $invalidSchemaJson = @' { - 'description', - 'type': 'object', - 'properties': { - 'name': {'type': 'string'}, - 'hobbies': { - 'type': 'array', - 'items': {'type': 'string'} + "description", + "type": "object", + "properties": { + "name": {"type": "string"}, + "hobbies": { + "type": "array", + "items": {"type": "string"} } } } -"@ +'@ - $validJson = @" + $validJson = @' { - 'name': 'James', - 'hobbies': ['.NET', 'Blogging', 'Reading', 'Xbox', 'LOLCATS'] + "name": "James", + "hobbies": [".NET", "Blogging", "Reading", "Xbox", "LOLCATS"] } -"@ +'@ - $invalidTypeInJson = @" + $invalidTypeInJson = @' { - 'name': 123, - 'hobbies': ['.NET', 'Blogging', 'Reading', 'Xbox', 'LOLCATS'] + "name": 123, + "hobbies": [".NET", "Blogging", "Reading", "Xbox", "LOLCATS"] } -"@ +'@ - $invalidTypeInJson2 = @" + $invalidTypeInJson2 = @' { - 'name': 123, - 'hobbies': [456, 'Blogging', 'Reading', 'Xbox', 'LOLCATS'] + "name": 123, + "hobbies": [456, "Blogging", "Reading", "Xbox", "LOLCATS"] } -"@ +'@ - $invalidNodeInJson = @" + $invalidNodeInJson = @' { - 'name': 'James', - 'hobbies': ['.NET', 'Blogging', 'Reading', 'Xbox', 'LOLCATS'] + "name": "James", + "hobbies": [".NET", "Blogging", "Reading", "Xbox", "LOLCATS"] errorNode } -"@ +'@ $validJsonPath = Join-Path -Path $TestDrive -ChildPath 'validJson.json' $validLiteralJsonPath = Join-Path -Path $TestDrive -ChildPath "[valid]Json.json" @@ -91,12 +91,10 @@ Describe "Test-Json" -Tags "CI" { It "Json is valid" { Test-Json -Json $validJson | Should -BeTrue - ($validJson | Test-Json) | Should -BeTrue } It "Json is valid against a valid schema from string" { Test-Json -Json $validJson -Schema $validSchemaJson | Should -BeTrue - ($validJson | Test-Json -Schema $validSchemaJson) | Should -BeTrue } It "Json is valid against a valid schema from file" { @@ -201,7 +199,7 @@ Describe "Test-Json" -Tags "CI" { } It "Test-Json write an error on invalid () Json against a valid schema from string" -TestCases @( - @{ name = "type"; json = $invalidTypeInJson; errorId = "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" } + @{ name = "type"; json = $invalidTypeInJson; errorId = "InvalidJsonAgainstSchemaDetailed,Microsoft.PowerShell.Commands.TestJsonCommand" } @{ name = "node"; json = $invalidNodeInJson; errorId = "InvalidJson,Microsoft.PowerShell.Commands.TestJsonCommand" } ) { param ($json, $errorId) @@ -213,7 +211,7 @@ Describe "Test-Json" -Tags "CI" { } It "Test-Json write an error on invalid () Json against a valid schema from file" -TestCases @( - @{ name = "type"; json = $invalidTypeInJson; errorId = "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" } + @{ name = "type"; json = $invalidTypeInJson; errorId = "InvalidJsonAgainstSchemaDetailed,Microsoft.PowerShell.Commands.TestJsonCommand" } @{ name = "node"; json = $invalidNodeInJson; errorId = "InvalidJson,Microsoft.PowerShell.Commands.TestJsonCommand" } ) { param ($json, $errorId) @@ -225,7 +223,7 @@ Describe "Test-Json" -Tags "CI" { } It "Test-Json write an error on invalid () Json file against a valid schema from string" -TestCases @( - @{ name = "type"; json = $invalidTypeInJsonPath; errorId = "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" } + @{ name = "type"; json = $invalidTypeInJsonPath; errorId = "InvalidJsonAgainstSchemaDetailed,Microsoft.PowerShell.Commands.TestJsonCommand" } @{ name = "node"; json = $invalidNodeInJsonPath; errorId = "InvalidJson,Microsoft.PowerShell.Commands.TestJsonCommand" } ) { param ($json, $errorId) @@ -237,7 +235,7 @@ Describe "Test-Json" -Tags "CI" { } It "Test-Json write an error on invalid () Json file against a valid schema from file" -TestCases @( - @{ name = "type"; json = $invalidTypeInJsonPath; errorId = "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" } + @{ name = "type"; json = $invalidTypeInJsonPath; errorId = "InvalidJsonAgainstSchemaDetailed,Microsoft.PowerShell.Commands.TestJsonCommand" } @{ name = "node"; json = $invalidNodeInJsonPath; errorId = "InvalidJson,Microsoft.PowerShell.Commands.TestJsonCommand" } ) { param ($json, $errorId) @@ -254,8 +252,8 @@ Describe "Test-Json" -Tags "CI" { # '$invalidTypeInJson2' contains two errors in property types. $errorVar.Count | Should -Be 2 - $errorVar[0].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" - $errorVar[1].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" + $errorVar[0].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchemaDetailed,Microsoft.PowerShell.Commands.TestJsonCommand" + $errorVar[1].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchemaDetailed,Microsoft.PowerShell.Commands.TestJsonCommand" } It "Test-Json return all errors when check invalid Json against a valid schema from file" { @@ -264,8 +262,8 @@ Describe "Test-Json" -Tags "CI" { # '$invalidTypeInJson2' contains two errors in property types. $errorVar.Count | Should -Be 2 - $errorVar[0].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" - $errorVar[1].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" + $errorVar[0].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchemaDetailed,Microsoft.PowerShell.Commands.TestJsonCommand" + $errorVar[1].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchemaDetailed,Microsoft.PowerShell.Commands.TestJsonCommand" } It "Test-Json return all errors when check invalid Json file against a valid schema from string" { @@ -274,8 +272,8 @@ Describe "Test-Json" -Tags "CI" { # '$invalidTypeInJson2Path' contains two errors in property types. $errorVar.Count | Should -Be 2 - $errorVar[0].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" - $errorVar[1].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" + $errorVar[0].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchemaDetailed,Microsoft.PowerShell.Commands.TestJsonCommand" + $errorVar[1].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchemaDetailed,Microsoft.PowerShell.Commands.TestJsonCommand" } It "Test-Json return all errors when check invalid Json file against a valid schema from file" { @@ -284,8 +282,8 @@ Describe "Test-Json" -Tags "CI" { # '$invalidTypeInJson2Path' contains two errors in property types. $errorVar.Count | Should -Be 2 - $errorVar[0].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" - $errorVar[1].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchema,Microsoft.PowerShell.Commands.TestJsonCommand" + $errorVar[0].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchemaDetailed,Microsoft.PowerShell.Commands.TestJsonCommand" + $errorVar[1].FullyQualifiedErrorId | Should -BeExactly "InvalidJsonAgainstSchemaDetailed,Microsoft.PowerShell.Commands.TestJsonCommand" } It 'Test-Json recognizes non-object types: ' -TestCases @( @@ -307,7 +305,7 @@ Describe "Test-Json" -Tags "CI" { # Exactly one type should match $types = 'string', 'number', 'boolean', 'null', 'array', 'object' $types | Where-Object { - $schema = "{ 'type': '$_' }" + $schema = "{ `"type`": `"$_`" }" Test-Json -Json $value -Schema $schema -ErrorAction SilentlyContinue } | Should -Be $expected } diff --git a/tools/packaging/boms/linux.json b/tools/packaging/boms/linux.json index b115e72b55..f735ea5cf1 100644 --- a/tools/packaging/boms/linux.json +++ b/tools/packaging/boms/linux.json @@ -492,7 +492,7 @@ "FileType": "NonProduct" }, { - "Pattern": "NJsonSchema.dll", + "Pattern": "JsonSchema.Net.dll", "FileType": "NonProduct" }, { diff --git a/tools/packaging/boms/mac.json b/tools/packaging/boms/mac.json index 28593cdd14..29f2bb8532 100644 --- a/tools/packaging/boms/mac.json +++ b/tools/packaging/boms/mac.json @@ -372,7 +372,7 @@ "FileType": "NonProduct" }, { - "Pattern": "NJsonSchema.dll", + "Pattern": "JsonSchema.Net.dll", "FileType": "NonProduct" }, { diff --git a/tools/packaging/boms/windows.json b/tools/packaging/boms/windows.json index dc4329e475..747fa838b5 100644 --- a/tools/packaging/boms/windows.json +++ b/tools/packaging/boms/windows.json @@ -555,6 +555,22 @@ "Pattern": "ja/WindowsFormsIntegration.resources.dll", "FileType": "NonProduct" }, + { + "Pattern": "JetBrains.Annotations.dll", + "FileType": "NonProduct" + }, + { + "Pattern": "Json.More.dll", + "FileType": "NonProduct" + }, + { + "Pattern": "JsonPointer.Net.dll", + "FileType": "NonProduct" + }, + { + "Pattern": "JsonSchema.Net.dll", + "FileType": "NonProduct" + }, { "Pattern": "ko/Microsoft.CodeAnalysis.CSharp.resources.dll", "FileType": "NonProduct" @@ -871,10 +887,6 @@ "Pattern": "msquic.dll", "FileType": "NonProduct" }, - { - "Pattern": "Namotion.Reflection.dll", - "FileType": "NonProduct" - }, { "Pattern": "netstandard.dll", "FileType": "NonProduct" @@ -883,10 +895,6 @@ "Pattern": "Newtonsoft.Json.dll", "FileType": "NonProduct" }, - { - "Pattern": "NJsonSchema.dll", - "FileType": "NonProduct" - }, { "Pattern": "PenImc_cor3.dll", "FileType": "NonProduct"