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

JSON: Add support for Int128, UInt128 and Half #88962

Merged
merged 9 commits into from
Jul 18, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,17 @@ namespace System.Text.Json
/// <exception cref="ArgumentException">The destination buffer is too small to hold the unescaped value.</exception>
public readonly int CopyString(Span<byte> utf8Destination)
{
if (_tokenType is not (JsonTokenType.String or JsonTokenType.PropertyName or JsonTokenType.Number))
if (_tokenType is not (JsonTokenType.String or JsonTokenType.PropertyName))
{
ThrowHelper.ThrowInvalidOperationException_ExpectedString(_tokenType);
}

return CopyValue(utf8Destination);
}

internal readonly int CopyValue(Span<byte> utf8Destination)
{
Debug.Assert(_tokenType is JsonTokenType.String or JsonTokenType.PropertyName or JsonTokenType.Number);
Debug.Assert(_tokenType != JsonTokenType.Number || !ValueIsEscaped, "Numbers can't contain escape characters.");

int bytesWritten;
Expand Down Expand Up @@ -126,11 +132,17 @@ namespace System.Text.Json
/// <exception cref="ArgumentException">The destination buffer is too small to hold the unescaped value.</exception>
public readonly int CopyString(Span<char> destination)
{
if (_tokenType is not (JsonTokenType.String or JsonTokenType.PropertyName or JsonTokenType.Number))
if (_tokenType is not (JsonTokenType.String or JsonTokenType.PropertyName))
{
ThrowHelper.ThrowInvalidOperationException_ExpectedString(_tokenType);
}

return CopyValue(destination);
}

internal readonly int CopyValue(Span<char> destination)
{
Debug.Assert(_tokenType is JsonTokenType.String or JsonTokenType.PropertyName or JsonTokenType.Number);
Debug.Assert(_tokenType != JsonTokenType.Number || !ValueIsEscaped, "Numbers can't contain escape characters.");

scoped ReadOnlySpan<byte> unescapedSource;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Diagnostics;

namespace System.Text.Json.Serialization.Converters
{
internal sealed class HalfConverter : JsonPrimitiveConverter<Half>
{
private const int MaxFormatLength = 16;
private const int MaxEscapedFormatLength = MaxFormatLength * JsonConstants.MaxExpansionFactorWhileEscaping;
private const int MaxFormatLength = 20;

public HalfConverter()
{
Expand All @@ -34,36 +34,57 @@ private static Half ReadCore(ref Utf8JsonReader reader)
{
Half result;

if (reader.ValueLength > MaxEscapedFormatLength)
byte[]? rentedByteBuffer = null;
char[]? rentedCharBuffer = null;
int bufferLength = reader.ValueLength;

try
{
ThrowHelper.ThrowFormatException(NumericType.Half);
}
Span<byte> byteBuffer = bufferLength <= JsonConstants.StackallocByteThreshold
? stackalloc byte[JsonConstants.StackallocCharThreshold]
: (rentedByteBuffer = ArrayPool<byte>.Shared.Rent(bufferLength));

Span<byte> buffer = stackalloc byte[MaxFormatLength];
int written = reader.CopyString(buffer);
buffer = buffer.Slice(0, written);
int written = reader.CopyValue(byteBuffer);
byteBuffer = byteBuffer.Slice(0, written);

if (reader.TokenType is JsonTokenType.String or JsonTokenType.PropertyName)
{
if (JsonReaderHelper.TryGetFloatingPointConstant(buffer, out result))
if (reader.TokenType is JsonTokenType.String or JsonTokenType.PropertyName)
{
return result;
if (JsonReaderHelper.TryGetFloatingPointConstant(byteBuffer, out result))
{
return result;
}
}
}

#if NET8_0_OR_GREATER
bool success = Half.TryParse(buffer, out result);
bool success = Half.TryParse(byteBuffer, out result);
#else
Span<char> charBuffer = stackalloc char[MaxFormatLength];
written = JsonReaderHelper.TranscodeHelper(buffer, charBuffer);
bool success = Half.TryParse(charBuffer.Slice(0, written), out result);
// Half.TryParse(ROS<byte>) is not available on .NET 7, only Half.TryParse(ROS<char>);
// we need to transcode here instead of letting CopyValue do it for us because
// TryGetFloatingPointConstant only accepts ROS<byte>.

Span<char> charBuffer = stackalloc char[MaxFormatLength];
written = JsonReaderHelper.TranscodeHelper(byteBuffer, charBuffer);
bool success = Half.TryParse(charBuffer.Slice(0, written), out result);
#endif
if (!success)
{
ThrowHelper.ThrowFormatException(NumericType.Half);
if (!success)
{
ThrowHelper.ThrowFormatException(NumericType.Half);
}

return result;
}
finally
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We generally don't use try/finally to guard against exceptions in code that uses rented buffers. Not returning a buffer because of an exception is not a big problem all things considered (the problems start if a buffer gets used after being returns or gets returned more than once). You should still move the returning logic above the if (!success) statement above though.

{
if (rentedByteBuffer != null)
{
ArrayPool<byte>.Shared.Return(rentedByteBuffer);
}

return result;
if (rentedCharBuffer != null)
{
ArrayPool<char>.Shared.Return(rentedCharBuffer);
}
}
}

private static void WriteCore(Utf8JsonWriter writer, Half value)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Diagnostics;

namespace System.Text.Json.Serialization.Converters
{
internal sealed class Int128Converter : JsonPrimitiveConverter<Int128>
{
private const int MaxFormatLength = 40;
private const int MaxEscapedFormatLength = MaxFormatLength * JsonConstants.MaxExpansionFactorWhileEscaping;

public Int128Converter()
{
Expand All @@ -32,23 +32,45 @@ public override void Write(Utf8JsonWriter writer, Int128 value, JsonSerializerOp

private static Int128 ReadCore(ref Utf8JsonReader reader)
{
if (reader.ValueLength > MaxEscapedFormatLength)
{
ThrowHelper.ThrowFormatException(NumericType.Int128);
}
int bufferLength = reader.ValueLength;

#if NET8_0_OR_GREATER
Span<byte> buffer = stackalloc byte[MaxFormatLength];
byte[]? rentedBuffer = null;
#else
Span<char> buffer = stackalloc char[MaxFormatLength];
char[]? rentedBuffer = null;
#endif
int written = reader.CopyString(buffer);
if (!Int128.TryParse(buffer.Slice(0, written), out Int128 result))
try
{
ThrowHelper.ThrowFormatException(NumericType.Int128);
}
#if NET8_0_OR_GREATER
Span<byte> buffer = bufferLength <= JsonConstants.StackallocByteThreshold
? stackalloc byte[JsonConstants.StackallocByteThreshold]
: (rentedBuffer = ArrayPool<byte>.Shared.Rent(bufferLength));
#else
// Int128.TryParse(ROS<byte>) is not available on .NET 7, only Int128.TryParse(ROS<char>).
Span<char> buffer = bufferLength <= JsonConstants.StackallocCharThreshold
? stackalloc char[JsonConstants.StackallocCharThreshold]
: (rentedBuffer = ArrayPool<char>.Shared.Rent(bufferLength));
#endif

return result;
int written = reader.CopyValue(buffer);
if (!Int128.TryParse(buffer.Slice(0, written), out Int128 result))
{
ThrowHelper.ThrowFormatException(NumericType.Int128);
}

return result;
}
finally
{
if (rentedBuffer != null)
{
#if NET8_0_OR_GREATER
ArrayPool<byte>.Shared.Return(rentedBuffer);
#else
ArrayPool<char>.Shared.Return(rentedBuffer);
#endif
}
}
}

private static void WriteCore(Utf8JsonWriter writer, Int128 value)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Diagnostics;

namespace System.Text.Json.Serialization.Converters
{
internal sealed class UInt128Converter : JsonPrimitiveConverter<UInt128>
{
private const int MaxFormatLength = 39;
private const int MaxEscapedFormatLength = MaxFormatLength * JsonConstants.MaxExpansionFactorWhileEscaping;

public UInt128Converter()
{
Expand All @@ -32,23 +32,44 @@ public override void Write(Utf8JsonWriter writer, UInt128 value, JsonSerializerO

private static UInt128 ReadCore(ref Utf8JsonReader reader)
{
if (reader.ValueLength > MaxEscapedFormatLength)
{
ThrowHelper.ThrowFormatException(NumericType.UInt128);
}
int bufferLength = reader.ValueLength;

#if NET8_0_OR_GREATER
Span<byte> buffer = stackalloc byte[MaxFormatLength];
byte[]? rentedBuffer = null;
#else
Span<char> buffer = stackalloc char[MaxFormatLength];
char[]? rentedBuffer = null;
#endif
int written = reader.CopyString(buffer);
if (!UInt128.TryParse(buffer.Slice(0, written), out UInt128 result))
try
{
ThrowHelper.ThrowFormatException(NumericType.UInt128);
}
#if NET8_0_OR_GREATER
Span<byte> buffer = bufferLength <= JsonConstants.StackallocByteThreshold
? stackalloc byte[JsonConstants.StackallocByteThreshold]
: (rentedBuffer = ArrayPool<byte>.Shared.Rent(bufferLength));
#else
// UInt128.TryParse(ROS<byte>) is not available on .NET 7, only UInt128.TryParse(ROS<char>).
Span<char> buffer = bufferLength <= JsonConstants.StackallocCharThreshold
? stackalloc char[JsonConstants.StackallocCharThreshold]
: (rentedBuffer = ArrayPool<char>.Shared.Rent(bufferLength));
#endif
int written = reader.CopyValue(buffer);
if (!UInt128.TryParse(buffer.Slice(0, written), out UInt128 result))
{
ThrowHelper.ThrowFormatException(NumericType.UInt128);
}

return result;
return result;
}
finally
{
if (rentedBuffer != null)
{
#if NET8_0_OR_GREATER
ArrayPool<byte>.Shared.Return(rentedBuffer);
#else
ArrayPool<char>.Shared.Return(rentedBuffer);
#endif
}
}
}

private static void WriteCore(Utf8JsonWriter writer, UInt128 value)
Expand Down