From e2c04e07c9fb4132c52c9a2f676fdfeee209cd5b Mon Sep 17 00:00:00 2001 From: =?utf8?q?David=20Cant=C3=BA?= Date: Tue, 18 Jul 2023 14:29:40 -0500 Subject: [PATCH] JSON: Add support for Int128, UInt128 and Half (#88962) * JSON: Add support for Int128, UInt128 and Half and add Number support for Utf8JsonReader.CopyString(...) * Remove parsing limits on Read and move Number support of CopyString to an internal helper * Fix AllowNamedFloatingPointLiterals on Write for Half * Specify InvariantCulture on TryParse and TryFormat Fix handling of floating-point literals on HalfConverter Remove CopyString tests related to Number support * Add test for invalid number input format * Fix net6.0 build error about missing Half.TryParse overload * Move rentedCharBuffer logic to TryParse helper * Address feedback * Disable test for OSX --- .../gen/Helpers/KnownTypeSymbols.cs | 9 + .../gen/JsonSourceGenerator.Parser.cs | 3 + .../ref/System.Text.Json.netcoreapp.cs | 7 + .../System.Text.Json/src/Resources/Strings.resx | 9 + .../System.Text.Json/src/System.Text.Json.csproj | 6 + .../System/Text/Json/Reader/JsonReaderHelper.cs | 33 ++++ .../Text/Json/Reader/Utf8JsonReader.TryGet.cs | 16 ++ .../Converters/Value/CharConverter.cs | 5 + .../Converters/Value/HalfConverter.cs | 217 +++++++++++++++++++++ .../Converters/Value/Int128Converter.cs | 155 +++++++++++++++ .../Converters/Value/UInt128Converter.cs | 154 +++++++++++++++ .../DefaultJsonTypeInfoResolver.Converters.cs | 7 +- .../Metadata/JsonMetadataServices.Converters.cs | 26 +++ .../Serialization/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 | 125 +++++++++++- .../Serialization/NumberHandlingTests.cs | 124 ++++++++++++ 20 files changed, 1003 insertions(+), 4 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 b590574..75967e7 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 @@ namespace System.Text.Json.SourceGeneration 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 cc5e703..2499321 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -1699,6 +1699,9 @@ namespace System.Text.Json.SourceGeneration 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 8c26294..847da90 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 31b1224..8e1da07 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 624bd3c..5765b6c 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -346,11 +346,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 9db9e80..9cdcc0d 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 @@ namespace System.Text.Json 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 91c0bfc..7b58a24 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 @@ -73,6 +73,14 @@ namespace System.Text.Json 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; if (ValueIsEscaped) @@ -129,6 +137,14 @@ namespace System.Text.Json 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; 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 b3caa32..8c19ce1 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 @@ namespace System.Text.Json.Serialization.Converters 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 0000000..c2bee78 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs @@ -0,0 +1,217 @@ +// 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; +using System.Globalization; + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class HalfConverter : JsonPrimitiveConverter + { + private const int MaxFormatLength = 20; + + 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; + + byte[]? rentedByteBuffer = null; + int bufferLength = reader.ValueLength; + + Span byteBuffer = bufferLength <= JsonConstants.StackallocByteThreshold + ? stackalloc byte[JsonConstants.StackallocByteThreshold] + : (rentedByteBuffer = ArrayPool.Shared.Rent(bufferLength)); + + int written = reader.CopyValue(byteBuffer); + byteBuffer = byteBuffer.Slice(0, written); + + bool success = TryParse(byteBuffer, out result); + if (rentedByteBuffer != null) + { + ArrayPool.Shared.Return(rentedByteBuffer); + } + + if (!success) + { + ThrowHelper.ThrowFormatException(NumericType.Half); + } + + Debug.Assert(!Half.IsNaN(result) && !Half.IsInfinity(result)); + 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 + Format(buffer, value, out int written); + 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 + Format(buffer, value, out int written); + 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) + { + if (TryGetFloatingPointConstant(ref reader, out Half value)) + { + return value; + } + + return ReadCore(ref reader); + } + else if ((JsonNumberHandling.AllowNamedFloatingPointLiterals & handling) != 0) + { + if (!TryGetFloatingPointConstant(ref reader, 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; + Format(buffer.Slice(1), value, out int written); + + int length = written + 2; + buffer[length - 1] = Quote; + writer.WriteRawValue(buffer.Slice(0, length)); + } + else if ((JsonNumberHandling.AllowNamedFloatingPointLiterals & handling) != 0) + { + WriteFloatingPointConstant(writer, value); + } + 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); + } + } + + 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 + ? 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 + + // 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. + return success && + (!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( +#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 new file mode 100644 index 0000000..da415ab --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int128Converter.cs @@ -0,0 +1,155 @@ +// 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; +using System.Globalization; + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class Int128Converter : JsonPrimitiveConverter + { + private const int MaxFormatLength = 40; + + 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) + { + int bufferLength = reader.ValueLength; + +#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 + + int written = reader.CopyValue(buffer); + if (!TryParse(buffer.Slice(0, written), out Int128 result)) + { + ThrowHelper.ThrowFormatException(NumericType.Int128); + } + + if (rentedBuffer != null) + { +#if NET8_0_OR_GREATER + ArrayPool.Shared.Return(rentedBuffer); +#else + ArrayPool.Shared.Return(rentedBuffer); +#endif + } + + 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 + Format(buffer, value, out int written); + 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 + Format(buffer, value, out int written); + 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; + Format(buffer.Slice(1), value, out int written); + + int length = written + 2; + buffer[length - 1] = Quote; + writer.WriteRawValue(buffer.Slice(0, length)); + } + else + { + 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 new file mode 100644 index 0000000..d1dd1fc --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt128Converter.cs @@ -0,0 +1,154 @@ +// 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; +using System.Globalization; + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class UInt128Converter : JsonPrimitiveConverter + { + private const int MaxFormatLength = 39; + + 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) + { + int bufferLength = reader.ValueLength; + +#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 + int written = reader.CopyValue(buffer); + if (!TryParse(buffer.Slice(0, written), out UInt128 result)) + { + ThrowHelper.ThrowFormatException(NumericType.UInt128); + } + + if (rentedBuffer != null) + { +#if NET8_0_OR_GREATER + ArrayPool.Shared.Return(rentedBuffer); +#else + ArrayPool.Shared.Return(rentedBuffer); +#endif + } + + 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 + Format(buffer, value, out int written); + 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 + Format(buffer, value, out int written); + 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; + Format(buffer.Slice(1), value, out int written); + + int length = written + 2; + buffer[length - 1] = Quote; + writer.WriteRawValue(buffer.Slice(0, length)); + } + else + { + 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/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 7516c3f..ed50066 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 @@ namespace System.Text.Json.Serialization.Metadata 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 @@ namespace System.Text.Json.Serialization.Metadata #if NETCOREAPP Add(JsonMetadataServices.DateOnlyConverter); Add(JsonMetadataServices.TimeOnlyConverter); + Add(JsonMetadataServices.HalfConverter); #endif Add(JsonMetadataServices.DoubleConverter); Add(JsonMetadataServices.DecimalConverter); @@ -73,6 +74,10 @@ namespace System.Text.Json.Serialization.Metadata 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 58c2f2a..83a2419 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 @@ namespace System.Text.Json.Serialization.Metadata 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 @@ namespace System.Text.Json.Serialization.Metadata 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 b6c1029..dabd626 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 @@ namespace System.Text.Json.Serialization.Metadata 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 eb44014..3d8913f 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 @@ namespace System.Text.Json 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 @@ namespace System.Text.Json 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 @@ namespace System.Text.Json 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 b6af85f..dfaff91 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 @@ namespace System.Text.Json.Serialization.Tests 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 @@ namespace System.Text.Json.Serialization.Tests 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 3844884..d2d2591 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 @@ namespace System.Text.Json.Tests 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 @@ namespace System.Text.Json.Tests 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 @@ namespace System.Text.Json.Tests } #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 @@ namespace System.Text.Json.Tests 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 23b66ab..f641a4a 100644 --- a/src/libraries/System.Text.Json/tests/Common/JsonTestHelper.cs +++ b/src/libraries/System.Text.Json/tests/Common/JsonTestHelper.cs @@ -23,6 +23,16 @@ namespace System.Text.Json 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 5220e17..2a2e2dd 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 @@ namespace System.Text.Json.Serialization.Tests 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 @@ namespace System.Text.Json.Serialization.Tests 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 @@ namespace System.Text.Json.Serialization.Tests 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 @@ namespace System.Text.Json.Serialization.Tests 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 } } @@ -761,35 +781,57 @@ namespace System.Text.Json.Serialization.Tests 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; @@ -831,11 +873,17 @@ namespace System.Text.Json.Serialization.Tests [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}"""; - - 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)); @@ -847,6 +895,11 @@ namespace System.Text.Json.Serialization.Tests [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)); @@ -862,6 +915,9 @@ namespace System.Text.Json.Serialization.Tests [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)); @@ -869,6 +925,9 @@ namespace System.Text.Json.Serialization.Tests public struct StructWithNumbers { +#if NETCOREAPP + public Half HalfNumber { get; set; } +#endif public float FloatNumber { get; set; } public double DoubleNumber { get; set; } } @@ -927,6 +986,12 @@ namespace System.Text.Json.Serialization.Tests 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() @@ -967,12 +1032,21 @@ namespace System.Text.Json.Serialization.Tests 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() { string[] testCases = new[] { + "01", // leading zeroes "$123.46", // Currency "100.00 %", // Percent "1234,57", // Fixed point @@ -985,6 +1059,27 @@ namespace System.Text.Json.Serialization.Tests } } +#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() { @@ -1009,6 +1104,12 @@ namespace System.Text.Json.Serialization.Tests 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) @@ -1080,6 +1181,11 @@ namespace System.Text.Json.Serialization.Tests 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 +1211,11 @@ namespace System.Text.Json.Serialization.Tests 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 +1244,11 @@ namespace System.Text.Json.Serialization.Tests 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 +1260,11 @@ namespace System.Text.Json.Serialization.Tests 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 36f68c8..0fdcf44 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 @@ namespace System.Text.Json.SourceGeneration.Tests [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 @@ namespace System.Text.Json.SourceGeneration.Tests [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))] -- 2.7.4