diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs index bb27d8b1a563b..c1568c8f2233f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs @@ -67,6 +67,8 @@ internal static class JsonConstants // When transcoding from UTF8 -> UTF16, the byte count threshold where we rent from the array pool before performing a normal alloc. public const long ArrayPoolMaxSizeBeforeUsingNormalAlloc = 1024 * 1024; + public const int MaxRawValueLength = int.MaxValue / MaxExpansionFactorWhileTranscoding; + public const int MaxEscapedTokenSize = 1_000_000_000; // Max size for already escaped value. public const int MaxUnescapedTokenSize = MaxEscapedTokenSize / MaxExpansionFactorWhileEscaping; // 166_666_666 bytes public const int MaxBase64ValueTokenSize = (MaxEscapedTokenSize >> 2) * 3 / MaxExpansionFactorWhileEscaping; // 125_000_000 bytes diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Raw.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Raw.cs index 3ea7e1f26c8aa..051f9e2d16965 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Raw.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Raw.cs @@ -8,26 +8,93 @@ namespace System.Text.Json public sealed partial class Utf8JsonWriter { /// - /// Writes the input as JSON content. + /// Writes the input as JSON content. It is expected that the input content is a single complete JSON value. /// /// The raw JSON content to write. - /// Whether to skip validation of the input JSON content. + /// Whether to validate if the input is an RFC 8259-compliant JSON payload. + /// Thrown if is . + /// Thrown if the length of the input is zero or greater than 715,827,882. + /// Thrown if is , and the input is not RFC 8259-compliant. + /// + /// When writing untrused JSON values, do not set to as this can result in invalid JSON + /// being written, and/or the overall payload being written to the writer instance being invalid. + /// + /// When using this method, the input content will be written to the writer destination as-is, unless validation fails. + /// + /// The value for the writer instance is honored when using this method. + /// + /// The and values for the writer instance are not applied when using this method. + /// public void WriteRawValue(string json, bool skipInputValidation = false) { + if (!_options.SkipValidation) + { + ValidateWritingValue(); + } + if (json == null) { throw new ArgumentNullException(nameof(json)); } - WriteRawValue(json.AsSpan(), skipInputValidation); + TranscodeAndWriteRawValue(json.AsSpan(), skipInputValidation); } /// - /// Writes the input as JSON content. + /// Writes the input as JSON content. It is expected that the input content is a single complete JSON value. /// /// The raw JSON content to write. - /// Whether to skip validation of the input JSON content. + /// Whether to validate if the input is an RFC 8259-compliant JSON payload. + /// Thrown if the length of the input is zero or greater than 715,827,882. + /// Thrown if is , and the input is not RFC 8259-compliant. + /// + /// When writing untrused JSON values, do not set to as this can result in invalid JSON + /// being written, and/or the overall payload being written to the writer instance being invalid. + /// + /// When using this method, the input content will be written to the writer destination as-is, unless validation fails. + /// + /// The value for the writer instance is honored when using this method. + /// + /// The and values for the writer instance are not applied when using this method. + /// public void WriteRawValue(ReadOnlySpan json, bool skipInputValidation = false) + { + if (!_options.SkipValidation) + { + ValidateWritingValue(); + } + + TranscodeAndWriteRawValue(json, skipInputValidation); + } + + /// + /// Writes the input as JSON content. It is expected that the input content is a single complete JSON value. + /// + /// The raw JSON content to write. + /// Whether to validate if the input is an RFC 8259-compliant JSON payload. + /// Thrown if the length of the input is zero or greater than 715,827,882. + /// Thrown if is , and the input is not RFC 8259-compliant. + /// + /// When writing untrused JSON values, do not set to as this can result in invalid JSON + /// being written, and/or the overall payload being written to the writer instance being invalid. + /// + /// When using this method, the input content will be written to the writer destination as-is, unless validation fails. + /// + /// The value for the writer instance is honored when using this method. + /// + /// The and values for the writer instance are not applied when using this method. + /// + public void WriteRawValue(ReadOnlySpan utf8Json, bool skipInputValidation = false) + { + if (!_options.SkipValidation) + { + ValidateWritingValue(); + } + + WriteRawValueInternal(utf8Json, skipInputValidation); + } + + private void TranscodeAndWriteRawValue(ReadOnlySpan json, bool skipInputValidation) { byte[]? tempArray = null; @@ -55,33 +122,40 @@ public void WriteRawValue(ReadOnlySpan json, bool skipInputValidation = fa } } - /// - /// Writes the input as JSON content. - /// - /// The raw JSON content to write. - /// Whether to skip validation of the input JSON content. - public void WriteRawValue(ReadOnlySpan utf8Json, bool skipInputValidation = false) + private void WriteRawValueInternal(ReadOnlySpan utf8Json, bool skipInputValidation) { - if (utf8Json.Length == 0) + int len = utf8Json.Length; + + if (len == 0) { ThrowHelper.ThrowArgumentException(SR.ExpectedJsonTokens); } - - if (!skipInputValidation) + else if (len > JsonConstants.MaxRawValueLength) { - Utf8JsonReader reader = new Utf8JsonReader(utf8Json); + ThrowHelper.ThrowArgumentException_ValueTooLarge(len); + } - try - { - while (reader.Read()); - } - catch (JsonReaderException ex) + if (skipInputValidation) + { + // Treat all unvalidated raw JSON value writes as string. If the payload is valid, this approach does + // not affect structural validation since a string token is equivalent to a complete object, array, + // or other complete JSON tokens when considering structural validation on subsequent writer calls. + // If the payload is not valid, then we make no guarantees about the structural validation of the final payload. + _tokenType = JsonTokenType.String; + } + else + { + // Utilize reader validation. + Utf8JsonReader reader = new(utf8Json); + while (reader.Read()) { - ThrowHelper.ThrowArgumentException(ex.Message); + _tokenType = reader.TokenType; } } - int maxRequired = utf8Json.Length + 1; // Optionally, 1 list separator + // TODO (https://github.com/dotnet/runtime/issues/29293): + // investigate writing this in chunks, rather than requesting one potentially long, contiguous buffer. + int maxRequired = len + 1; // Optionally, 1 list separator if (_memory.Length - BytesPending < maxRequired) { @@ -96,12 +170,10 @@ public void WriteRawValue(ReadOnlySpan utf8Json, bool skipInputValidation } utf8Json.CopyTo(output.Slice(BytesPending)); - BytesPending += utf8Json.Length; + BytesPending += len; - SetFlagToAddListSeparatorBeforeNextItem(); - // Treat all raw JSON value writes as string. - _tokenType = JsonTokenType.String; + SetFlagToAddListSeparatorBeforeNextItem(); } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj index d3ba50335a1d6..964f2c6d33a79 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj @@ -186,10 +186,10 @@ + - diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.WriteRaw.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.WriteRaw.cs index 219d49393416f..27a342e43a5ec 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.WriteRaw.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.WriteRaw.cs @@ -12,7 +12,6 @@ public partial class Utf8JsonWriterTests { private const string TestGuidAsStr = "eb97fadd-3ebf-4781-8722-f4773989160e"; private readonly static Guid s_guid = Guid.Parse(TestGuidAsStr); - private static byte[] s_guidAsJson = WrapInQuotes(TestGuidAsStr); private static byte[] s_oneAsJson = new byte[] { (byte)'1' }; @@ -67,7 +66,8 @@ public static IEnumerable GetRootLevelPrimitives() yield return new object[] { Encoding.UTF8.GetBytes(@"""Hello"""), validate }; validate = (data) => Assert.Equal(s_guid, JsonSerializer.Deserialize(data)); - yield return new object[] { s_guidAsJson, validate }; + byte[] guidAsJson = WrapInQuotes(Encoding.UTF8.GetBytes(TestGuidAsStr)); + yield return new object[] { guidAsJson, validate }; } public static IEnumerable GetArrays() @@ -105,13 +105,13 @@ public static IEnumerable GetArrays() yield return new object[] { json, validate }; } - private static byte[] WrapInQuotes(string json) + private static byte[] WrapInQuotes(ReadOnlySpan buffer) { - byte[] buffer = new byte[json.Length + 2]; - buffer[0] = (byte)'"'; - Encoding.UTF8.GetBytes(json).CopyTo(buffer, 1); - buffer[json.Length + 1] = (byte)'"'; - return buffer; + byte[] quotedBuffer = new byte[buffer.Length + 2]; + quotedBuffer[0] = (byte)'"'; + buffer.CopyTo(quotedBuffer.AsSpan().Slice(1)); + quotedBuffer[buffer.Length + 1] = (byte)'"'; + return quotedBuffer; } [Theory] @@ -169,6 +169,11 @@ public static void WriteRawObjectProperty(bool skipInputValidation, int numEleme [InlineData("[}")] [InlineData("xxx")] [InlineData("{hello:")] + [InlineData("\\u007Bhello:")] + [InlineData(@"{""hello:""""")] + [InlineData(" ")] + [InlineData("// This is a single line comment")] + [InlineData("/* This is a multi-\nline comment*/")] public static void WriteRawInvalidJson(string json) { RunTest(true); @@ -181,11 +186,52 @@ void RunTest(bool skipValidation) if (!skipValidation) { - Assert.Throws(() => writer.WriteRawValue(json)); + Assert.ThrowsAny(() => writer.WriteRawValue(json)); } else { writer.WriteRawValue(json, true); + writer.Flush(); + Assert.True(Encoding.UTF8.GetBytes(json).SequenceEqual(ms.ToArray())); + } + } + } + + [Fact] + public static void WriteRawNullOrEmptyTokenInvalid() + { + using MemoryStream ms = new(); + using Utf8JsonWriter writer = new(ms); + Assert.Throws(() => writer.WriteRawValue(json: default(string))); + Assert.Throws(() => writer.WriteRawValue(json: "")); + Assert.Throws(() => writer.WriteRawValue(json: default(ReadOnlySpan))); + Assert.Throws(() => writer.WriteRawValue(utf8Json: default)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public static void WriteRawHonorSkipValidation(bool skipValidation) + { + RunTest(true); + RunTest(false); + + void RunTest(bool skipInputValidation) + { + using MemoryStream ms = new(); + using Utf8JsonWriter writer = new(ms, new JsonWriterOptions { SkipValidation = skipValidation }); + + writer.WriteStartObject(); + + if (skipValidation) + { + writer.WriteRawValue(@"{}", skipInputValidation); + writer.Flush(); + Assert.True(ms.ToArray().SequenceEqual(new byte[] { (byte)'{', (byte)'{', (byte)'}' })); + } + else + { + Assert.Throws(() => writer.WriteRawValue(@"{}", skipInputValidation)); } } } @@ -195,12 +241,12 @@ void RunTest(bool skipValidation) /// problems on Linux due to the way deferred memory allocation works. On Linux, the allocation can /// succeed even if there is not enough memory but then the test may get killed by the OOM killer at the /// time the memory is accessed which triggers the full memory allocation. - /// Also see + /// Also see /// [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.OSX)] [ConditionalFact(nameof(IsX64))] [OuterLoop] - public void WriteLargeRawJsonToStreamWithoutFlushing() + public void WriteRawLargeJsonToStreamWithoutFlushing() { var largeArray = new char[150_000_000]; largeArray.AsSpan().Fill('a'); @@ -214,46 +260,110 @@ public void WriteLargeRawJsonToStreamWithoutFlushing() using (var writer = new Utf8JsonWriter(output)) { writer.WriteStartArray(); - writer.WriteRawValue(text1.EncodedUtf8Bytes); + writer.WriteRawValue(WrapInQuotes(text1.EncodedUtf8Bytes)); Assert.Equal(7_503, writer.BytesPending); for (int i = 0; i < 30_000; i++) { - writer.WriteRawValue(text2.EncodedUtf8Bytes); + writer.WriteRawValue(WrapInQuotes(text2.EncodedUtf8Bytes)); } Assert.Equal(150_097_503, writer.BytesPending); for (int i = 0; i < 13; i++) { - writer.WriteRawValue(text3.EncodedUtf8Bytes); + writer.WriteRawValue(WrapInQuotes(text3.EncodedUtf8Bytes)); } Assert.Equal(2_100_097_542, writer.BytesPending); // Next write forces a grow beyond max array length - Assert.Throws(() => writer.WriteRawValue(text3.EncodedUtf8Bytes)); + Assert.Throws(() => writer.WriteRawValue(WrapInQuotes(text3.EncodedUtf8Bytes))); Assert.Equal(2_100_097_542, writer.BytesPending); var text4 = JsonEncodedText.Encode(largeArray.AsSpan(0, 1)); for (int i = 0; i < 10_000_000; i++) { - writer.WriteRawValue(text4.EncodedUtf8Bytes); + writer.WriteRawValue(WrapInQuotes(text4.EncodedUtf8Bytes)); } Assert.Equal(2_100_097_542 + (4 * 10_000_000), writer.BytesPending); } } - [Fact] - public static void WriteRawNullOrEmptyTokenInvalid() + [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.OSX)] + [ConditionalTheory(nameof(IsX64))] + [InlineData(JsonTokenType.String)] + [InlineData(JsonTokenType.StartArray)] + [InlineData(JsonTokenType.StartObject)] + public static void WriteRawMaxInputLength(JsonTokenType tokenType) { + // Max raw payload length supported by the writer. + int maxLength = int.MaxValue / 3; + + byte[] payload = new byte[maxLength]; + payload[0] = (byte)'"'; + payload[maxLength - 1] = (byte)'"'; + + for (int i = 1; i < maxLength - 1; i++) + { + payload[i] = (byte)'a'; + } + using MemoryStream ms = new(); using Utf8JsonWriter writer = new(ms); - Assert.Throws(() => writer.WriteRawValue(json: default(string))); - Assert.Throws(() => writer.WriteRawValue(json: "")); - Assert.Throws(() => writer.WriteRawValue(json: default(ReadOnlySpan))); - Assert.Throws(() => writer.WriteRawValue(utf8Json: default)); + + switch(tokenType) + { + case JsonTokenType.String: + writer.WriteRawValue(payload); + writer.Flush(); + Assert.Equal(payload.Length, writer.BytesCommitted); + break; + case JsonTokenType.StartArray: + writer.WriteStartArray(); + writer.WriteRawValue(payload); + writer.WriteRawValue(payload); + writer.WriteEndArray(); + writer.Flush(); + // Start/EndArray + comma, 2 array elements + Assert.Equal(3 + (payload.Length * 2), writer.BytesCommitted); + break; + case JsonTokenType.StartObject: + writer.WriteStartObject(); + writer.WritePropertyName("1"); + writer.WriteRawValue(payload); + writer.WritePropertyName("2"); + writer.WriteRawValue(payload); + writer.WriteEndObject(); + writer.Flush(); + // Start/EndToken + comma, 2 property names, 2 property values + Assert.Equal(3 + (4 * 2) + (payload.Length * 2), writer.BytesCommitted); + break; + default: + Assert.True(false, "Unexpected test configuration"); + break; + } + } + + [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.OSX)] + [ConditionalTheory(nameof(IsX64))] + [InlineData((int.MaxValue / 3) + 1)] + [InlineData(int.MaxValue / 3 + 2)] + public static void WriteRawLengthGreaterThanMax(int len) + { + byte[] payload = new byte[len]; + payload[0] = (byte)'"'; + payload[len - 1] = (byte)'"'; + + for (int i = 1; i < len - 1; i++) + { + payload[i] = (byte)'a'; + } + + using MemoryStream ms = new(); + using Utf8JsonWriter writer = new(ms); + Assert.Throws(() => writer.WriteRawValue(payload)); } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.cs index ce68f8495e112..f18f23a64f3c6 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.cs @@ -739,7 +739,7 @@ private static string GetExpectedLargeArrayOfStrings(int length) /// problems on Linux due to the way deferred memory allocation works. On Linux, the allocation can /// succeed even if there is not enough memory but then the test may get killed by the OOM killer at the /// time the memory is accessed which triggers the full memory allocation. - /// Also see + /// Also see /// [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.OSX)] [ConditionalFact(nameof(IsX64))]