From ca06856229d8dec1c59fa7e913050fd2258b0a7c Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 9 Jul 2024 13:02:28 +0100 Subject: [PATCH 1/4] Add JsonMarshal.TryGetRawValue --- .../System.Text.Json/ref/System.Text.Json.cs | 7 + .../src/System.Text.Json.csproj | 1 + .../Runtime/InteropServices/JsonMarshal.cs | 29 ++++ .../JsonElementParseTests.cs | 129 ++++++++++++++++++ 4 files changed, 166 insertions(+) create mode 100644 src/libraries/System.Text.Json/src/System/Runtime/InteropServices/JsonMarshal.cs diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index 99d7be45b10773..4486418c19b0cc 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -4,6 +4,13 @@ // Changes to this file must follow the https://aka.ms/api-review process. // ------------------------------------------------------------------------------ +namespace System.Runtime.InteropServices +{ + public static partial class JsonMarshal + { + public static bool TryGetRawValue(System.Text.Json.JsonElement element, out System.ReadOnlySpan rawValue) { throw null; } + } +} namespace System.Text.Json { public enum JsonCommentHandling : byte diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index a38e38ceb6a93c..c01f6ad2342f84 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -50,6 +50,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET + diff --git a/src/libraries/System.Text.Json/src/System/Runtime/InteropServices/JsonMarshal.cs b/src/libraries/System.Text.Json/src/System/Runtime/InteropServices/JsonMarshal.cs new file mode 100644 index 00000000000000..cf8af2acd3eec3 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Runtime/InteropServices/JsonMarshal.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; + +namespace System.Runtime.InteropServices +{ + /// + /// An unsafe class that provides a set of methods to access the underlying data representations of JSON types. + /// + public static class JsonMarshal + { + /// + /// Gets a view over the raw JSON data of the given . + /// + /// The JSON element from which to extract the span. + /// The span containing the raw JSON data of . + /// + /// if is backed by a UTF-8 encoded , + /// otherwise. + /// + /// The underlying has been disposed. + public static bool TryGetRawValue(JsonElement element, out ReadOnlySpan rawValue) + { + rawValue = element.GetRawValue().Span; + return true; + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonElementParseTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonElementParseTests.cs index 15a399c01fbada..5c4651f202361f 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonElementParseTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonElementParseTests.cs @@ -3,6 +3,7 @@ using Xunit; using System.Collections.Generic; +using System.Runtime.InteropServices; namespace System.Text.Json.Tests { @@ -225,5 +226,133 @@ public static void TryParseValueInvalidDataFail(string json) Assert.Equal(0, reader.BytesConsumed); } + + [Theory] + [InlineData("null")] + [InlineData("\r\n null ")] + [InlineData("false")] + [InlineData("true ")] + [InlineData(" 42.0 ")] + [InlineData(" \"str\" \r\n")] + [InlineData(" [ ]")] + [InlineData(" [null, true, 42.0, \"str\", [], {}, ]")] + [InlineData(" { } ")] + [InlineData(""" + + { + /* I am a comment */ + "key1" : 1, + "key2" : null, + "key3" : true, + } + + """)] + public static void JsonMarshal_TryGetRawValue_RootValue_ReturnsFullValue(string json) + { + JsonDocumentOptions options = new JsonDocumentOptions { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip }; + using JsonDocument jDoc = JsonDocument.Parse(json, options); + JsonElement element = jDoc.RootElement; + + Assert.True(JsonMarshal.TryGetRawValue(element, out ReadOnlySpan rawValue)); + Assert.Equal(json.Trim(), Encoding.UTF8.GetString(rawValue.ToArray())); + } + + [Fact] + public static void JsonMarshal_TryGetRawValue_NestedValues_ReturnsExpectedValue() + { + const string json = """ + { + "date": "2021-06-01T00:00:00Z", + "temperatureC": 25, + "summary": "Hot", + + /* The next property is a JSON object */ + + "nested": { + /* This is a nested JSON object */ + + "nestedDate": "2021-06-01T00:00:00Z", + "nestedTemperatureC": 25, + "nestedSummary": "Hot" + }, + + /* The next property is a JSON array */ + + "nestedArray": [ + /* This is a JSON array */ + { + "nestedDate": "2021-06-01T00:00:00Z", + "nestedTemperatureC": 25, + "nestedSummary": "Hot" + }, + ] + } + """; + + JsonDocumentOptions options = new JsonDocumentOptions { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip }; + using JsonDocument jDoc = JsonDocument.Parse(json, options); + JsonElement element = jDoc.RootElement; + + AssertGetRawValue(json, element); + AssertGetRawValue("\"2021-06-01T00:00:00Z\"", element.GetProperty("date")); + AssertGetRawValue("25", element.GetProperty("temperatureC")); + AssertGetRawValue("\"Hot\"", element.GetProperty("summary")); + + JsonElement nested = element.GetProperty("nested"); + AssertGetRawValue(""" + { + /* This is a nested JSON object */ + + "nestedDate": "2021-06-01T00:00:00Z", + "nestedTemperatureC": 25, + "nestedSummary": "Hot" + } + """, nested); + + AssertGetRawValue("\"2021-06-01T00:00:00Z\"", nested.GetProperty("nestedDate")); + AssertGetRawValue("25", nested.GetProperty("nestedTemperatureC")); + AssertGetRawValue("\"Hot\"", nested.GetProperty("nestedSummary")); + + JsonElement nestedArray = element.GetProperty("nestedArray"); + AssertGetRawValue(""" + [ + /* This is a JSON array */ + { + "nestedDate": "2021-06-01T00:00:00Z", + "nestedTemperatureC": 25, + "nestedSummary": "Hot" + }, + ] + """, nestedArray); + + JsonElement nestedArrayElement = nestedArray[0]; + AssertGetRawValue(""" + { + "nestedDate": "2021-06-01T00:00:00Z", + "nestedTemperatureC": 25, + "nestedSummary": "Hot" + } + """, nestedArrayElement); + + AssertGetRawValue("\"2021-06-01T00:00:00Z\"", nestedArrayElement.GetProperty("nestedDate")); + AssertGetRawValue("25", nestedArrayElement.GetProperty("nestedTemperatureC")); + AssertGetRawValue("\"Hot\"", nestedArrayElement.GetProperty("nestedSummary")); + + static void AssertGetRawValue(string expectedJson, JsonElement element) + { + Assert.True(JsonMarshal.TryGetRawValue(element, out ReadOnlySpan rawValue)); + Assert.Equal(expectedJson.Trim(), Encoding.UTF8.GetString(rawValue.ToArray())); + } + } + + [Fact] + public static void JsonMarshal_TryGetRawValue_DisposedDocument_ThrowsObjectDisposedException() + { + JsonDocument jDoc = JsonDocument.Parse("{}"); + JsonElement element = jDoc.RootElement; + jDoc.Dispose(); + + Assert.Throws(() => JsonMarshal.TryGetRawValue(element, out ReadOnlySpan rawValue)); + } } } From e0becd9e9685952466ae39004e64c6b736cfc28c Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 9 Jul 2024 13:06:49 +0100 Subject: [PATCH 2/4] Add escaping test. --- .../tests/System.Text.Json.Tests/JsonElementParseTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonElementParseTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonElementParseTests.cs index 5c4651f202361f..bb1365036e5ba7 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonElementParseTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonElementParseTests.cs @@ -234,6 +234,7 @@ public static void TryParseValueInvalidDataFail(string json) [InlineData("true ")] [InlineData(" 42.0 ")] [InlineData(" \"str\" \r\n")] + [InlineData(" \"string with escaping: \\u0041\\u0042\\u0043\" \r\n")] [InlineData(" [ ]")] [InlineData(" [null, true, 42.0, \"str\", [], {}, ]")] [InlineData(" { } ")] From 1d6796eaf6afc0f3f3d7f443873074d6433a9d7f Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 9 Jul 2024 20:37:28 +0100 Subject: [PATCH 3/4] Apply API review suggestions. --- .../System.Text.Json/ref/System.Text.Json.cs | 2 +- .../src/System/Runtime/InteropServices/JsonMarshal.cs | 11 +++-------- .../System.Text.Json.Tests/JsonElementParseTests.cs | 6 +++--- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index 4486418c19b0cc..e4ab3457e1ddb4 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -8,7 +8,7 @@ namespace System.Runtime.InteropServices { public static partial class JsonMarshal { - public static bool TryGetRawValue(System.Text.Json.JsonElement element, out System.ReadOnlySpan rawValue) { throw null; } + public static System.ReadOnlySpan GetRawUtf8Value(System.Text.Json.JsonElement element) { throw null; } } } namespace System.Text.Json diff --git a/src/libraries/System.Text.Json/src/System/Runtime/InteropServices/JsonMarshal.cs b/src/libraries/System.Text.Json/src/System/Runtime/InteropServices/JsonMarshal.cs index cf8af2acd3eec3..ec1ad33a1abf29 100644 --- a/src/libraries/System.Text.Json/src/System/Runtime/InteropServices/JsonMarshal.cs +++ b/src/libraries/System.Text.Json/src/System/Runtime/InteropServices/JsonMarshal.cs @@ -14,16 +14,11 @@ public static class JsonMarshal /// Gets a view over the raw JSON data of the given . /// /// The JSON element from which to extract the span. - /// The span containing the raw JSON data of . - /// - /// if is backed by a UTF-8 encoded , - /// otherwise. - /// + /// The span containing the raw JSON data of. /// The underlying has been disposed. - public static bool TryGetRawValue(JsonElement element, out ReadOnlySpan rawValue) + public static ReadOnlySpan GetRawUtf8Value(JsonElement element) { - rawValue = element.GetRawValue().Span; - return true; + return element.GetRawValue().Span; } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonElementParseTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonElementParseTests.cs index bb1365036e5ba7..e0428f5309173f 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonElementParseTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonElementParseTests.cs @@ -254,7 +254,7 @@ public static void JsonMarshal_TryGetRawValue_RootValue_ReturnsFullValue(string using JsonDocument jDoc = JsonDocument.Parse(json, options); JsonElement element = jDoc.RootElement; - Assert.True(JsonMarshal.TryGetRawValue(element, out ReadOnlySpan rawValue)); + ReadOnlySpan rawValue = JsonMarshal.GetRawUtf8Value(element); Assert.Equal(json.Trim(), Encoding.UTF8.GetString(rawValue.ToArray())); } @@ -341,7 +341,7 @@ public static void JsonMarshal_TryGetRawValue_NestedValues_ReturnsExpectedValue( static void AssertGetRawValue(string expectedJson, JsonElement element) { - Assert.True(JsonMarshal.TryGetRawValue(element, out ReadOnlySpan rawValue)); + ReadOnlySpan rawValue = JsonMarshal.GetRawUtf8Value(element); Assert.Equal(expectedJson.Trim(), Encoding.UTF8.GetString(rawValue.ToArray())); } } @@ -353,7 +353,7 @@ public static void JsonMarshal_TryGetRawValue_DisposedDocument_ThrowsObjectDispo JsonElement element = jDoc.RootElement; jDoc.Dispose(); - Assert.Throws(() => JsonMarshal.TryGetRawValue(element, out ReadOnlySpan rawValue)); + Assert.Throws(() => JsonMarshal.GetRawUtf8Value(element)); } } } From 080540c805be249ba711a81f8607f95f1dda543e Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 10 Jul 2024 10:03:45 +0100 Subject: [PATCH 4/4] Address feedback --- .../src/System/Runtime/InteropServices/JsonMarshal.cs | 6 ++++++ .../tests/System.Text.Json.Tests/JsonElementParseTests.cs | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Runtime/InteropServices/JsonMarshal.cs b/src/libraries/System.Text.Json/src/System/Runtime/InteropServices/JsonMarshal.cs index ec1ad33a1abf29..5c8c43805c91a4 100644 --- a/src/libraries/System.Text.Json/src/System/Runtime/InteropServices/JsonMarshal.cs +++ b/src/libraries/System.Text.Json/src/System/Runtime/InteropServices/JsonMarshal.cs @@ -16,6 +16,12 @@ public static class JsonMarshal /// The JSON element from which to extract the span. /// The span containing the raw JSON data of. /// The underlying has been disposed. + /// + /// While the method itself does check for disposal of the underlying , + /// it is possible that it could be disposed after the method returns, which would result in + /// the span pointing to a buffer that has been returned to the shared pool. Callers should take + /// extra care to make sure that such a scenario isn't possible to avoid potential data corruption. + /// public static ReadOnlySpan GetRawUtf8Value(JsonElement element) { return element.GetRawValue().Span; diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonElementParseTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonElementParseTests.cs index e0428f5309173f..017b1422ab7d74 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonElementParseTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonElementParseTests.cs @@ -248,7 +248,7 @@ public static void TryParseValueInvalidDataFail(string json) } """)] - public static void JsonMarshal_TryGetRawValue_RootValue_ReturnsFullValue(string json) + public static void JsonMarshal_GetRawUtf8Value_RootValue_ReturnsFullValue(string json) { JsonDocumentOptions options = new JsonDocumentOptions { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip }; using JsonDocument jDoc = JsonDocument.Parse(json, options); @@ -259,7 +259,7 @@ public static void JsonMarshal_TryGetRawValue_RootValue_ReturnsFullValue(string } [Fact] - public static void JsonMarshal_TryGetRawValue_NestedValues_ReturnsExpectedValue() + public static void JsonMarshal_GetRawUtf8Value_NestedValues_ReturnsExpectedValue() { const string json = """ { @@ -347,7 +347,7 @@ static void AssertGetRawValue(string expectedJson, JsonElement element) } [Fact] - public static void JsonMarshal_TryGetRawValue_DisposedDocument_ThrowsObjectDisposedException() + public static void JsonMarshal_GetRawUtf8Value_DisposedDocument_ThrowsObjectDisposedException() { JsonDocument jDoc = JsonDocument.Parse("{}"); JsonElement element = jDoc.RootElement;