From b4b569e881ab7ca04bac6969ca97f253feb0b68e Mon Sep 17 00:00:00 2001 From: David Cantu Date: Sun, 16 Jul 2023 02:12:50 -0500 Subject: [PATCH 1/9] JSON: Add support for Int128, UInt128 and Half and add Number support for Utf8JsonReader.CopyString(...) --- .../gen/Helpers/KnownTypeSymbols.cs | 9 ++ .../gen/JsonSourceGenerator.Parser.cs | 3 + .../ref/System.Text.Json.netcoreapp.cs | 7 + .../src/Resources/Strings.resx | 9 ++ .../src/System.Text.Json.csproj | 6 + .../Text/Json/Reader/JsonReaderHelper.cs | 33 ++++ .../Text/Json/Reader/Utf8JsonReader.TryGet.cs | 8 +- .../Converters/Value/CharConverter.cs | 5 + .../Converters/Value/HalfConverter.cs | 146 ++++++++++++++++++ .../Converters/Value/Int128Converter.cs | 120 ++++++++++++++ .../Converters/Value/UInt128Converter.cs | 120 ++++++++++++++ .../DefaultJsonTypeInfoResolver.Converters.cs | 7 +- .../JsonMetadataServices.Converters.cs | 26 ++++ .../Metadata/JsonPropertyInfo.cs | 7 + .../src/System/Text/Json/ThrowHelper.cs | 12 ++ ...CollectionTests.Dictionary.NonStringKey.cs | 15 +- .../tests/Common/JsonNumberTestData.cs | 67 ++++++++ .../tests/Common/JsonTestHelper.cs | 10 ++ .../tests/Common/NumberHandlingTests.cs | 40 +++++ .../Serialization/NumberHandlingTests.cs | 124 +++++++++++++++ .../Utf8JsonReaderTests.TryGet.cs | 88 ++++++++++- 21 files changed, 857 insertions(+), 5 deletions(-) create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int128Converter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt128Converter.cs diff --git a/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs b/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs index 99ea95b8ec451..a4d5e8a25824d 100644 --- a/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs +++ b/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs @@ -135,6 +135,15 @@ public KnownTypeSymbols(Compilation compilation) public INamedTypeSymbol? TimeOnlyType => GetOrResolveType("System.TimeOnly", ref _TimeOnlyType); private Option _TimeOnlyType; + public INamedTypeSymbol? Int128Type => GetOrResolveType("System.Int128", ref _Int128Type); + private Option _Int128Type; + + public INamedTypeSymbol? UInt128Type => GetOrResolveType("System.UInt128", ref _UInt128Type); + private Option _UInt128Type; + + public INamedTypeSymbol? HalfType => GetOrResolveType("System.Half", ref _HalfType); + private Option _HalfType; + public IArrayTypeSymbol? ByteArrayType => _ByteArrayType.HasValue ? _ByteArrayType.Value : (_ByteArrayType = new(Compilation.CreateArrayTypeSymbol(Compilation.GetSpecialType(SpecialType.System_Byte), rank: 1))).Value; diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index 423d34020c681..9772e403b410e 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -1494,6 +1494,9 @@ private static HashSet CreateBuiltInSupportTypeSet(KnownTypeSymbols AddTypeIfNotNull(knownSymbols.DateTimeOffsetType); AddTypeIfNotNull(knownSymbols.DateOnlyType); AddTypeIfNotNull(knownSymbols.TimeOnlyType); + AddTypeIfNotNull(knownSymbols.Int128Type); + AddTypeIfNotNull(knownSymbols.UInt128Type); + AddTypeIfNotNull(knownSymbols.HalfType); AddTypeIfNotNull(knownSymbols.GuidType); AddTypeIfNotNull(knownSymbols.UriType); AddTypeIfNotNull(knownSymbols.VersionType); diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.netcoreapp.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.netcoreapp.cs index 8c2629401abbe..847da900ad35d 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.netcoreapp.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.netcoreapp.cs @@ -9,6 +9,13 @@ namespace System.Text.Json.Serialization.Metadata public static partial class JsonMetadataServices { public static System.Text.Json.Serialization.JsonConverter DateOnlyConverter { get { throw null; } } + public static System.Text.Json.Serialization.JsonConverter HalfConverter { get { throw null; } } public static System.Text.Json.Serialization.JsonConverter TimeOnlyConverter { get { throw null; } } + +#if NET7_0_OR_GREATER + public static System.Text.Json.Serialization.JsonConverter Int128Converter { get { throw null; } } + [System.CLSCompliantAttribute(false)] + public static System.Text.Json.Serialization.JsonConverter UInt128Converter { get { throw null; } } +#endif } } diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index 31b12248015bd..8e1da070471bc 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -690,4 +690,13 @@ JsonObjectCreationHandling.Populate is incompatible with reference handling. + + Either the JSON value is not in a supported format, or is out of bounds for an Int128. + + + Either the JSON value is not in a supported format, or is out of bounds for an UInt128. + + + Either the JSON value is not in a supported format, or is out of bounds for a Half. + 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 49b9be5f46ec6..a5c2cda7363c6 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -345,11 +345,17 @@ 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/Text/Json/Reader/JsonReaderHelper.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs index 9db9e80c771d7..9cdcc0ddfad74 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs @@ -145,6 +145,39 @@ public static bool TryGetEscapedGuid(ReadOnlySpan source, out Guid value) return false; } +#if NETCOREAPP + public static bool TryGetFloatingPointConstant(ReadOnlySpan span, out Half value) + { + if (span.Length == 3) + { + if (span.SequenceEqual(JsonConstants.NaNValue)) + { + value = Half.NaN; + return true; + } + } + else if (span.Length == 8) + { + if (span.SequenceEqual(JsonConstants.PositiveInfinityValue)) + { + value = Half.PositiveInfinity; + return true; + } + } + else if (span.Length == 9) + { + if (span.SequenceEqual(JsonConstants.NegativeInfinityValue)) + { + value = Half.NegativeInfinity; + return true; + } + } + + value = default; + return false; + } +#endif + public static bool TryGetFloatingPointConstant(ReadOnlySpan span, out float value) { if (span.Length == 3) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs index 91c0bfc4daf6f..e7e88308beba8 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs @@ -68,11 +68,13 @@ namespace System.Text.Json /// The destination buffer is too small to hold the unescaped value. public readonly int CopyString(Span utf8Destination) { - if (_tokenType is not (JsonTokenType.String or JsonTokenType.PropertyName)) + if (_tokenType is not (JsonTokenType.String or JsonTokenType.PropertyName or JsonTokenType.Number)) { ThrowHelper.ThrowInvalidOperationException_ExpectedString(_tokenType); } + Debug.Assert(_tokenType != JsonTokenType.Number || !ValueIsEscaped, "Numbers can't contain escape characters."); + int bytesWritten; if (ValueIsEscaped) @@ -124,11 +126,13 @@ namespace System.Text.Json /// The destination buffer is too small to hold the unescaped value. public readonly int CopyString(Span destination) { - if (_tokenType is not (JsonTokenType.String or JsonTokenType.PropertyName)) + if (_tokenType is not (JsonTokenType.String or JsonTokenType.PropertyName or JsonTokenType.Number)) { ThrowHelper.ThrowInvalidOperationException_ExpectedString(_tokenType); } + Debug.Assert(_tokenType != JsonTokenType.Number || !ValueIsEscaped, "Numbers can't contain escape characters."); + scoped ReadOnlySpan unescapedSource; byte[]? rentedBuffer = null; int valueLength; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/CharConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/CharConverter.cs index b3caa32d186bc..8c19ce1474a60 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/CharConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/CharConverter.cs @@ -12,6 +12,11 @@ internal sealed class CharConverter : JsonPrimitiveConverter public override char Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType is not (JsonTokenType.String or JsonTokenType.PropertyName)) + { + ThrowHelper.ThrowInvalidOperationException_ExpectedString(reader.TokenType); + } + if (!JsonHelpers.IsInRangeInclusive(reader.ValueLength, 1, MaxEscapedCharacterLength)) { ThrowHelper.ThrowInvalidOperationException_ExpectedChar(reader.TokenType); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs new file mode 100644 index 0000000000000..122d0a3875722 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class HalfConverter : JsonPrimitiveConverter + { + private const int MaxFormatLength = 16; + private const int MaxEscapedFormatLength = MaxFormatLength * JsonConstants.MaxExpansionFactorWhileEscaping; + + public HalfConverter() + { + IsInternalConverterForNumberType = true; + } + + public override Half Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.Number) + { + ThrowHelper.ThrowInvalidOperationException_ExpectedNumber(reader.TokenType); + } + + return ReadCore(ref reader); + } + + public override void Write(Utf8JsonWriter writer, Half value, JsonSerializerOptions options) + { + WriteCore(writer, value); + } + + private static Half ReadCore(ref Utf8JsonReader reader) + { + Half result; + + if (reader.ValueLength > MaxEscapedFormatLength) + { + ThrowHelper.ThrowFormatException(NumericType.Half); + } + + Span buffer = stackalloc byte[MaxFormatLength]; + int written = reader.CopyString(buffer); + buffer = buffer.Slice(0, written); + + if (reader.TokenType is JsonTokenType.String or JsonTokenType.PropertyName) + { + if (JsonReaderHelper.TryGetFloatingPointConstant(buffer, out result)) + { + return result; + } + } + +#if NET8_0_OR_GREATER + bool success = Half.TryParse(buffer, out result); +#else + Span charBuffer = stackalloc char[MaxFormatLength]; + written = JsonReaderHelper.TranscodeHelper(buffer, charBuffer); + bool success = Half.TryParse(charBuffer.Slice(0, written), out result); +#endif + if (!success) + { + ThrowHelper.ThrowFormatException(NumericType.Half); + } + + return result; + } + + private static void WriteCore(Utf8JsonWriter writer, Half value) + { +#if NET8_0_OR_GREATER + Span buffer = stackalloc byte[MaxFormatLength]; +#else + Span buffer = stackalloc char[MaxFormatLength]; +#endif + bool formattedSuccessfully = value.TryFormat(buffer, out int written); + Debug.Assert(formattedSuccessfully); + writer.WriteRawValue(buffer.Slice(0, written)); + } + + internal override Half ReadAsPropertyNameCore(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); + return ReadCore(ref reader); + } + + internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, Half value, JsonSerializerOptions options, bool isWritingExtensionDataProperty) + { +#if NET8_0_OR_GREATER + Span buffer = stackalloc byte[MaxFormatLength]; +#else + Span buffer = stackalloc char[MaxFormatLength]; +#endif + bool formattedSuccessfully = value.TryFormat(buffer, out int written); + Debug.Assert(formattedSuccessfully); + writer.WritePropertyName(buffer.Slice(0, written)); + } + + internal override Half ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + if ((JsonNumberHandling.AllowReadingFromString & handling) != 0) + { + return ReadCore(ref reader); + } + else if ((JsonNumberHandling.AllowNamedFloatingPointLiterals & handling) != 0) + { + if (!JsonReaderHelper.TryGetFloatingPointConstant(default, out Half value)) + { + ThrowHelper.ThrowFormatException(NumericType.Half); + } + + return value; + } + } + + return Read(ref reader, Type, options); + } + + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, Half value, JsonNumberHandling handling) + { + if ((JsonNumberHandling.WriteAsString & handling) != 0) + { +#if NET8_0_OR_GREATER + const byte Quote = JsonConstants.Quote; + Span buffer = stackalloc byte[MaxFormatLength + 2]; +#else + const char Quote = (char)JsonConstants.Quote; + Span buffer = stackalloc char[MaxFormatLength + 2]; +#endif + buffer[0] = Quote; + bool formattedSuccessfully = value.TryFormat(buffer.Slice(1), out int written); + Debug.Assert(formattedSuccessfully); + + int length = written + 2; + buffer[length - 1] = Quote; + writer.WriteRawValue(buffer.Slice(0, length)); + } + else + { + WriteCore(writer, value); + } + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int128Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int128Converter.cs new file mode 100644 index 0000000000000..5350d997345c6 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int128Converter.cs @@ -0,0 +1,120 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class Int128Converter : JsonPrimitiveConverter + { + private const int MaxFormatLength = 40; + private const int MaxEscapedFormatLength = MaxFormatLength * JsonConstants.MaxExpansionFactorWhileEscaping; + + public Int128Converter() + { + IsInternalConverterForNumberType = true; + } + + public override Int128 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.Number) + { + ThrowHelper.ThrowInvalidOperationException_ExpectedNumber(reader.TokenType); + } + + return ReadCore(ref reader); + } + + public override void Write(Utf8JsonWriter writer, Int128 value, JsonSerializerOptions options) + { + WriteCore(writer, value); + } + + private static Int128 ReadCore(ref Utf8JsonReader reader) + { + if (reader.ValueLength > MaxEscapedFormatLength) + { + ThrowHelper.ThrowFormatException(NumericType.Int128); + } + +#if NET8_0_OR_GREATER + Span buffer = stackalloc byte[MaxFormatLength]; +#else + Span buffer = stackalloc char[MaxFormatLength]; +#endif + int written = reader.CopyString(buffer); + if (!Int128.TryParse(buffer.Slice(0, written), out Int128 result)) + { + ThrowHelper.ThrowFormatException(NumericType.Int128); + } + + return result; + } + + private static void WriteCore(Utf8JsonWriter writer, Int128 value) + { +#if NET8_0_OR_GREATER + Span buffer = stackalloc byte[MaxFormatLength]; +#else + Span buffer = stackalloc char[MaxFormatLength]; +#endif + bool formattedSuccessfully = value.TryFormat(buffer, out int written); + Debug.Assert(formattedSuccessfully); + writer.WriteRawValue(buffer.Slice(0, written)); + } + + internal override Int128 ReadAsPropertyNameCore(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); + return ReadCore(ref reader); + } + + internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, Int128 value, JsonSerializerOptions options, bool isWritingExtensionDataProperty) + { +#if NET8_0_OR_GREATER + Span buffer = stackalloc byte[MaxFormatLength]; +#else + Span buffer = stackalloc char[MaxFormatLength]; +#endif + bool formattedSuccessfully = value.TryFormat(buffer, out int bytesWritten); + Debug.Assert(formattedSuccessfully); + writer.WritePropertyName(buffer); + } + + internal override Int128 ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String && + (JsonNumberHandling.AllowReadingFromString & handling) != 0) + { + return ReadCore(ref reader); + } + + return Read(ref reader, Type, options); + } + + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, Int128 value, JsonNumberHandling handling) + { + if ((JsonNumberHandling.WriteAsString & handling) != 0) + { +#if NET8_0_OR_GREATER + const byte Quote = JsonConstants.Quote; + Span buffer = stackalloc byte[MaxFormatLength + 2]; +#else + const char Quote = (char)JsonConstants.Quote; + Span buffer = stackalloc char[MaxFormatLength + 2]; +#endif + buffer[0] = Quote; + bool formattedSuccessfully = value.TryFormat(buffer.Slice(1), out int bytesWritten); + Debug.Assert(formattedSuccessfully); + + int length = bytesWritten + 2; + buffer[length - 1] = Quote; + writer.WriteRawValue(buffer.Slice(0, length)); + } + else + { + WriteCore(writer, value); + } + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt128Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt128Converter.cs new file mode 100644 index 0000000000000..89deb45e6e755 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt128Converter.cs @@ -0,0 +1,120 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class UInt128Converter : JsonPrimitiveConverter + { + private const int MaxFormatLength = 39; + private const int MaxEscapedFormatLength = MaxFormatLength * JsonConstants.MaxExpansionFactorWhileEscaping; + + public UInt128Converter() + { + IsInternalConverterForNumberType = true; + } + + public override UInt128 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.Number) + { + ThrowHelper.ThrowInvalidOperationException_ExpectedNumber(reader.TokenType); + } + + return ReadCore(ref reader); + } + + public override void Write(Utf8JsonWriter writer, UInt128 value, JsonSerializerOptions options) + { + WriteCore(writer, value); + } + + private static UInt128 ReadCore(ref Utf8JsonReader reader) + { + if (reader.ValueLength > MaxEscapedFormatLength) + { + ThrowHelper.ThrowFormatException(NumericType.UInt128); + } + +#if NET8_0_OR_GREATER + Span buffer = stackalloc byte[MaxFormatLength]; +#else + Span buffer = stackalloc char[MaxFormatLength]; +#endif + int written = reader.CopyString(buffer); + if (!UInt128.TryParse(buffer.Slice(0, written), out UInt128 result)) + { + ThrowHelper.ThrowFormatException(NumericType.UInt128); + } + + return result; + } + + private static void WriteCore(Utf8JsonWriter writer, UInt128 value) + { +#if NET8_0_OR_GREATER + Span buffer = stackalloc byte[MaxFormatLength]; +#else + Span buffer = stackalloc char[MaxFormatLength]; +#endif + bool formattedSuccessfully = value.TryFormat(buffer, out int written); + Debug.Assert(formattedSuccessfully); + writer.WriteRawValue(buffer.Slice(0, written)); + } + + internal override UInt128 ReadAsPropertyNameCore(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); + return ReadCore(ref reader); + } + + internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, UInt128 value, JsonSerializerOptions options, bool isWritingExtensionDataProperty) + { +#if NET8_0_OR_GREATER + Span buffer = stackalloc byte[MaxFormatLength]; +#else + Span buffer = stackalloc char[MaxFormatLength]; +#endif + bool formattedSuccessfully = value.TryFormat(buffer, out int bytesWritten); + Debug.Assert(formattedSuccessfully); + writer.WritePropertyName(buffer); + } + + internal override UInt128 ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String && + (JsonNumberHandling.AllowReadingFromString & handling) != 0) + { + return ReadCore(ref reader); + } + + return Read(ref reader, Type, options); + } + + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, UInt128 value, JsonNumberHandling handling) + { + if ((JsonNumberHandling.WriteAsString & handling) != 0) + { +#if NET8_0_OR_GREATER + const byte Quote = JsonConstants.Quote; + Span buffer = stackalloc byte[MaxFormatLength + 2]; +#else + const char Quote = (char)JsonConstants.Quote; + Span buffer = stackalloc char[MaxFormatLength + 2]; +#endif + buffer[0] = Quote; + bool formattedSuccessfully = value.TryFormat(buffer.Slice(1), out int bytesWritten); + Debug.Assert(formattedSuccessfully); + + int length = bytesWritten + 2; + buffer[length - 1] = Quote; + writer.WriteRawValue(buffer.Slice(0, length)); + } + else + { + WriteCore(writer, value); + } + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Converters.cs index 7516c3f735932..ed50066b0da86 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Converters.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Converters.cs @@ -40,7 +40,7 @@ private static JsonConverterFactory[] GetDefaultFactoryConverters() private static Dictionary GetDefaultSimpleConverters() { - const int NumberOfSimpleConverters = 28; + const int NumberOfSimpleConverters = 31; var converters = new Dictionary(NumberOfSimpleConverters); // Use a dictionary for simple converters. @@ -54,6 +54,7 @@ private static JsonConverterFactory[] GetDefaultFactoryConverters() #if NETCOREAPP Add(JsonMetadataServices.DateOnlyConverter); Add(JsonMetadataServices.TimeOnlyConverter); + Add(JsonMetadataServices.HalfConverter); #endif Add(JsonMetadataServices.DoubleConverter); Add(JsonMetadataServices.DecimalConverter); @@ -73,6 +74,10 @@ private static JsonConverterFactory[] GetDefaultFactoryConverters() Add(JsonMetadataServices.UInt16Converter); Add(JsonMetadataServices.UInt32Converter); Add(JsonMetadataServices.UInt64Converter); +#if NET7_0_OR_GREATER + Add(JsonMetadataServices.Int128Converter); + Add(JsonMetadataServices.UInt128Converter); +#endif Add(JsonMetadataServices.UriConverter); Add(JsonMetadataServices.VersionConverter); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Converters.cs index 58c2f2a80461d..83a24191b8fe2 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Converters.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Converters.cs @@ -108,6 +108,23 @@ public static partial class JsonMetadataServices public static JsonConverter Int64Converter => s_int64Converter ??= new Int64Converter(); private static JsonConverter? s_int64Converter; +#if NET7_0_OR_GREATER + /// + /// Returns a instance that converts values. + /// + /// This API is for use by the output of the System.Text.Json source generator and should not be called directly. + public static JsonConverter Int128Converter => s_int128Converter ??= new Int128Converter(); + private static JsonConverter? s_int128Converter; + + /// + /// Returns a instance that converts values. + /// + /// This API is for use by the output of the System.Text.Json source generator and should not be called directly. + [CLSCompliant(false)] + public static JsonConverter UInt128Converter => s_uint128Converter ??= new UInt128Converter(); + private static JsonConverter? s_uint128Converter; +#endif + /// /// Returns a instance that converts values. /// @@ -171,6 +188,15 @@ public static partial class JsonMetadataServices public static JsonConverter ObjectConverter => s_objectConverter ??= new DefaultObjectConverter(); private static JsonConverter? s_objectConverter; +#if NETCOREAPP + /// + /// Returns a instance that converts values. + /// + /// This API is for use by the output of the System.Text.Json source generator and should not be called directly. + public static JsonConverter HalfConverter => s_halfConverter ??= new HalfConverter(); + private static JsonConverter? s_halfConverter; +#endif + /// /// Returns a instance that converts values. /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs index a2a65769868a6..a3cf416575cce 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs @@ -583,6 +583,13 @@ private bool NumberHandingIsApplicable() potentialNumberType == typeof(ushort) || potentialNumberType == typeof(uint) || potentialNumberType == typeof(ulong) || +#if NETCOREAPP + potentialNumberType == typeof(Half) || +#endif +#if NET7_0_OR_GREATER + potentialNumberType == typeof(Int128) || + potentialNumberType == typeof(UInt128) || +#endif potentialNumberType == JsonTypeInfo.ObjectType; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs index eb44014346731..3d8913f8d2c78 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs @@ -615,6 +615,9 @@ public static void ThrowFormatException(NumericType numericType) case NumericType.Int64: message = SR.FormatInt64; break; + case NumericType.Int128: + message = SR.FormatInt128; + break; case NumericType.UInt16: message = SR.FormatUInt16; break; @@ -624,6 +627,12 @@ public static void ThrowFormatException(NumericType numericType) case NumericType.UInt64: message = SR.FormatUInt64; break; + case NumericType.UInt128: + message = SR.FormatUInt128; + break; + case NumericType.Half: + message = SR.FormatHalf; + break; case NumericType.Single: message = SR.FormatSingle; break; @@ -740,9 +749,12 @@ internal enum NumericType Int16, Int32, Int64, + Int128, UInt16, UInt32, UInt64, + UInt128, + Half, Single, Double, Decimal diff --git a/src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Dictionary.NonStringKey.cs b/src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Dictionary.NonStringKey.cs index b6af85f4a2dfd..dfaff9130fa78 100644 --- a/src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Dictionary.NonStringKey.cs +++ b/src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Dictionary.NonStringKey.cs @@ -66,6 +66,11 @@ public static IEnumerable GetTestDictionaries() yield return WrapArgs(ushort.MaxValue, 1); yield return WrapArgs(uint.MaxValue, 1); yield return WrapArgs(ulong.MaxValue, 1); +#if NETCOREAPP + yield return WrapArgs(Half.MinValue, 1); + yield return WrapArgs(Int128.MinValue, 1); + yield return WrapArgs(UInt128.MaxValue, 1); +#endif static object[] WrapArgs(TKey key, TValue value, string? expectedJson = null) { @@ -334,7 +339,15 @@ public async Task TestEscapedValuesOnDeserialize(string escapedPropertyName, obj MyEnum.Bar, typeof(Dictionary) }, new object[] { @"\u0042\u0061\u0072\u002c\u0042\u0061\u007a", MyEnumFlags.Bar | MyEnumFlags.Baz, typeof(Dictionary) }, - new object[] { @"\u002b", '+', typeof(Dictionary) } + new object[] { @"\u002b", '+', typeof(Dictionary) }, +#if NETCOREAPP + new object[] { @"\u0033\u002e\u0031\u0032\u0035\u0065\u0034", + (Half)3.125e4, typeof(Dictionary) }, + new object[] { @"\u002D\u0031\u0037\u0030\u0031\u0034\u0031\u0031\u0038\u0033\u0034\u0036\u0030\u0034\u0036\u0039\u0032\u0033\u0031\u0037\u0033\u0031\u0036\u0038\u0037\u0033\u0030\u0033\u0037\u0031\u0035\u0038\u0038\u0034\u0031\u0030\u0035\u0037\u0032\u0038", + Int128.MinValue, typeof(Dictionary) }, + new object[] { @"\u0033\u0034\u0030\u0032\u0038\u0032\u0033\u0036\u0036\u0039\u0032\u0030\u0039\u0033\u0038\u0034\u0036\u0033\u0034\u0036\u0033\u0033\u0037\u0034\u0036\u0030\u0037\u0034\u0033\u0031\u0037\u0036\u0038\u0032\u0031\u0031\u0034\u0035\u0035", + UInt128.MaxValue, typeof(Dictionary) }, +#endif }; public class MyPublicClass { } diff --git a/src/libraries/System.Text.Json/tests/Common/JsonNumberTestData.cs b/src/libraries/System.Text.Json/tests/Common/JsonNumberTestData.cs index 384488485c980..d2d2591ade5ff 100644 --- a/src/libraries/System.Text.Json/tests/Common/JsonNumberTestData.cs +++ b/src/libraries/System.Text.Json/tests/Common/JsonNumberTestData.cs @@ -20,6 +20,11 @@ internal class JsonNumberTestData public static List Floats { get; set; } public static List Doubles { get; set; } public static List Decimals { get; set; } +#if NETCOREAPP + public static List Int128s { get; set; } + public static List UInt128s { get; set; } + public static List Halfs { get; set; } +#endif public static List NullableBytes { get; set; } public static List NullableSBytes { get; set; } @@ -32,6 +37,11 @@ internal class JsonNumberTestData public static List NullableFloats { get; set; } public static List NullableDoubles { get; set; } public static List NullableDecimals { get; set; } +#if NETCOREAPP + public static List NullableInt128s { get; set; } + public static List NullableUInt128s { get; set; } + public static List NullableHalfs { get; set; } +#endif public static byte[] JsonData { get; set; } @@ -237,6 +247,58 @@ static JsonNumberTestData() } #endregion +#if NETCOREAPP + #region generate Int128s + Int128s = new List + { + 0, + (Int128)long.MaxValue + 1, + (Int128)long.MinValue - 1, + Int128.MaxValue, + Int128.MinValue + }; + for (int i = 0; i < numberOfItems; i++) + { + Int128 value = random.Next(int.MinValue, int.MaxValue); + if (value < 0) + value += Int128.MinValue; + else + value += Int128.MaxValue; + Int128s.Add(value); + } + #endregion + + #region generate UInt128s + UInt128s = new List + { + (UInt128)ulong.MaxValue + 1, + UInt128.MaxValue, + UInt128.MinValue + }; + for (int i = 0; i < numberOfItems; i++) + { + UInt128 value = (UInt128)random.Next(int.MinValue, int.MaxValue); + UInt128s.Add(value); + } + #endregion + + #region generate Halfs + Halfs = new List + { + (Half)0.000, + (Half)1.1234e1, + (Half)(-1.1234e1), + Half.MaxValue, + Half.MinValue + }; + for (int i = 0; i < numberOfItems; i++) + { + Half value = JsonTestHelper.NextHalf(random); + Halfs.Add(value); + } + #endregion +#endif + #region generate the json var builder = new StringBuilder(); builder.Append("{"); @@ -319,6 +381,11 @@ static JsonNumberTestData() NullableFloats = new List(Floats.Select(num => (float?)num)); NullableDoubles = new List(Doubles.Select(num => (double?)num)); NullableDecimals = new List(Decimals.Select(num => (decimal?)num)); +#if NETCOREAPP + NullableInt128s = new List(Int128s.Select(num => (Int128?)num)); + NullableUInt128s = new List(UInt128s.Select(num => (UInt128?)num)); + NullableHalfs = new List(Halfs.Select(num => (Half?)num)); +#endif string jsonString = builder.ToString(); JsonData = Encoding.UTF8.GetBytes(jsonString); diff --git a/src/libraries/System.Text.Json/tests/Common/JsonTestHelper.cs b/src/libraries/System.Text.Json/tests/Common/JsonTestHelper.cs index 2428e979009c8..f15103a336045 100644 --- a/src/libraries/System.Text.Json/tests/Common/JsonTestHelper.cs +++ b/src/libraries/System.Text.Json/tests/Common/JsonTestHelper.cs @@ -20,6 +20,16 @@ internal static partial class JsonTestHelper public const string SingleFormatString = "G9"; #endif +#if NETCOREAPP + public static Half NextHalf(Random random) + { + double mantissa = (random.NextDouble() * 2.0) - 1.0; + double exponent = Math.Pow(2.0, random.Next(-15, 16)); + Half value = (Half)(mantissa * exponent); + return value; + } +#endif + public static float NextFloat(Random random) { double mantissa = (random.NextDouble() * 2.0) - 1.0; diff --git a/src/libraries/System.Text.Json/tests/Common/NumberHandlingTests.cs b/src/libraries/System.Text.Json/tests/Common/NumberHandlingTests.cs index 5220e177575dc..a8940cf6aec34 100644 --- a/src/libraries/System.Text.Json/tests/Common/NumberHandlingTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/NumberHandlingTests.cs @@ -63,6 +63,11 @@ public async Task Number_AsRootType_RoundTrip() await RunAsRootTypeTest(JsonNumberTestData.Floats); await RunAsRootTypeTest(JsonNumberTestData.Doubles); await RunAsRootTypeTest(JsonNumberTestData.Decimals); +#if NETCOREAPP + await RunAsRootTypeTest(JsonNumberTestData.Int128s); + await RunAsRootTypeTest(JsonNumberTestData.UInt128s); + await RunAsRootTypeTest(JsonNumberTestData.Halfs); +#endif await RunAsRootTypeTest(JsonNumberTestData.NullableBytes); await RunAsRootTypeTest(JsonNumberTestData.NullableSBytes); await RunAsRootTypeTest(JsonNumberTestData.NullableShorts); @@ -74,6 +79,11 @@ public async Task Number_AsRootType_RoundTrip() await RunAsRootTypeTest(JsonNumberTestData.NullableFloats); await RunAsRootTypeTest(JsonNumberTestData.NullableDoubles); await RunAsRootTypeTest(JsonNumberTestData.NullableDecimals); +#if NETCOREAPP + await RunAsRootTypeTest(JsonNumberTestData.NullableInt128s); + await RunAsRootTypeTest(JsonNumberTestData.NullableUInt128s); + await RunAsRootTypeTest(JsonNumberTestData.NullableHalfs); +#endif } private async Task RunAsRootTypeTest(List numbers) @@ -373,6 +383,11 @@ public async Task Number_AsCollectionElement_RoundTrip() await RunAsCollectionElementTest(JsonNumberTestData.Floats); await RunAsCollectionElementTest(JsonNumberTestData.Doubles); await RunAsCollectionElementTest(JsonNumberTestData.Decimals); +#if NETCOREAPP + await RunAsCollectionElementTest(JsonNumberTestData.Int128s); + await RunAsCollectionElementTest(JsonNumberTestData.UInt128s); + await RunAsCollectionElementTest(JsonNumberTestData.Halfs); +#endif // https://github.com/dotnet/runtime/issues/66220 if (!PlatformDetection.IsAppleMobile) @@ -388,6 +403,11 @@ public async Task Number_AsCollectionElement_RoundTrip() await RunAsCollectionElementTest(JsonNumberTestData.NullableFloats); await RunAsCollectionElementTest(JsonNumberTestData.NullableDoubles); await RunAsCollectionElementTest(JsonNumberTestData.NullableDecimals); +#if NETCOREAPP + await RunAsCollectionElementTest(JsonNumberTestData.NullableInt128s); + await RunAsCollectionElementTest(JsonNumberTestData.NullableUInt128s); + await RunAsCollectionElementTest(JsonNumberTestData.NullableHalfs); +#endif } } @@ -1080,6 +1100,11 @@ public async Task Number_RoundtripNull() await Perform_Number_RoundTripNull_Test(); await Perform_Number_RoundTripNull_Test(); await Perform_Number_RoundTripNull_Test(); +#if NETCOREAPP + await Perform_Number_RoundTripNull_Test(); + await Perform_Number_RoundTripNull_Test(); + await Perform_Number_RoundTripNull_Test(); +#endif } private async Task Perform_Number_RoundTripNull_Test() @@ -1105,6 +1130,11 @@ public async Task NullableNumber_RoundtripNull() await Perform_NullableNumber_RoundTripNull_Test(); await Perform_NullableNumber_RoundTripNull_Test(); await Perform_NullableNumber_RoundTripNull_Test(); +#if NETCOREAPP + await Perform_NullableNumber_RoundTripNull_Test(); + await Perform_NullableNumber_RoundTripNull_Test(); + await Perform_NullableNumber_RoundTripNull_Test(); +#endif } private async Task Perform_NullableNumber_RoundTripNull_Test() @@ -1133,6 +1163,11 @@ public async Task Disallow_ArbritaryStrings_On_AllowFloatingPointConstants() await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, s_optionsAllowFloatConstants)); await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, s_optionsAllowFloatConstants)); await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, s_optionsAllowFloatConstants)); +#if NETCOREAPP + await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, s_optionsAllowFloatConstants)); + await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, s_optionsAllowFloatConstants)); + await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, s_optionsAllowFloatConstants)); +#endif await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, s_optionsAllowFloatConstants)); await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, s_optionsAllowFloatConstants)); await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, s_optionsAllowFloatConstants)); @@ -1144,6 +1179,11 @@ public async Task Disallow_ArbritaryStrings_On_AllowFloatingPointConstants() await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, s_optionsAllowFloatConstants)); await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, s_optionsAllowFloatConstants)); await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, s_optionsAllowFloatConstants)); +#if NETCOREAPP + await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, s_optionsAllowFloatConstants)); + await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, s_optionsAllowFloatConstants)); + await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, s_optionsAllowFloatConstants)); +#endif } [Fact] diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NumberHandlingTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NumberHandlingTests.cs index 36f68c8441b3d..0fdcf44a0e88e 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NumberHandlingTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NumberHandlingTests.cs @@ -244,6 +244,68 @@ public NumberHandlingTests_Metadata() [JsonSerializable(typeof(List))] [JsonSerializable(typeof(Queue))] [JsonSerializable(typeof(ImmutableList))] +#if NETCOREAPP + [JsonSerializable(typeof(Int128))] + [JsonSerializable(typeof(Int128[]))] + [JsonSerializable(typeof(ConcurrentQueue))] + [JsonSerializable(typeof(GenericICollectionWrapper))] + [JsonSerializable(typeof(IEnumerable))] + [JsonSerializable(typeof(Collection))] + [JsonSerializable(typeof(HashSet))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(Queue))] + [JsonSerializable(typeof(ImmutableList))] + [JsonSerializable(typeof(UInt128))] + [JsonSerializable(typeof(UInt128[]))] + [JsonSerializable(typeof(ConcurrentQueue))] + [JsonSerializable(typeof(GenericICollectionWrapper))] + [JsonSerializable(typeof(IEnumerable))] + [JsonSerializable(typeof(Collection))] + [JsonSerializable(typeof(HashSet))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(Queue))] + [JsonSerializable(typeof(ImmutableList))] + [JsonSerializable(typeof(Half))] + [JsonSerializable(typeof(Half[]))] + [JsonSerializable(typeof(ConcurrentQueue))] + [JsonSerializable(typeof(GenericICollectionWrapper))] + [JsonSerializable(typeof(IEnumerable))] + [JsonSerializable(typeof(Collection))] + [JsonSerializable(typeof(HashSet))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(Queue))] + [JsonSerializable(typeof(ImmutableList))] + [JsonSerializable(typeof(Int128?))] + [JsonSerializable(typeof(Int128?[]))] + [JsonSerializable(typeof(ConcurrentQueue))] + [JsonSerializable(typeof(GenericICollectionWrapper))] + [JsonSerializable(typeof(IEnumerable))] + [JsonSerializable(typeof(Collection))] + [JsonSerializable(typeof(HashSet))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(Queue))] + [JsonSerializable(typeof(ImmutableList))] + [JsonSerializable(typeof(UInt128?))] + [JsonSerializable(typeof(UInt128?[]))] + [JsonSerializable(typeof(ConcurrentQueue))] + [JsonSerializable(typeof(GenericICollectionWrapper))] + [JsonSerializable(typeof(IEnumerable))] + [JsonSerializable(typeof(Collection))] + [JsonSerializable(typeof(HashSet))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(Queue))] + [JsonSerializable(typeof(ImmutableList))] + [JsonSerializable(typeof(Half?))] + [JsonSerializable(typeof(Half?[]))] + [JsonSerializable(typeof(ConcurrentQueue))] + [JsonSerializable(typeof(GenericICollectionWrapper))] + [JsonSerializable(typeof(IEnumerable))] + [JsonSerializable(typeof(Collection))] + [JsonSerializable(typeof(HashSet))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(Queue))] + [JsonSerializable(typeof(ImmutableList))] +#endif [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] @@ -632,6 +694,68 @@ public NumberHandlingTests_Default() [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] +#if NETCOREAPP + [JsonSerializable(typeof(Int128))] + [JsonSerializable(typeof(Int128[]))] + [JsonSerializable(typeof(ConcurrentQueue))] + [JsonSerializable(typeof(GenericICollectionWrapper))] + [JsonSerializable(typeof(IEnumerable))] + [JsonSerializable(typeof(Collection))] + [JsonSerializable(typeof(HashSet))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(Queue))] + [JsonSerializable(typeof(ImmutableList))] + [JsonSerializable(typeof(UInt128))] + [JsonSerializable(typeof(UInt128[]))] + [JsonSerializable(typeof(ConcurrentQueue))] + [JsonSerializable(typeof(GenericICollectionWrapper))] + [JsonSerializable(typeof(IEnumerable))] + [JsonSerializable(typeof(Collection))] + [JsonSerializable(typeof(HashSet))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(Queue))] + [JsonSerializable(typeof(ImmutableList))] + [JsonSerializable(typeof(Half))] + [JsonSerializable(typeof(Half[]))] + [JsonSerializable(typeof(ConcurrentQueue))] + [JsonSerializable(typeof(GenericICollectionWrapper))] + [JsonSerializable(typeof(IEnumerable))] + [JsonSerializable(typeof(Collection))] + [JsonSerializable(typeof(HashSet))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(Queue))] + [JsonSerializable(typeof(ImmutableList))] + [JsonSerializable(typeof(Int128?))] + [JsonSerializable(typeof(Int128?[]))] + [JsonSerializable(typeof(ConcurrentQueue))] + [JsonSerializable(typeof(GenericICollectionWrapper))] + [JsonSerializable(typeof(IEnumerable))] + [JsonSerializable(typeof(Collection))] + [JsonSerializable(typeof(HashSet))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(Queue))] + [JsonSerializable(typeof(ImmutableList))] + [JsonSerializable(typeof(UInt128?))] + [JsonSerializable(typeof(UInt128?[]))] + [JsonSerializable(typeof(ConcurrentQueue))] + [JsonSerializable(typeof(GenericICollectionWrapper))] + [JsonSerializable(typeof(IEnumerable))] + [JsonSerializable(typeof(Collection))] + [JsonSerializable(typeof(HashSet))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(Queue))] + [JsonSerializable(typeof(ImmutableList))] + [JsonSerializable(typeof(Half?))] + [JsonSerializable(typeof(Half?[]))] + [JsonSerializable(typeof(ConcurrentQueue))] + [JsonSerializable(typeof(GenericICollectionWrapper))] + [JsonSerializable(typeof(IEnumerable))] + [JsonSerializable(typeof(Collection))] + [JsonSerializable(typeof(HashSet))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(Queue))] + [JsonSerializable(typeof(ImmutableList))] +#endif [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonReaderTests.TryGet.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonReaderTests.TryGet.cs index 2c462acb0c8fb..97249dd55637e 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonReaderTests.TryGet.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonReaderTests.TryGet.cs @@ -1189,11 +1189,97 @@ void Test(ref Utf8JsonReader reader) } } +#if NET8_0_OR_GREATER + [Theory] + [MemberData(nameof(CopyString_JsonNumber_Utf8_TheoryData))] + public static void CopyString_JsonNumber_Utf8_SuccessPath(string jsonNumber, byte[] expectedOutputUtf8) + { + JsonTestHelper.AssertWithSingleAndMultiSegmentReader(jsonNumber, Test); + + void Test(ref Utf8JsonReader reader) + { + Assert.True(reader.Read()); + Span destination = new byte[40]; + int bytesWritten = reader.CopyString(destination); + Assert.Equal(expectedOutputUtf8.Length, bytesWritten); + AssertExtensions.SequenceEqual(expectedOutputUtf8, destination.Slice(0, bytesWritten)); + } + } + + [Theory] + [MemberData(nameof(CopyString_JsonNumber_Utf16_TheoryData))] + public static void CopyString_JsonNumber_Utf16_SuccessPath(string jsonNumber, char[] expectedOutputUtf16) + { + JsonTestHelper.AssertWithSingleAndMultiSegmentReader(jsonNumber, Test); + + void Test(ref Utf8JsonReader reader) + { + Assert.True(reader.Read()); + Span destination = new char[40]; + int charsWritten = reader.CopyString(destination); + Assert.Equal(expectedOutputUtf16.Length, charsWritten); + AssertExtensions.SequenceEqual(expectedOutputUtf16, destination.Slice(0, charsWritten)); + } + } + + public static IEnumerable CopyString_JsonNumber_Utf8_TheoryData() + => CopyString_JsonNumber_TheoryData(useUtf8: true); + + public static IEnumerable CopyString_JsonNumber_Utf16_TheoryData() + => CopyString_JsonNumber_TheoryData(useUtf8: false); + + private static IEnumerable CopyString_JsonNumber_TheoryData(bool useUtf8) + { + // Signed + yield return GetStringAndFormatFromNumber(int.MaxValue); + yield return GetStringAndFormatFromNumber(int.MinValue); + yield return GetStringAndFormatFromNumber(long.MaxValue); + yield return GetStringAndFormatFromNumber(long.MinValue); + yield return GetStringAndFormatFromNumber(Int128.MaxValue); + yield return GetStringAndFormatFromNumber(Int128.MinValue); + // Unsigned + yield return GetStringAndFormatFromNumber(uint.MaxValue); + yield return GetStringAndFormatFromNumber(uint.MinValue); + yield return GetStringAndFormatFromNumber(ulong.MaxValue); + yield return GetStringAndFormatFromNumber(ulong.MinValue); + yield return GetStringAndFormatFromNumber(UInt128.MaxValue); + yield return GetStringAndFormatFromNumber(UInt128.MinValue); + // Floating point + yield return GetStringAndFormatFromNumber(Half.MaxValue); + yield return GetStringAndFormatFromNumber(Half.MinValue); + yield return GetStringAndFormatFromNumber(float.MaxValue); + yield return GetStringAndFormatFromNumber(float.MinValue); + yield return GetStringAndFormatFromNumber(double.MaxValue); + yield return GetStringAndFormatFromNumber(double.MinValue); + + object[] GetStringAndFormatFromNumber(T number) + { + if (useUtf8) + { + IUtf8SpanFormattable numberAsUtf8SpanFormattable = Assert.IsAssignableFrom(number); + Span destination = stackalloc byte[40]; + bool formattedSuccessfully = numberAsUtf8SpanFormattable.TryFormat(destination, out int bytesWritten, format: default, provider: null); + Assert.True(formattedSuccessfully); + + return new object[] { number.ToString(), destination.Slice(0, bytesWritten).ToArray() }; + } + else + { + ISpanFormattable numberAsSpanFormattable = Assert.IsAssignableFrom(number); + Span destination = stackalloc char[40]; + bool formattedSuccessfully = numberAsSpanFormattable.TryFormat(destination, out int charsWritten, format: default, provider: null); + Assert.True(formattedSuccessfully); + + return new object[] { number.ToString(), destination.Slice(0, charsWritten).ToArray() }; + } + } + } +#endif + [Theory] [InlineData("null")] [InlineData("false")] [InlineData("true")] - [InlineData("42")] [InlineData("[]")] [InlineData("{}")] [InlineData("/* comment */ null", JsonCommentHandling.Allow)] From d42cf66d7612238ce2ddbe398f7d4397485db6bf Mon Sep 17 00:00:00 2001 From: David Cantu Date: Mon, 17 Jul 2023 12:46:59 -0500 Subject: [PATCH 2/9] Remove parsing limits on Read and move Number support of CopyString to an internal helper --- .../Text/Json/Reader/Utf8JsonReader.TryGet.cs | 16 ++++- .../Converters/Value/HalfConverter.cs | 63 ++++++++++++------- .../Converters/Value/Int128Converter.cs | 46 ++++++++++---- .../Converters/Value/UInt128Converter.cs | 45 +++++++++---- 4 files changed, 123 insertions(+), 47 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs index e7e88308beba8..7b58a24a9c4d7 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs @@ -68,11 +68,17 @@ namespace System.Text.Json /// The destination buffer is too small to hold the unescaped value. public readonly int CopyString(Span 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 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; @@ -126,11 +132,17 @@ namespace System.Text.Json /// The destination buffer is too small to hold the unescaped value. public readonly int CopyString(Span 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 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 unescapedSource; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs index 122d0a3875722..51a8c74b801e9 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs @@ -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 { - private const int MaxFormatLength = 16; - private const int MaxEscapedFormatLength = MaxFormatLength * JsonConstants.MaxExpansionFactorWhileEscaping; + private const int MaxFormatLength = 20; public HalfConverter() { @@ -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 byteBuffer = bufferLength <= JsonConstants.StackallocByteThreshold + ? stackalloc byte[JsonConstants.StackallocCharThreshold] + : (rentedByteBuffer = ArrayPool.Shared.Rent(bufferLength)); - Span 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 charBuffer = stackalloc char[MaxFormatLength]; - written = JsonReaderHelper.TranscodeHelper(buffer, charBuffer); - bool success = Half.TryParse(charBuffer.Slice(0, written), out result); + // Half.TryParse(ROS) is not available on .NET 7, only Half.TryParse(ROS); + // we need to transcode here instead of letting CopyValue do it for us because + // TryGetFloatingPointConstant only accepts ROS. + + Span 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 + { + if (rentedByteBuffer != null) + { + ArrayPool.Shared.Return(rentedByteBuffer); + } - return result; + if (rentedCharBuffer != null) + { + ArrayPool.Shared.Return(rentedCharBuffer); + } + } } private static void WriteCore(Utf8JsonWriter writer, Half value) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int128Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int128Converter.cs index 5350d997345c6..fbd10b92b6b0e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int128Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int128Converter.cs @@ -1,6 +1,7 @@ // 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 @@ -8,7 +9,6 @@ namespace System.Text.Json.Serialization.Converters internal sealed class Int128Converter : JsonPrimitiveConverter { private const int MaxFormatLength = 40; - private const int MaxEscapedFormatLength = MaxFormatLength * JsonConstants.MaxExpansionFactorWhileEscaping; public Int128Converter() { @@ -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 buffer = stackalloc byte[MaxFormatLength]; + byte[]? rentedBuffer = null; #else - Span 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 buffer = bufferLength <= JsonConstants.StackallocByteThreshold + ? stackalloc byte[JsonConstants.StackallocByteThreshold] + : (rentedBuffer = ArrayPool.Shared.Rent(bufferLength)); +#else + // Int128.TryParse(ROS) is not available on .NET 7, only Int128.TryParse(ROS). + Span buffer = bufferLength <= JsonConstants.StackallocCharThreshold + ? stackalloc char[JsonConstants.StackallocCharThreshold] + : (rentedBuffer = ArrayPool.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.Shared.Return(rentedBuffer); +#else + ArrayPool.Shared.Return(rentedBuffer); +#endif + } + } } private static void WriteCore(Utf8JsonWriter writer, Int128 value) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt128Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt128Converter.cs index 89deb45e6e755..17dbd7dd5e72b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt128Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt128Converter.cs @@ -1,6 +1,7 @@ // 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 @@ -8,7 +9,6 @@ namespace System.Text.Json.Serialization.Converters internal sealed class UInt128Converter : JsonPrimitiveConverter { private const int MaxFormatLength = 39; - private const int MaxEscapedFormatLength = MaxFormatLength * JsonConstants.MaxExpansionFactorWhileEscaping; public UInt128Converter() { @@ -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 buffer = stackalloc byte[MaxFormatLength]; + byte[]? rentedBuffer = null; #else - Span 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 buffer = bufferLength <= JsonConstants.StackallocByteThreshold + ? stackalloc byte[JsonConstants.StackallocByteThreshold] + : (rentedBuffer = ArrayPool.Shared.Rent(bufferLength)); +#else + // UInt128.TryParse(ROS) is not available on .NET 7, only UInt128.TryParse(ROS). + Span buffer = bufferLength <= JsonConstants.StackallocCharThreshold + ? stackalloc char[JsonConstants.StackallocCharThreshold] + : (rentedBuffer = ArrayPool.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.Shared.Return(rentedBuffer); +#else + ArrayPool.Shared.Return(rentedBuffer); +#endif + } + } } private static void WriteCore(Utf8JsonWriter writer, UInt128 value) From 8b71bf31db50419131da3799dbf6d7eda5f7b7fa Mon Sep 17 00:00:00 2001 From: David Cantu Date: Mon, 17 Jul 2023 12:47:34 -0500 Subject: [PATCH 3/9] Fix AllowNamedFloatingPointLiterals on Write for Half --- .../Converters/Value/HalfConverter.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs index 51a8c74b801e9..dff5a11cc4a5d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs @@ -158,6 +158,26 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, Half buffer[length - 1] = Quote; writer.WriteRawValue(buffer.Slice(0, length)); } + else if ((JsonNumberHandling.AllowNamedFloatingPointLiterals & handling) != 0) + { + + if (Half.IsNaN(value)) + { + writer.WriteNumberValueAsStringUnescaped(JsonConstants.NaNValue); + } + else if (Half.IsPositiveInfinity(value)) + { + writer.WriteNumberValueAsStringUnescaped(JsonConstants.PositiveInfinityValue); + } + else if (Half.IsNegativeInfinity(value)) + { + writer.WriteNumberValueAsStringUnescaped(JsonConstants.NegativeInfinityValue); + } + else + { + WriteCore(writer, value); + } + } else { WriteCore(writer, value); From 38483e59422c54bdc44651bc119fcc24bb69554b Mon Sep 17 00:00:00 2001 From: David Cantu Date: Mon, 17 Jul 2023 19:33:12 -0500 Subject: [PATCH 4/9] Specify InvariantCulture on TryParse and TryFormat Fix handling of floating-point literals on HalfConverter Remove CopyString tests related to Number support --- .../Converters/Value/HalfConverter.cs | 163 +++++++++++------- .../Converters/Value/Int128Converter.cs | 77 +++++---- .../Converters/Value/UInt128Converter.cs | 75 ++++---- .../tests/Common/NumberHandlingTests.cs | 62 ++++++- .../Utf8JsonReaderTests.TryGet.cs | 88 +--------- 5 files changed, 252 insertions(+), 213 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs index dff5a11cc4a5d..0604a36cceca0 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Diagnostics; +using System.Globalization; namespace System.Text.Json.Serialization.Converters { @@ -38,53 +39,38 @@ private static Half ReadCore(ref Utf8JsonReader reader) char[]? rentedCharBuffer = null; int bufferLength = reader.ValueLength; - try - { - Span byteBuffer = bufferLength <= JsonConstants.StackallocByteThreshold - ? stackalloc byte[JsonConstants.StackallocCharThreshold] - : (rentedByteBuffer = ArrayPool.Shared.Rent(bufferLength)); + Span byteBuffer = bufferLength <= JsonConstants.StackallocByteThreshold + ? stackalloc byte[JsonConstants.StackallocByteThreshold] + : (rentedByteBuffer = ArrayPool.Shared.Rent(bufferLength)); - int written = reader.CopyValue(byteBuffer); - byteBuffer = byteBuffer.Slice(0, written); - - if (reader.TokenType is JsonTokenType.String or JsonTokenType.PropertyName) - { - if (JsonReaderHelper.TryGetFloatingPointConstant(byteBuffer, out result)) - { - return result; - } - } + int written = reader.CopyValue(byteBuffer); + byteBuffer = byteBuffer.Slice(0, written); #if NET8_0_OR_GREATER - bool success = Half.TryParse(byteBuffer, out result); + bool success = TryParse(byteBuffer, out result); #else - // Half.TryParse(ROS) is not available on .NET 7, only Half.TryParse(ROS); - // we need to transcode here instead of letting CopyValue do it for us because - // TryGetFloatingPointConstant only accepts ROS. - - Span charBuffer = stackalloc char[MaxFormatLength]; - written = JsonReaderHelper.TranscodeHelper(byteBuffer, charBuffer); - bool success = Half.TryParse(charBuffer.Slice(0, written), out result); + // We need to transcode here instead of letting CopyValue do it for us because TryGetFloatingPointConstant only accepts ROS. + Span charBuffer = stackalloc char[MaxFormatLength]; + written = JsonReaderHelper.TranscodeHelper(byteBuffer, charBuffer); + bool success = TryParse(charBuffer, out result); #endif - if (!success) - { - ThrowHelper.ThrowFormatException(NumericType.Half); - } - - return result; + if (rentedByteBuffer != null) + { + ArrayPool.Shared.Return(rentedByteBuffer); } - finally + + if (rentedCharBuffer != null) { - if (rentedByteBuffer != null) - { - ArrayPool.Shared.Return(rentedByteBuffer); - } + ArrayPool.Shared.Return(rentedCharBuffer); + } - if (rentedCharBuffer != null) - { - ArrayPool.Shared.Return(rentedCharBuffer); - } + if (!success) + { + ThrowHelper.ThrowFormatException(NumericType.Half); } + + Debug.Assert(!Half.IsNaN(result) && !Half.IsInfinity(result)); + return result; } private static void WriteCore(Utf8JsonWriter writer, Half value) @@ -94,8 +80,7 @@ private static void WriteCore(Utf8JsonWriter writer, Half value) #else Span buffer = stackalloc char[MaxFormatLength]; #endif - bool formattedSuccessfully = value.TryFormat(buffer, out int written); - Debug.Assert(formattedSuccessfully); + Format(buffer, value, out int written); writer.WriteRawValue(buffer.Slice(0, written)); } @@ -112,8 +97,7 @@ internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, Half value #else Span buffer = stackalloc char[MaxFormatLength]; #endif - bool formattedSuccessfully = value.TryFormat(buffer, out int written); - Debug.Assert(formattedSuccessfully); + Format(buffer, value, out int written); writer.WritePropertyName(buffer.Slice(0, written)); } @@ -123,11 +107,16 @@ internal override Half ReadNumberWithCustomHandling(ref Utf8JsonReader reader, J { if ((JsonNumberHandling.AllowReadingFromString & handling) != 0) { + if (TryGetFloatingPointConstant(ref reader, out Half value)) + { + return value; + } + return ReadCore(ref reader); } else if ((JsonNumberHandling.AllowNamedFloatingPointLiterals & handling) != 0) { - if (!JsonReaderHelper.TryGetFloatingPointConstant(default, out Half value)) + if (!TryGetFloatingPointConstant(ref reader, out Half value)) { ThrowHelper.ThrowFormatException(NumericType.Half); } @@ -151,8 +140,7 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, Half Span buffer = stackalloc char[MaxFormatLength + 2]; #endif buffer[0] = Quote; - bool formattedSuccessfully = value.TryFormat(buffer.Slice(1), out int written); - Debug.Assert(formattedSuccessfully); + Format(buffer.Slice(1), value, out int written); int length = written + 2; buffer[length - 1] = Quote; @@ -160,28 +148,81 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, Half } else if ((JsonNumberHandling.AllowNamedFloatingPointLiterals & handling) != 0) { + WriteFloatingPointConstant(writer, value); + } + else + { + WriteCore(writer, value); + } + } - if (Half.IsNaN(value)) - { - writer.WriteNumberValueAsStringUnescaped(JsonConstants.NaNValue); - } - else if (Half.IsPositiveInfinity(value)) - { - writer.WriteNumberValueAsStringUnescaped(JsonConstants.PositiveInfinityValue); - } - else if (Half.IsNegativeInfinity(value)) - { - writer.WriteNumberValueAsStringUnescaped(JsonConstants.NegativeInfinityValue); - } - else - { - WriteCore(writer, value); - } + private static bool TryGetFloatingPointConstant(ref Utf8JsonReader reader, out Half value) + { + Span buffer = stackalloc byte[MaxFormatLength]; + int written = reader.CopyValue(buffer); + + return JsonReaderHelper.TryGetFloatingPointConstant(buffer.Slice(0, written), out value); + } + + private static void WriteFloatingPointConstant(Utf8JsonWriter writer, Half value) + { + if (Half.IsNaN(value)) + { + writer.WriteNumberValueAsStringUnescaped(JsonConstants.NaNValue); + } + else if (Half.IsPositiveInfinity(value)) + { + writer.WriteNumberValueAsStringUnescaped(JsonConstants.PositiveInfinityValue); + } + else if (Half.IsNegativeInfinity(value)) + { + writer.WriteNumberValueAsStringUnescaped(JsonConstants.NegativeInfinityValue); } else { WriteCore(writer, value); } } + + // Half.TryFormat/TryParse(ROS) are not available on .NET 7 + // we need to use Half.TryFormat/TryParse(ROS) in that case. + private static bool TryParse( +#if NET8_0_OR_GREATER + ReadOnlySpan buffer, +#else + ReadOnlySpan buffer, +#endif + out Half result) + { + bool success = Half.TryParse(buffer, CultureInfo.InvariantCulture, out result); + + // Half.TryParse is more lax with floating-point literals than other S.T.Json floating-point types + // e.g: it parses "naN" successfully. Only succeed with the exact match. +#if NET8_0_OR_GREATER + ReadOnlySpan NaN = JsonConstants.NaNValue; + ReadOnlySpan PositiveInfinity = JsonConstants.PositiveInfinityValue; + ReadOnlySpan NegativeInfinity = JsonConstants.NegativeInfinityValue; +#else + const string NaN = "NaN"; + const string PositiveInfinity = "Infinity"; + const string NegativeInfinity = "-Infinity"; +#endif + return success && + (!Half.IsNaN(result) || buffer.SequenceEqual(NaN)) && + (!Half.IsPositiveInfinity(result) || buffer.SequenceEqual(PositiveInfinity)) && + (!Half.IsNegativeInfinity(result) || buffer.SequenceEqual(NegativeInfinity)); + } + + private static void Format( +#if NET8_0_OR_GREATER + Span destination, +#else + Span destination, +#endif + Half value, out int written) + { + bool formattedSuccessfully = value.TryFormat(destination, out written, provider: CultureInfo.InvariantCulture); + Debug.Assert(formattedSuccessfully); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int128Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int128Converter.cs index fbd10b92b6b0e..da415abbb785c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int128Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int128Converter.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Diagnostics; +using System.Globalization; namespace System.Text.Json.Serialization.Converters { @@ -36,41 +37,32 @@ private static Int128 ReadCore(ref Utf8JsonReader reader) #if NET8_0_OR_GREATER byte[]? rentedBuffer = null; + Span buffer = bufferLength <= JsonConstants.StackallocByteThreshold + ? stackalloc byte[JsonConstants.StackallocByteThreshold] + : (rentedBuffer = ArrayPool.Shared.Rent(bufferLength)); #else char[]? rentedBuffer = null; + Span buffer = bufferLength <= JsonConstants.StackallocCharThreshold + ? stackalloc char[JsonConstants.StackallocCharThreshold] + : (rentedBuffer = ArrayPool.Shared.Rent(bufferLength)); #endif - try - { -#if NET8_0_OR_GREATER - Span buffer = bufferLength <= JsonConstants.StackallocByteThreshold - ? stackalloc byte[JsonConstants.StackallocByteThreshold] - : (rentedBuffer = ArrayPool.Shared.Rent(bufferLength)); -#else - // Int128.TryParse(ROS) is not available on .NET 7, only Int128.TryParse(ROS). - Span buffer = bufferLength <= JsonConstants.StackallocCharThreshold - ? stackalloc char[JsonConstants.StackallocCharThreshold] - : (rentedBuffer = ArrayPool.Shared.Rent(bufferLength)); -#endif - - int written = reader.CopyValue(buffer); - if (!Int128.TryParse(buffer.Slice(0, written), out Int128 result)) - { - ThrowHelper.ThrowFormatException(NumericType.Int128); - } - return result; + int written = reader.CopyValue(buffer); + if (!TryParse(buffer.Slice(0, written), out Int128 result)) + { + ThrowHelper.ThrowFormatException(NumericType.Int128); } - finally + + if (rentedBuffer != null) { - if (rentedBuffer != null) - { #if NET8_0_OR_GREATER - ArrayPool.Shared.Return(rentedBuffer); + ArrayPool.Shared.Return(rentedBuffer); #else - ArrayPool.Shared.Return(rentedBuffer); + ArrayPool.Shared.Return(rentedBuffer); #endif - } } + + return result; } private static void WriteCore(Utf8JsonWriter writer, Int128 value) @@ -80,8 +72,7 @@ private static void WriteCore(Utf8JsonWriter writer, Int128 value) #else Span buffer = stackalloc char[MaxFormatLength]; #endif - bool formattedSuccessfully = value.TryFormat(buffer, out int written); - Debug.Assert(formattedSuccessfully); + Format(buffer, value, out int written); writer.WriteRawValue(buffer.Slice(0, written)); } @@ -98,8 +89,7 @@ internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, Int128 val #else Span buffer = stackalloc char[MaxFormatLength]; #endif - bool formattedSuccessfully = value.TryFormat(buffer, out int bytesWritten); - Debug.Assert(formattedSuccessfully); + Format(buffer, value, out int written); writer.WritePropertyName(buffer); } @@ -126,10 +116,9 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, Int1 Span buffer = stackalloc char[MaxFormatLength + 2]; #endif buffer[0] = Quote; - bool formattedSuccessfully = value.TryFormat(buffer.Slice(1), out int bytesWritten); - Debug.Assert(formattedSuccessfully); + Format(buffer.Slice(1), value, out int written); - int length = bytesWritten + 2; + int length = written + 2; buffer[length - 1] = Quote; writer.WriteRawValue(buffer.Slice(0, length)); } @@ -138,5 +127,29 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, Int1 WriteCore(writer, value); } } + + // Int128.TryParse(ROS) is not available on .NET 7, only Int128.TryParse(ROS). + private static bool TryParse( +#if NET8_0_OR_GREATER + ReadOnlySpan buffer, +#else + ReadOnlySpan buffer, +#endif + out Int128 result) + { + return Int128.TryParse(buffer, CultureInfo.InvariantCulture, out result); + } + + private static void Format( +#if NET8_0_OR_GREATER + Span destination, +#else + Span destination, +#endif + Int128 value, out int written) + { + bool formattedSuccessfully = value.TryFormat(destination, out written, provider: CultureInfo.InvariantCulture); + Debug.Assert(formattedSuccessfully); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt128Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt128Converter.cs index 17dbd7dd5e72b..d1dd1fc05fc6e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt128Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt128Converter.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Diagnostics; +using System.Globalization; namespace System.Text.Json.Serialization.Converters { @@ -36,40 +37,31 @@ private static UInt128 ReadCore(ref Utf8JsonReader reader) #if NET8_0_OR_GREATER byte[]? rentedBuffer = null; + Span buffer = bufferLength <= JsonConstants.StackallocByteThreshold + ? stackalloc byte[JsonConstants.StackallocByteThreshold] + : (rentedBuffer = ArrayPool.Shared.Rent(bufferLength)); #else char[]? rentedBuffer = null; + Span buffer = bufferLength <= JsonConstants.StackallocCharThreshold + ? stackalloc char[JsonConstants.StackallocCharThreshold] + : (rentedBuffer = ArrayPool.Shared.Rent(bufferLength)); #endif - try + int written = reader.CopyValue(buffer); + if (!TryParse(buffer.Slice(0, written), out UInt128 result)) { -#if NET8_0_OR_GREATER - Span buffer = bufferLength <= JsonConstants.StackallocByteThreshold - ? stackalloc byte[JsonConstants.StackallocByteThreshold] - : (rentedBuffer = ArrayPool.Shared.Rent(bufferLength)); -#else - // UInt128.TryParse(ROS) is not available on .NET 7, only UInt128.TryParse(ROS). - Span buffer = bufferLength <= JsonConstants.StackallocCharThreshold - ? stackalloc char[JsonConstants.StackallocCharThreshold] - : (rentedBuffer = ArrayPool.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; + ThrowHelper.ThrowFormatException(NumericType.UInt128); } - finally + + if (rentedBuffer != null) { - if (rentedBuffer != null) - { #if NET8_0_OR_GREATER - ArrayPool.Shared.Return(rentedBuffer); + ArrayPool.Shared.Return(rentedBuffer); #else - ArrayPool.Shared.Return(rentedBuffer); + ArrayPool.Shared.Return(rentedBuffer); #endif - } } + + return result; } private static void WriteCore(Utf8JsonWriter writer, UInt128 value) @@ -79,8 +71,7 @@ private static void WriteCore(Utf8JsonWriter writer, UInt128 value) #else Span buffer = stackalloc char[MaxFormatLength]; #endif - bool formattedSuccessfully = value.TryFormat(buffer, out int written); - Debug.Assert(formattedSuccessfully); + Format(buffer, value, out int written); writer.WriteRawValue(buffer.Slice(0, written)); } @@ -97,8 +88,7 @@ internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, UInt128 va #else Span buffer = stackalloc char[MaxFormatLength]; #endif - bool formattedSuccessfully = value.TryFormat(buffer, out int bytesWritten); - Debug.Assert(formattedSuccessfully); + Format(buffer, value, out int written); writer.WritePropertyName(buffer); } @@ -125,10 +115,9 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, UInt Span buffer = stackalloc char[MaxFormatLength + 2]; #endif buffer[0] = Quote; - bool formattedSuccessfully = value.TryFormat(buffer.Slice(1), out int bytesWritten); - Debug.Assert(formattedSuccessfully); + Format(buffer.Slice(1), value, out int written); - int length = bytesWritten + 2; + int length = written + 2; buffer[length - 1] = Quote; writer.WriteRawValue(buffer.Slice(0, length)); } @@ -137,5 +126,29 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, UInt WriteCore(writer, value); } } + + // UInt128.TryParse(ROS) is not available on .NET 7, only UInt128.TryParse(ROS). + private static bool TryParse( +#if NET8_0_OR_GREATER + ReadOnlySpan buffer, +#else + ReadOnlySpan buffer, +#endif + out UInt128 result) + { + return UInt128.TryParse(buffer, CultureInfo.InvariantCulture, out result); + } + + private static void Format( +#if NET8_0_OR_GREATER + Span destination, +#else + Span destination, +#endif + UInt128 value, out int written) + { + bool formattedSuccessfully = value.TryFormat(destination, out written, provider: CultureInfo.InvariantCulture); + Debug.Assert(formattedSuccessfully); + } } } diff --git a/src/libraries/System.Text.Json/tests/Common/NumberHandlingTests.cs b/src/libraries/System.Text.Json/tests/Common/NumberHandlingTests.cs index a8940cf6aec34..f57cf20cdf0c0 100644 --- a/src/libraries/System.Text.Json/tests/Common/NumberHandlingTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/NumberHandlingTests.cs @@ -781,35 +781,57 @@ public async Task FloatingPointConstants_Pass() async Task PerformFloatingPointSerialization(string testString) { string testStringAsJson = $@"""{testString}"""; +#if NETCOREAPP + string testJson = @$"{{""HalfNumber"":{testStringAsJson},""FloatNumber"":{testStringAsJson},""DoubleNumber"":{testStringAsJson}}}"; +#else string testJson = @$"{{""FloatNumber"":{testStringAsJson},""DoubleNumber"":{testStringAsJson}}}"; +#endif StructWithNumbers obj; switch (testString) { case "NaN": obj = await Serializer.DeserializeWrapper(testJson, s_optionsAllowFloatConstants); +#if NETCOREAPP + Assert.Equal(Half.NaN, obj.HalfNumber); +#endif Assert.Equal(float.NaN, obj.FloatNumber); Assert.Equal(double.NaN, obj.DoubleNumber); obj = await Serializer.DeserializeWrapper(testJson, s_optionReadFromStr); +#if NETCOREAPP + Assert.Equal(Half.NaN, obj.HalfNumber); +#endif Assert.Equal(float.NaN, obj.FloatNumber); Assert.Equal(double.NaN, obj.DoubleNumber); break; case "Infinity": obj = await Serializer.DeserializeWrapper(testJson, s_optionsAllowFloatConstants); +#if NETCOREAPP + Assert.Equal(Half.PositiveInfinity, obj.HalfNumber); +#endif Assert.Equal(float.PositiveInfinity, obj.FloatNumber); Assert.Equal(double.PositiveInfinity, obj.DoubleNumber); obj = await Serializer.DeserializeWrapper(testJson, s_optionReadFromStr); +#if NETCOREAPP + Assert.Equal(Half.PositiveInfinity, obj.HalfNumber); +#endif Assert.Equal(float.PositiveInfinity, obj.FloatNumber); Assert.Equal(double.PositiveInfinity, obj.DoubleNumber); break; case "-Infinity": obj = await Serializer.DeserializeWrapper(testJson, s_optionsAllowFloatConstants); +#if NETCOREAPP + Assert.Equal(Half.NegativeInfinity, obj.HalfNumber); +#endif Assert.Equal(float.NegativeInfinity, obj.FloatNumber); Assert.Equal(double.NegativeInfinity, obj.DoubleNumber); obj = await Serializer.DeserializeWrapper(testJson, s_optionReadFromStr); +#if NETCOREAPP + Assert.Equal(Half.NegativeInfinity, obj.HalfNumber); +#endif Assert.Equal(float.NegativeInfinity, obj.FloatNumber); Assert.Equal(double.NegativeInfinity, obj.DoubleNumber); break; @@ -854,8 +876,13 @@ async Task PerformFloatingPointSerialization(string testString) public async Task FloatingPointConstants_Fail(string testString) { string testStringAsJson = $@"""{testString}"""; - - string testJson = @$"{{""FloatNumber"":{testStringAsJson}}}"; + string testJson; +#if NETCOREAPP + testJson = @$"{{""HalfNumber"":{testStringAsJson}}}"; + await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(testJson, s_optionsAllowFloatConstants)); + await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(testJson, s_optionReadFromStr)); +#endif + testJson = @$"{{""FloatNumber"":{testStringAsJson}}}"; await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(testJson, s_optionsAllowFloatConstants)); await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(testJson, s_optionReadFromStr)); @@ -867,6 +894,11 @@ public async Task FloatingPointConstants_Fail(string testString) [Fact] public async Task AllowFloatingPointConstants_WriteAsNumber_IfNotConstant() { +#if NETCOREAPP + Half half = (Half)1; + // Not written as "1" + Assert.Equal("1", await Serializer.SerializeWrapper(half, s_optionsAllowFloatConstants)); +#endif float @float = 1; // Not written as "1" Assert.Equal("1", await Serializer.SerializeWrapper(@float, s_optionsAllowFloatConstants)); @@ -882,6 +914,9 @@ public async Task AllowFloatingPointConstants_WriteAsNumber_IfNotConstant() [InlineData("-Infinity")] public async Task Unquoted_FloatingPointConstants_Read_Fail(string testString) { +#if NETCOREAPP + await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(testString, s_optionsAllowFloatConstants)); +#endif await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(testString, s_optionsAllowFloatConstants)); await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(testString, s_optionReadFromStr)); await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(testString, s_optionReadFromStrAllowFloatConstants)); @@ -889,6 +924,9 @@ public async Task Unquoted_FloatingPointConstants_Read_Fail(string testString) public struct StructWithNumbers { +#if NETCOREAPP + public Half HalfNumber { get; set; } +#endif public float FloatNumber { get; set; } public double DoubleNumber { get; set; } } @@ -947,6 +985,12 @@ public async Task FloatingPointConstants_IncompatibleNumber() await AssertFloatingPointIncompatible_Fails(); await AssertFloatingPointIncompatible_Fails(); await AssertFloatingPointIncompatible_Fails(); +#if NETCOREAPP + await AssertFloatingPointIncompatible_Fails(); + await AssertFloatingPointIncompatible_Fails(); + await AssertFloatingPointIncompatible_Fails(); + await AssertFloatingPointIncompatible_Fails(); +#endif } private async Task AssertFloatingPointIncompatible_Fails() @@ -987,6 +1031,14 @@ public async Task UnsupportedFormats() await AssertUnsupportedFormatThrows(); await AssertUnsupportedFormatThrows(); await AssertUnsupportedFormatThrows(); +#if NETCOREAPP + await AssertUnsupportedFormatThrows(); + await AssertUnsupportedFormatThrows(); + await AssertUnsupportedFormatThrows(); + await AssertUnsupportedFormatThrows(); + await AssertUnsupportedFormatThrows(); + await AssertUnsupportedFormatThrows(); +#endif } private async Task AssertUnsupportedFormatThrows() @@ -1029,6 +1081,12 @@ public async Task EscapingTest() await PerformEscapingTest(JsonNumberTestData.Floats, options); await PerformEscapingTest(JsonNumberTestData.Doubles, options); await PerformEscapingTest(JsonNumberTestData.Decimals, options); +#if NETCOREAPP + await PerformEscapingTest(JsonNumberTestData.Int128s, options); + await PerformEscapingTest(JsonNumberTestData.UInt128s, options); + await PerformEscapingTest(JsonNumberTestData.Halfs, options); +#endif + } private async Task PerformEscapingTest(List numbers, JsonSerializerOptions options) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonReaderTests.TryGet.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonReaderTests.TryGet.cs index 97249dd55637e..2c462acb0c8fb 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonReaderTests.TryGet.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonReaderTests.TryGet.cs @@ -1189,97 +1189,11 @@ void Test(ref Utf8JsonReader reader) } } -#if NET8_0_OR_GREATER - [Theory] - [MemberData(nameof(CopyString_JsonNumber_Utf8_TheoryData))] - public static void CopyString_JsonNumber_Utf8_SuccessPath(string jsonNumber, byte[] expectedOutputUtf8) - { - JsonTestHelper.AssertWithSingleAndMultiSegmentReader(jsonNumber, Test); - - void Test(ref Utf8JsonReader reader) - { - Assert.True(reader.Read()); - Span destination = new byte[40]; - int bytesWritten = reader.CopyString(destination); - Assert.Equal(expectedOutputUtf8.Length, bytesWritten); - AssertExtensions.SequenceEqual(expectedOutputUtf8, destination.Slice(0, bytesWritten)); - } - } - - [Theory] - [MemberData(nameof(CopyString_JsonNumber_Utf16_TheoryData))] - public static void CopyString_JsonNumber_Utf16_SuccessPath(string jsonNumber, char[] expectedOutputUtf16) - { - JsonTestHelper.AssertWithSingleAndMultiSegmentReader(jsonNumber, Test); - - void Test(ref Utf8JsonReader reader) - { - Assert.True(reader.Read()); - Span destination = new char[40]; - int charsWritten = reader.CopyString(destination); - Assert.Equal(expectedOutputUtf16.Length, charsWritten); - AssertExtensions.SequenceEqual(expectedOutputUtf16, destination.Slice(0, charsWritten)); - } - } - - public static IEnumerable CopyString_JsonNumber_Utf8_TheoryData() - => CopyString_JsonNumber_TheoryData(useUtf8: true); - - public static IEnumerable CopyString_JsonNumber_Utf16_TheoryData() - => CopyString_JsonNumber_TheoryData(useUtf8: false); - - private static IEnumerable CopyString_JsonNumber_TheoryData(bool useUtf8) - { - // Signed - yield return GetStringAndFormatFromNumber(int.MaxValue); - yield return GetStringAndFormatFromNumber(int.MinValue); - yield return GetStringAndFormatFromNumber(long.MaxValue); - yield return GetStringAndFormatFromNumber(long.MinValue); - yield return GetStringAndFormatFromNumber(Int128.MaxValue); - yield return GetStringAndFormatFromNumber(Int128.MinValue); - // Unsigned - yield return GetStringAndFormatFromNumber(uint.MaxValue); - yield return GetStringAndFormatFromNumber(uint.MinValue); - yield return GetStringAndFormatFromNumber(ulong.MaxValue); - yield return GetStringAndFormatFromNumber(ulong.MinValue); - yield return GetStringAndFormatFromNumber(UInt128.MaxValue); - yield return GetStringAndFormatFromNumber(UInt128.MinValue); - // Floating point - yield return GetStringAndFormatFromNumber(Half.MaxValue); - yield return GetStringAndFormatFromNumber(Half.MinValue); - yield return GetStringAndFormatFromNumber(float.MaxValue); - yield return GetStringAndFormatFromNumber(float.MinValue); - yield return GetStringAndFormatFromNumber(double.MaxValue); - yield return GetStringAndFormatFromNumber(double.MinValue); - - object[] GetStringAndFormatFromNumber(T number) - { - if (useUtf8) - { - IUtf8SpanFormattable numberAsUtf8SpanFormattable = Assert.IsAssignableFrom(number); - Span destination = stackalloc byte[40]; - bool formattedSuccessfully = numberAsUtf8SpanFormattable.TryFormat(destination, out int bytesWritten, format: default, provider: null); - Assert.True(formattedSuccessfully); - - return new object[] { number.ToString(), destination.Slice(0, bytesWritten).ToArray() }; - } - else - { - ISpanFormattable numberAsSpanFormattable = Assert.IsAssignableFrom(number); - Span destination = stackalloc char[40]; - bool formattedSuccessfully = numberAsSpanFormattable.TryFormat(destination, out int charsWritten, format: default, provider: null); - Assert.True(formattedSuccessfully); - - return new object[] { number.ToString(), destination.Slice(0, charsWritten).ToArray() }; - } - } - } -#endif - [Theory] [InlineData("null")] [InlineData("false")] [InlineData("true")] + [InlineData("42")] [InlineData("[]")] [InlineData("{}")] [InlineData("/* comment */ null", JsonCommentHandling.Allow)] From 2c5cb22b08b1839ded167146e0216d9a06eb45b4 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Mon, 17 Jul 2023 20:16:01 -0500 Subject: [PATCH 5/9] Add test for invalid number input format --- .../tests/Common/NumberHandlingTests.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/libraries/System.Text.Json/tests/Common/NumberHandlingTests.cs b/src/libraries/System.Text.Json/tests/Common/NumberHandlingTests.cs index f57cf20cdf0c0..a11f47454701e 100644 --- a/src/libraries/System.Text.Json/tests/Common/NumberHandlingTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/NumberHandlingTests.cs @@ -1057,6 +1057,27 @@ private async Task AssertUnsupportedFormatThrows() } } +#if NETCOREAPP + [Fact] + public async Task InvalidNumberFormatThrows() + { + await AssertInvalidNumberFormatThrows("170141183460469231731687303715884105728"); // MaxValue + 1 + await AssertInvalidNumberFormatThrows("-170141183460469231731687303715884105729"); // MaxValue - 1 + await AssertInvalidNumberFormatThrows("3.14"); + await AssertInvalidNumberFormatThrows("340282366920938463463374607431768211456"); // MaxValue + 1 + await AssertInvalidNumberFormatThrows("3.14"); + await AssertInvalidNumberFormatThrows("-1"); + await AssertInvalidNumberFormatThrows("65520"); + await AssertInvalidNumberFormatThrows("-65520"); + } + + private async Task AssertInvalidNumberFormatThrows(string testString) + { + await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(testString)); + await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper($@"""{testString}""", s_optionReadFromStr)); + } +#endif + [Fact] public async Task EscapingTest() { From 22bc1f79a87bd4f1668cf57dd61ddb9c5d8991f1 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Mon, 17 Jul 2023 23:48:23 -0500 Subject: [PATCH 6/9] Fix net6.0 build error about missing Half.TryParse overload --- .../Text/Json/Serialization/Converters/Value/HalfConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs index 0604a36cceca0..a30c65f854e83 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs @@ -194,7 +194,7 @@ private static void WriteFloatingPointConstant(Utf8JsonWriter writer, Half value #endif out Half result) { - bool success = Half.TryParse(buffer, CultureInfo.InvariantCulture, out result); + bool success = Half.TryParse(buffer, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out result); // Half.TryParse is more lax with floating-point literals than other S.T.Json floating-point types // e.g: it parses "naN" successfully. Only succeed with the exact match. From 43fe8a8fc7642a1268e80b0433aebb92b049ee09 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Tue, 18 Jul 2023 10:41:40 -0500 Subject: [PATCH 7/9] Move rentedCharBuffer logic to TryParse helper --- .../Converters/Value/HalfConverter.cs | 51 ++++++++----------- 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs index a30c65f854e83..85bb9ae2a201d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs @@ -36,7 +36,6 @@ private static Half ReadCore(ref Utf8JsonReader reader) Half result; byte[]? rentedByteBuffer = null; - char[]? rentedCharBuffer = null; int bufferLength = reader.ValueLength; Span byteBuffer = bufferLength <= JsonConstants.StackallocByteThreshold @@ -46,24 +45,12 @@ private static Half ReadCore(ref Utf8JsonReader reader) int written = reader.CopyValue(byteBuffer); byteBuffer = byteBuffer.Slice(0, written); -#if NET8_0_OR_GREATER bool success = TryParse(byteBuffer, out result); -#else - // We need to transcode here instead of letting CopyValue do it for us because TryGetFloatingPointConstant only accepts ROS. - Span charBuffer = stackalloc char[MaxFormatLength]; - written = JsonReaderHelper.TranscodeHelper(byteBuffer, charBuffer); - bool success = TryParse(charBuffer, out result); -#endif if (rentedByteBuffer != null) { ArrayPool.Shared.Return(rentedByteBuffer); } - if (rentedCharBuffer != null) - { - ArrayPool.Shared.Return(rentedCharBuffer); - } - if (!success) { ThrowHelper.ThrowFormatException(NumericType.Half); @@ -186,31 +173,33 @@ private static void WriteFloatingPointConstant(Utf8JsonWriter writer, Half value // Half.TryFormat/TryParse(ROS) are not available on .NET 7 // we need to use Half.TryFormat/TryParse(ROS) in that case. - private static bool TryParse( + private static bool TryParse(ReadOnlySpan buffer, out Half result) + { #if NET8_0_OR_GREATER - ReadOnlySpan buffer, + bool success = Half.TryParse(buffer, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out result); #else - ReadOnlySpan buffer, + char[]? rentedCharBuffer = null; + + Span charBuffer = buffer.Length <= JsonConstants.StackallocCharThreshold + ? stackalloc char[JsonConstants.StackallocCharThreshold] + : (rentedCharBuffer = ArrayPool.Shared.Rent(buffer.Length)); + + int written = JsonReaderHelper.TranscodeHelper(buffer, charBuffer); + + bool success = Half.TryParse(charBuffer, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out result); + + if (rentedCharBuffer != null) + { + ArrayPool.Shared.Return(rentedCharBuffer); + } #endif - out Half result) - { - bool success = Half.TryParse(buffer, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out result); // Half.TryParse is more lax with floating-point literals than other S.T.Json floating-point types // e.g: it parses "naN" successfully. Only succeed with the exact match. -#if NET8_0_OR_GREATER - ReadOnlySpan NaN = JsonConstants.NaNValue; - ReadOnlySpan PositiveInfinity = JsonConstants.PositiveInfinityValue; - ReadOnlySpan NegativeInfinity = JsonConstants.NegativeInfinityValue; -#else - const string NaN = "NaN"; - const string PositiveInfinity = "Infinity"; - const string NegativeInfinity = "-Infinity"; -#endif return success && - (!Half.IsNaN(result) || buffer.SequenceEqual(NaN)) && - (!Half.IsPositiveInfinity(result) || buffer.SequenceEqual(PositiveInfinity)) && - (!Half.IsNegativeInfinity(result) || buffer.SequenceEqual(NegativeInfinity)); + (!Half.IsNaN(result) || buffer.SequenceEqual(JsonConstants.NaNValue)) && + (!Half.IsPositiveInfinity(result) || buffer.SequenceEqual(JsonConstants.PositiveInfinityValue)) && + (!Half.IsNegativeInfinity(result) || buffer.SequenceEqual(JsonConstants.NegativeInfinityValue)); } private static void Format( From 02e83b387eff6287dcd2af4faa017c4fd71fd7a1 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Tue, 18 Jul 2023 12:08:40 -0500 Subject: [PATCH 8/9] Address feedback --- .../Text/Json/Serialization/Converters/Value/HalfConverter.cs | 4 ++-- .../System.Text.Json/tests/Common/NumberHandlingTests.cs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs index 85bb9ae2a201d..c2bee78a5fa67 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs @@ -171,13 +171,13 @@ private static void WriteFloatingPointConstant(Utf8JsonWriter writer, Half value } } - // Half.TryFormat/TryParse(ROS) are not available on .NET 7 - // we need to use Half.TryFormat/TryParse(ROS) in that case. private static bool TryParse(ReadOnlySpan buffer, out Half result) { #if NET8_0_OR_GREATER bool success = Half.TryParse(buffer, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out result); #else + // Half.TryFormat/TryParse(ROS) are not available on .NET 7 + // we need to use Half.TryFormat/TryParse(ROS) in that case. char[]? rentedCharBuffer = null; Span charBuffer = buffer.Length <= JsonConstants.StackallocCharThreshold diff --git a/src/libraries/System.Text.Json/tests/Common/NumberHandlingTests.cs b/src/libraries/System.Text.Json/tests/Common/NumberHandlingTests.cs index a11f47454701e..109123b92a009 100644 --- a/src/libraries/System.Text.Json/tests/Common/NumberHandlingTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/NumberHandlingTests.cs @@ -1045,6 +1045,7 @@ private async Task AssertUnsupportedFormatThrows() { string[] testCases = new[] { + "01", // leading zeroes "$123.46", // Currency "100.00 %", // Percent "1234,57", // Fixed point From 8c3e14320fb9e6159f1fde74d4b1db402c801582 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Tue, 18 Jul 2023 12:11:08 -0500 Subject: [PATCH 9/9] Disable test for OSX --- .../System.Text.Json/tests/Common/NumberHandlingTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libraries/System.Text.Json/tests/Common/NumberHandlingTests.cs b/src/libraries/System.Text.Json/tests/Common/NumberHandlingTests.cs index 109123b92a009..2a2e2dd38dc27 100644 --- a/src/libraries/System.Text.Json/tests/Common/NumberHandlingTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/NumberHandlingTests.cs @@ -873,6 +873,7 @@ async Task PerformFloatingPointSerialization(string testString) [InlineData("\u0020Inf\u0069ni\u0074y")] // " Infinity" [InlineData("\u002BInf\u0069nity")] // "+Infinity" #pragma warning restore xUnit1025 + [ActiveIssue("https://github.com/dotnet/runtime/issues/89094", TestPlatforms.OSX)] public async Task FloatingPointConstants_Fail(string testString) { string testStringAsJson = $@"""{testString}""";