Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove Base64 value length limit from Utf8JsonWriter #85334

Merged
merged 3 commits into from
Apr 28, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ internal static partial class JsonConstants

public const int MaxEscapedTokenSize = 1_000_000_000; // Max size for already escaped value.
stephentoub marked this conversation as resolved.
Show resolved Hide resolved
public const int MaxUnescapedTokenSize = MaxEscapedTokenSize / MaxExpansionFactorWhileEscaping; // 166_666_666 bytes
public const int MaxBase64ValueTokenSize = (MaxEscapedTokenSize >> 2) * 3 / MaxExpansionFactorWhileEscaping; // 125_000_000 bytes
public const int MaxCharacterTokenSize = MaxEscapedTokenSize / MaxExpansionFactorWhileEscaping; // 166_666_666 characters

public const int MaximumFormatBooleanLength = 5;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ public static void ThrowInvalidOperationException_NeedLargerSpan()
throw GetInvalidOperationException(SR.FailedToGetLargerSpan);
}

[DoesNotReturn]
public static void ThrowPropertyNameTooLargeArgumentException(int length)
{
throw GetArgumentException(SR.Format(SR.PropertyNameTooLarge, length));
stephentoub marked this conversation as resolved.
Show resolved Hide resolved
}

[DoesNotReturn]
public static void ThrowArgumentException(ReadOnlySpan<byte> propertyName, ReadOnlySpan<byte> value)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,6 @@ public static void ValidateValue(ReadOnlySpan<byte> value)
ThrowHelper.ThrowArgumentException_ValueTooLarge(value.Length);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ValidateBytes(ReadOnlySpan<byte> bytes)
{
if (bytes.Length > JsonConstants.MaxBase64ValueTokenSize)
ThrowHelper.ThrowArgumentException_ValueTooLarge(bytes.Length);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ValidateDouble(double value)
{
Expand Down Expand Up @@ -114,17 +107,17 @@ public static void ValidatePropertyAndValue(ReadOnlySpan<char> propertyName, Rea
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ValidatePropertyAndBytes(ReadOnlySpan<char> propertyName, ReadOnlySpan<byte> bytes)
public static void ValidatePropertyNameLength(ReadOnlySpan<char> propertyName)
{
if (propertyName.Length > JsonConstants.MaxCharacterTokenSize || bytes.Length > JsonConstants.MaxBase64ValueTokenSize)
ThrowHelper.ThrowArgumentException(propertyName, bytes);
if (propertyName.Length > JsonConstants.MaxCharacterTokenSize)
ThrowHelper.ThrowPropertyNameTooLargeArgumentException(propertyName.Length);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ValidatePropertyAndBytes(ReadOnlySpan<byte> propertyName, ReadOnlySpan<byte> bytes)
public static void ValidatePropertyNameLength(ReadOnlySpan<byte> propertyName)
{
if (propertyName.Length > JsonConstants.MaxUnescapedTokenSize || bytes.Length > JsonConstants.MaxBase64ValueTokenSize)
ThrowHelper.ThrowArgumentException(propertyName, bytes);
if (propertyName.Length > JsonConstants.MaxUnescapedTokenSize)
ThrowHelper.ThrowPropertyNameTooLargeArgumentException(propertyName.Length);
}

internal static void ValidateNumber(ReadOnlySpan<byte> utf8FormattedNumber)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ public void WriteBase64String(JsonEncodedText propertyName, ReadOnlySpan<byte> b
ReadOnlySpan<byte> utf8PropertyName = propertyName.EncodedUtf8Bytes;
Debug.Assert(utf8PropertyName.Length <= JsonConstants.MaxUnescapedTokenSize);

JsonWriterHelper.ValidateBytes(bytes);

WriteBase64ByOptions(utf8PropertyName, bytes);

SetFlagToAddListSeparatorBeforeNextItem();
Expand Down Expand Up @@ -72,7 +70,7 @@ public void WriteBase64String(string propertyName, ReadOnlySpan<byte> bytes)
/// </remarks>
public void WriteBase64String(ReadOnlySpan<char> propertyName, ReadOnlySpan<byte> bytes)
{
JsonWriterHelper.ValidatePropertyAndBytes(propertyName, bytes);
JsonWriterHelper.ValidatePropertyNameLength(propertyName);

WriteBase64Escape(propertyName, bytes);

Expand All @@ -96,7 +94,7 @@ public void WriteBase64String(ReadOnlySpan<char> propertyName, ReadOnlySpan<byte
/// </remarks>
public void WriteBase64String(ReadOnlySpan<byte> utf8PropertyName, ReadOnlySpan<byte> bytes)
{
JsonWriterHelper.ValidatePropertyAndBytes(utf8PropertyName, bytes);
JsonWriterHelper.ValidatePropertyNameLength(utf8PropertyName);

WriteBase64Escape(utf8PropertyName, bytes);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ public sealed partial class Utf8JsonWriter
/// </remarks>
public void WriteBase64StringValue(ReadOnlySpan<byte> bytes)
{
JsonWriterHelper.ValidateBytes(bytes);

WriteBase64ByOptions(bytes);

SetFlagToAddListSeparatorBeforeNextItem();
Expand All @@ -51,13 +49,22 @@ private void WriteBase64ByOptions(ReadOnlySpan<byte> bytes)
// TODO: https://github.com/dotnet/runtime/issues/29293
private void WriteBase64Minimized(ReadOnlySpan<byte> bytes)
{
int encodingLength = Base64.GetMaxEncodedToUtf8Length(bytes.Length);
// Base64.GetMaxEncodedToUtf8Length checks to make sure the length is <= int.MaxValue / 4 * 3,
// as a length longer than that would overflow int.MaxValue when Base64 encoded. To ensure we
// throw an appropriate exception, we check the same condition here first.
const int MaxLengthAllowed = int.MaxValue / 4 * 3;
if (bytes.Length > MaxLengthAllowed)
{
ThrowHelper.ThrowArgumentException_ValueTooLarge(bytes.Length);
}

Debug.Assert(encodingLength < int.MaxValue - 3);
int encodingLength = Base64.GetMaxEncodedToUtf8Length(bytes.Length);
Debug.Assert(encodingLength <= int.MaxValue - 3);

// 2 quotes to surround the base-64 encoded string value.
// Optionally, 1 list separator
int maxRequired = encodingLength + 3;
Debug.Assert((uint)maxRequired <= int.MaxValue);

if (_memory.Length - BytesPending < maxRequired)
{
Expand All @@ -83,13 +90,21 @@ private void WriteBase64Indented(ReadOnlySpan<byte> bytes)
int indent = Indentation;
Debug.Assert(indent <= 2 * _options.MaxDepth);

int encodingLength = Base64.GetMaxEncodedToUtf8Length(bytes.Length);
// Base64.GetMaxEncodedToUtf8Length checks to make sure the length is <= int.MaxValue / 4 * 3,
// as a length longer than that would overflow int.MaxValue when Base64 encoded. However, we
// also need the indentation + 2 quotes, and optionally a list separate and 1-2 bytes for a new line.
// Validate the encoded bytes length won't overflow with all of the length.
int extraSpaceRequired = indent + 3 + s_newLineLength;
int maxLengthAllowed = int.MaxValue / 4 * 3 - extraSpaceRequired;
if (bytes.Length > maxLengthAllowed)
{
ThrowHelper.ThrowArgumentException_ValueTooLarge(bytes.Length);
}

Debug.Assert(encodingLength < int.MaxValue - indent - 3 - s_newLineLength);
int encodingLength = Base64.GetMaxEncodedToUtf8Length(bytes.Length);

// indentation + 2 quotes to surround the base-64 encoded string value.
// Optionally, 1 list separator, and 1-2 bytes for new line
int maxRequired = indent + encodingLength + 3 + s_newLineLength;
int maxRequired = encodingLength + extraSpaceRequired;
Debug.Assert((uint)maxRequired <= int.MaxValue - 3);

if (_memory.Length - BytesPending < maxRequired)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using System.Text.Unicode;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.XUnitExtensions;
using Newtonsoft.Json;
using Xunit;

Expand Down Expand Up @@ -3118,7 +3119,7 @@ public void WritingTooLargeBase64Bytes(bool formatted, bool skipValidation)

using (var jsonUtf8 = new Utf8JsonWriter(output, options))
{
Assert.Throws<ArgumentException>(() => jsonUtf8.WriteBase64StringValue(value.AsSpan(0, 125_000_001)));
jsonUtf8.WriteBase64StringValue(value.AsSpan(0, 125_000_001));
stephentoub marked this conversation as resolved.
Show resolved Hide resolved
}

using (var jsonUtf8 = new Utf8JsonWriter(output, options))
Expand All @@ -3133,31 +3134,31 @@ public void WritingTooLargeBase64Bytes(bool formatted, bool skipValidation)

using (var jsonUtf8 = new Utf8JsonWriter(output, options))
{
Assert.Throws<ArgumentException>(() => jsonUtf8.WriteBase64StringValue(value));
jsonUtf8.WriteBase64StringValue(value);
}

using (var jsonUtf8 = new Utf8JsonWriter(output, options))
{
jsonUtf8.WriteStartObject();
Assert.Throws<ArgumentException>(() => jsonUtf8.WriteBase64String("foo", value));
jsonUtf8.WriteBase64String("foo", value);
}

using (var jsonUtf8 = new Utf8JsonWriter(output, options))
{
jsonUtf8.WriteStartObject();
Assert.Throws<ArgumentException>(() => jsonUtf8.WriteBase64String("foo"u8, value));
jsonUtf8.WriteBase64String("foo"u8, value);
}

using (var jsonUtf8 = new Utf8JsonWriter(output, options))
{
jsonUtf8.WriteStartObject();
Assert.Throws<ArgumentException>(() => jsonUtf8.WriteBase64String("foo".AsSpan(), value));
jsonUtf8.WriteBase64String("foo".AsSpan(), value);
}

using (var jsonUtf8 = new Utf8JsonWriter(output, options))
{
jsonUtf8.WriteStartObject();
Assert.Throws<ArgumentException>(() => jsonUtf8.WriteBase64String(JsonEncodedText.Encode("foo"), value));
jsonUtf8.WriteBase64String(JsonEncodedText.Encode("foo"), value);
}
}

Expand All @@ -3172,59 +3173,57 @@ public void WritingTooLargeBase64Bytes(bool formatted, bool skipValidation)
[InlineData(true, false)]
[InlineData(false, true)]
[InlineData(false, false)]
public void WritingLargestPossibleBase64Bytes(bool formatted, bool skipValidation)
public void WritingHugeBase64Bytes(bool formatted, bool skipValidation)
{
byte[] value;

try
{
value = new byte[125_000_000];
}
catch (OutOfMemoryException)
{
return;
}
byte[] value = new byte[1_000_000_000];

value.AsSpan().Fill(168);
value.AsSpan().Fill(168);

var options = new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation };
var output = new ArrayBufferWriter<byte>(1024);
var options = new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation };
var output = new ArrayBufferWriter<byte>(1024);

using (var jsonUtf8 = new Utf8JsonWriter(output, options))
{
jsonUtf8.WriteBase64StringValue(value);
}
using (var jsonUtf8 = new Utf8JsonWriter(output, options))
{
jsonUtf8.WriteBase64StringValue(value);
}

output.Clear();
using (var jsonUtf8 = new Utf8JsonWriter(output, options))
{
jsonUtf8.WriteStartObject();
jsonUtf8.WriteBase64String("foo", value);
jsonUtf8.WriteEndObject();
}
output.Clear();
using (var jsonUtf8 = new Utf8JsonWriter(output, options))
{
jsonUtf8.WriteStartObject();
jsonUtf8.WriteBase64String("foo", value);
jsonUtf8.WriteEndObject();
}

output.Clear();
using (var jsonUtf8 = new Utf8JsonWriter(output, options))
{
jsonUtf8.WriteStartObject();
jsonUtf8.WriteBase64String("foo"u8, value);
jsonUtf8.WriteEndObject();
}
output.Clear();
using (var jsonUtf8 = new Utf8JsonWriter(output, options))
{
jsonUtf8.WriteStartObject();
jsonUtf8.WriteBase64String("foo"u8, value);
jsonUtf8.WriteEndObject();
}

output.Clear();
using (var jsonUtf8 = new Utf8JsonWriter(output, options))
{
jsonUtf8.WriteStartObject();
jsonUtf8.WriteBase64String("foo".AsSpan(), value);
jsonUtf8.WriteEndObject();
}
output.Clear();
using (var jsonUtf8 = new Utf8JsonWriter(output, options))
{
jsonUtf8.WriteStartObject();
jsonUtf8.WriteBase64String("foo".AsSpan(), value);
jsonUtf8.WriteEndObject();
}

output.Clear();
using (var jsonUtf8 = new Utf8JsonWriter(output, options))
output.Clear();
using (var jsonUtf8 = new Utf8JsonWriter(output, options))
{
jsonUtf8.WriteStartObject();
jsonUtf8.WriteBase64String(JsonEncodedText.Encode("foo"), value);
jsonUtf8.WriteEndObject();
}
}
catch (OutOfMemoryException)
{
jsonUtf8.WriteStartObject();
jsonUtf8.WriteBase64String(JsonEncodedText.Encode("foo"), value);
jsonUtf8.WriteEndObject();
throw new SkipTestException("Out of memory allocating large objects");
}
}

Expand Down