diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx
index b8791ea8fb171a..dc9458c6d705bb 100644
--- a/src/libraries/System.Text.Json/src/Resources/Strings.resx
+++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx
@@ -246,6 +246,9 @@
The requested operation requires an element of type '{0}', but the target element has type '{1}'.
+
+ The exponent value in the specified JSON number is too large.
+
Cannot add callbacks to the 'Modifiers' property after the resolver has been used for the first time.
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs
index f5d2ff099e7233..0a08875c7c562f 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.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.Text;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
@@ -1241,8 +1242,7 @@ internal static bool DeepEquals(JsonElement left, JsonElement right)
return true;
case JsonValueKind.Number:
- // JSON numbers are equal if their raw representations are equal.
- return left.GetRawValue().Span.SequenceEqual(right.GetRawValue().Span);
+ return JsonHelpers.AreEqualJsonNumbers(left.GetRawValue().Span, right.GetRawValue().Span);
case JsonValueKind.String:
if (right.ValueIsEscaped)
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs
index 63e2e177f49eb3..3933b9ee623dc4 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Buffers;
+using System.Buffers.Text;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
@@ -260,5 +261,242 @@ public static bool HasAllSet(this BitArray bitArray)
#else
private static Regex CreateIntegerRegex() => new(IntegerRegexPattern, RegexOptions.Compiled, TimeSpan.FromMilliseconds(IntegerRegexTimeoutMs));
#endif
+
+ ///
+ /// Compares two valid UTF-8 encoded JSON numbers for decimal equality.
+ ///
+ public static bool AreEqualJsonNumbers(ReadOnlySpan left, ReadOnlySpan right)
+ {
+ Debug.Assert(left.Length > 0 && right.Length > 0);
+
+ ParseNumber(left,
+ out bool leftIsNegative,
+ out ReadOnlySpan leftIntegral,
+ out ReadOnlySpan leftFractional,
+ out int leftExponent);
+
+ ParseNumber(right,
+ out bool rightIsNegative,
+ out ReadOnlySpan rightIntegral,
+ out ReadOnlySpan rightFractional,
+ out int rightExponent);
+
+ int nDigits;
+ if (leftIsNegative != rightIsNegative ||
+ leftExponent != rightExponent ||
+ (nDigits = (leftIntegral.Length + leftFractional.Length)) !=
+ rightIntegral.Length + rightFractional.Length)
+ {
+ return false;
+ }
+
+ if (leftIntegral.Length == rightIntegral.Length)
+ {
+ return leftIntegral.SequenceEqual(rightIntegral) &&
+ leftFractional.SequenceEqual(rightFractional);
+ }
+
+ // There is differentiation in the integral and fractional lengths,
+ // concatenate both into singular buffers and compare them.
+ scoped Span leftDigits;
+ scoped Span rightDigits;
+ byte[]? rentedLeftBuffer;
+ byte[]? rentedRightBuffer;
+
+ if (nDigits <= JsonConstants.StackallocByteThreshold)
+ {
+ leftDigits = stackalloc byte[JsonConstants.StackallocByteThreshold];
+ rightDigits = stackalloc byte[JsonConstants.StackallocByteThreshold];
+ rentedLeftBuffer = rentedRightBuffer = null;
+ }
+ else
+ {
+ leftDigits = (rentedLeftBuffer = ArrayPool.Shared.Rent(nDigits));
+ rightDigits = (rentedRightBuffer = ArrayPool.Shared.Rent(nDigits));
+ }
+
+ leftIntegral.CopyTo(leftDigits);
+ leftFractional.CopyTo(leftDigits.Slice(leftIntegral.Length));
+ rightIntegral.CopyTo(rightDigits);
+ rightFractional.CopyTo(rightDigits.Slice(rightIntegral.Length));
+
+ bool result = leftDigits.Slice(0, nDigits).SequenceEqual(rightDigits.Slice(0, nDigits));
+
+ if (rentedLeftBuffer != null)
+ {
+ Debug.Assert(rentedRightBuffer != null);
+ rentedLeftBuffer.AsSpan(0, nDigits).Clear();
+ rentedRightBuffer.AsSpan(0, nDigits).Clear();
+ ArrayPool.Shared.Return(rentedLeftBuffer);
+ ArrayPool.Shared.Return(rentedRightBuffer);
+ }
+
+ return result;
+
+ static void ParseNumber(
+ ReadOnlySpan span,
+ out bool isNegative,
+ out ReadOnlySpan integral,
+ out ReadOnlySpan fractional,
+ out int exponent)
+ {
+ // Parses a JSON number into its integral, fractional, and exponent parts.
+ // The returned components use a normal-form decimal representation:
+ //
+ // Number := sign * * 10^exponent
+ //
+ // where integral and fractional are sequences of digits whose concatenation
+ // represents the significand of the number without leading or trailing zeros.
+ // Two such normal-form numbers are treated as equal if and only if they have
+ // equal signs, significands, and exponents.
+
+ bool neg;
+ ReadOnlySpan intg;
+ ReadOnlySpan frac;
+ int exp;
+
+ Debug.Assert(span.Length > 0);
+
+ if (span[0] == '-')
+ {
+ neg = true;
+ span = span.Slice(1);
+ }
+ else
+ {
+ Debug.Assert(char.IsDigit((char)span[0]), "leading plus not allowed in valid JSON numbers.");
+ neg = false;
+ }
+
+ int i = span.IndexOfAny((byte)'.', (byte)'e', (byte)'E');
+ if (i < 0)
+ {
+ intg = span;
+ frac = default;
+ exp = 0;
+ goto Normalize;
+ }
+
+ intg = span.Slice(0, i);
+
+ if (span[i] == '.')
+ {
+ span = span.Slice(i + 1);
+ i = span.IndexOfAny((byte)'e', (byte)'E');
+ if (i < 0)
+ {
+ frac = span;
+ exp = 0;
+ goto Normalize;
+ }
+
+ frac = span.Slice(0, i);
+ }
+ else
+ {
+ frac = default;
+ }
+
+ Debug.Assert(span[i] is (byte)'e' or (byte)'E');
+ if (!Utf8Parser.TryParse(span.Slice(i + 1), out exp, out _))
+ {
+ Debug.Assert(span.Length >= 10);
+ ThrowHelper.ThrowArgumentOutOfRangeException_JsonNumberExponentTooLarge(nameof(exponent));
+ }
+
+ Normalize: // Calculates the normal form of the number.
+
+ if (IndexOfFirstTrailingZero(frac) is >= 0 and int iz)
+ {
+ // Trim trailing zeros from the fractional part.
+ // e.g. 3.1400 -> 3.14
+ frac = frac.Slice(0, iz);
+ }
+
+ if (intg[0] == '0')
+ {
+ Debug.Assert(intg.Length == 1, "Leading zeros not permitted in JSON numbers.");
+
+ if (IndexOfLastLeadingZero(frac) is >= 0 and int lz)
+ {
+ // Trim leading zeros from the fractional part
+ // and update the exponent accordingly.
+ // e.g. 0.000123 -> 0.123e-3
+ frac = frac.Slice(lz + 1);
+ exp -= lz + 1;
+ }
+
+ // Normalize "0" to the empty span.
+ intg = default;
+ }
+
+ if (frac.IsEmpty && IndexOfFirstTrailingZero(intg) is >= 0 and int fz)
+ {
+ // There is no fractional part, trim trailing zeros from
+ // the integral part and increase the exponent accordingly.
+ // e.g. 1000 -> 1e3
+ exp += intg.Length - fz;
+ intg = intg.Slice(0, fz);
+ }
+
+ // Normalize the exponent by subtracting the length of the fractional part.
+ // e.g. 3.14 -> 314e-2
+ exp -= frac.Length;
+
+ if (intg.IsEmpty && frac.IsEmpty)
+ {
+ // Normalize zero representations.
+ neg = false;
+ exp = 0;
+ }
+
+ // Copy to out parameters.
+ isNegative = neg;
+ integral = intg;
+ fractional = frac;
+ exponent = exp;
+
+ static int IndexOfLastLeadingZero(ReadOnlySpan span)
+ {
+#if NET
+ int firstNonZero = span.IndexOfAnyExcept((byte)'0');
+ return firstNonZero < 0 ? span.Length - 1 : firstNonZero - 1;
+#else
+ for (int i = 0; i < span.Length; i++)
+ {
+ if (span[i] != '0')
+ {
+ return i - 1;
+ }
+ }
+
+ return span.Length - 1;
+#endif
+ }
+
+ static int IndexOfFirstTrailingZero(ReadOnlySpan span)
+ {
+#if NET
+ int lastNonZero = span.LastIndexOfAnyExcept((byte)'0');
+ return lastNonZero == span.Length - 1 ? -1 : lastNonZero + 1;
+#else
+ if (span.IsEmpty)
+ {
+ return -1;
+ }
+
+ for (int i = span.Length - 1; i >= 0; i--)
+ {
+ if (span[i] != '0')
+ {
+ return i == span.Length - 1 ? -1 : i + 1;
+ }
+ }
+
+ return 0;
+#endif
+ }
+ }
+ }
}
}
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 6976d42b967bc9..fb1786be551dae 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
@@ -36,6 +36,12 @@ public static void ThrowArgumentOutOfRangeException_MaxDepthMustBePositive(strin
throw GetArgumentOutOfRangeException(parameterName, SR.MaxDepthMustBePositive);
}
+ [DoesNotReturn]
+ public static void ThrowArgumentOutOfRangeException_JsonNumberExponentTooLarge(string parameterName)
+ {
+ throw GetArgumentOutOfRangeException(parameterName, SR.JsonNumberExponentTooLarge);
+ }
+
private static ArgumentOutOfRangeException GetArgumentOutOfRangeException(string parameterName, string message)
{
return new ArgumentOutOfRangeException(parameterName, message);
diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs
index 94505ff194149b..29ae9662eedd4b 100644
--- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs
+++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs
@@ -380,6 +380,104 @@ public static void DeepEqualsPrimitiveType()
JsonNodeTests.AssertNotDeepEqual(JsonValue.Create(10), JsonValue.Create("10"));
}
+ [Theory]
+ [InlineData("-0.0", "0")]
+ [InlineData("0", "0.0000e4")]
+ [InlineData("0", "0.0000e-4")]
+ [InlineData("1", "1.0")]
+ [InlineData("1", "1e0")]
+ [InlineData("1", "1.0000")]
+ [InlineData("1", "1.0000e0")]
+ [InlineData("1", "0.10000e1")]
+ [InlineData("1", "10.0000e-1")]
+ [InlineData("10001", "1.0001e4")]
+ [InlineData("10001e-3", "1.0001e1")]
+ [InlineData("1", "0.1e1")]
+ [InlineData("0.1", "1e-1")]
+ [InlineData("0.001", "1e-3")]
+ [InlineData("1e9", "1000000000")]
+ [InlineData("11", "1.100000000e1")]
+ [InlineData("3.141592653589793", "3141592653589793E-15")]
+ [InlineData("0.000000000000000000000000000000000000000001", "1e-42")]
+ [InlineData("1000000000000000000000000000000000000000000", "1e42")]
+ [InlineData("-1.1e3", "-1100")]
+ [InlineData("79228162514264337593543950336", "792281625142643375935439503360e-1")] // decimal.MaxValue + 1
+ [InlineData("79228162514.264337593543950336", "792281625142643375935439503360e-19")]
+ [InlineData("1.75e+300", "1.75E+300")] // Variations in exponent casing
+ [InlineData( // > 256 digits
+ "1.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+ "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+ "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+ "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001" ,
+
+ "100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+ "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+ "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+ "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001" + "E-512")]
+ public static void DeepEqualsNumericType(string leftStr, string rightStr)
+ {
+ JsonNode left = JsonNode.Parse(leftStr);
+ JsonNode right = JsonNode.Parse(rightStr);
+
+ JsonNodeTests.AssertDeepEqual(left, right);
+ }
+
+ [Theory]
+ [InlineData("0", "1")]
+ [InlineData("1", "-1")]
+ [InlineData("1.1", "-1.1")]
+ [InlineData("1.1e5", "-1.1e5")]
+ [InlineData("0", "1e-1024")]
+ [InlineData("1", "0.1")]
+ [InlineData("1", "1.1")]
+ [InlineData("1", "1e1")]
+ [InlineData("1", "1.00001")]
+ [InlineData("1", "1.0000e1")]
+ [InlineData("1", "0.1000e-1")]
+ [InlineData("1", "10.0000e-2")]
+ [InlineData("10001", "1.0001e3")]
+ [InlineData("10001e-3", "1.0001e2")]
+ [InlineData("1", "0.1e2")]
+ [InlineData("0.1", "1e-2")]
+ [InlineData("0.001", "1e-4")]
+ [InlineData("1e9", "1000000001")]
+ [InlineData("11", "1.100000001e1")]
+ [InlineData("0.000000000000000000000000000000000000000001", "1e-43")]
+ [InlineData("1000000000000000000000000000000000000000000", "1e43")]
+ [InlineData("-1.1e3", "-1100.1")]
+ [InlineData("79228162514264337593543950336", "7922816251426433759354395033600e-1")] // decimal.MaxValue + 1
+ [InlineData("79228162514.264337593543950336", "7922816251426433759354395033601e-19")]
+ [InlineData("1.75e+300", "1.75E+301")] // Variations in exponent casing
+ [InlineData("1e2147483647", "1e-2147483648")] // int.MaxValue, int.MinValue exponents
+ [InlineData( // > 256 digits
+ "1.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+ "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+ "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+ "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001",
+
+ "100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+ "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+ "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+ "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003" + "E-512")]
+ public static void NotDeepEqualsNumericType(string leftStr, string rightStr)
+ {
+ JsonNode left = JsonNode.Parse(leftStr);
+ JsonNode right = JsonNode.Parse(rightStr);
+
+ JsonNodeTests.AssertNotDeepEqual(left, right);
+ }
+
+ [Theory]
+ [InlineData(int.MinValue - 1L)]
+ [InlineData(int.MaxValue + 1L)]
+ [InlineData(long.MinValue)]
+ [InlineData(long.MaxValue)]
+ public static void DeepEquals_ExponentExceedsInt32_ThrowsArgumentOutOfRangeException(long exponent)
+ {
+ JsonNode node = JsonNode.Parse($"1e{exponent}");
+ Assert.Throws(() => JsonNode.DeepEquals(node, node));
+ }
+
[Fact]
public static void DeepEqualsJsonElement()
{