|
// 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.Buffers.Text;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
namespace System.Text.Json
{
internal static partial class JsonHelpers
{
/// <summary>
/// Returns the unescaped span for the given reader.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ReadOnlySpan<byte> GetUnescapedSpan(this scoped ref Utf8JsonReader reader)
{
Debug.Assert(reader.TokenType is JsonTokenType.String or JsonTokenType.PropertyName);
ReadOnlySpan<byte> span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
return reader.ValueIsEscaped ? JsonReaderHelper.GetUnescapedSpan(span) : span;
}
/// <summary>
/// Attempts to perform a Read() operation and optionally checks that the full JSON value has been buffered.
/// The reader will be reset if the operation fails.
/// </summary>
/// <param name="reader">The reader to advance.</param>
/// <param name="requiresReadAhead">If reading a partial payload, read ahead to ensure that the full JSON value has been buffered.</param>
/// <returns>True if the reader has been buffered with all required data.</returns>
// AggressiveInlining used since this method is on a hot path and short. The AdvanceWithReadAhead method should not be inlined.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryAdvanceWithOptionalReadAhead(this scoped ref Utf8JsonReader reader, bool requiresReadAhead)
{
// No read-ahead necessary if we're at the final block of JSON data.
bool readAhead = requiresReadAhead && !reader.IsFinalBlock;
return readAhead ? TryAdvanceWithReadAhead(ref reader) : reader.Read();
}
/// <summary>
/// Attempts to read ahead to the next root-level JSON value, if it exists.
/// </summary>
public static bool TryAdvanceToNextRootLevelValueWithOptionalReadAhead(this scoped ref Utf8JsonReader reader, bool requiresReadAhead, out bool isAtEndOfStream)
{
Debug.Assert(reader.AllowMultipleValues, "only supported by readers that support multiple values.");
Debug.Assert(reader.CurrentDepth == 0, "should only invoked for top-level values.");
Utf8JsonReader checkpoint = reader;
if (!reader.Read())
{
// If the reader didn't return any tokens and it's the final block,
// then there are no other JSON values to be read.
isAtEndOfStream = reader.IsFinalBlock;
reader = checkpoint;
return false;
}
// We found another JSON value, read ahead accordingly.
isAtEndOfStream = false;
if (requiresReadAhead && !reader.IsFinalBlock)
{
// Perform full read-ahead to ensure the full JSON value has been buffered.
reader = checkpoint;
return TryAdvanceWithReadAhead(ref reader);
}
return true;
}
private static bool TryAdvanceWithReadAhead(scoped ref Utf8JsonReader reader)
{
// When we're reading ahead we always have to save the state
// as we don't know if the next token is a start object or array.
Utf8JsonReader restore = reader;
if (!reader.Read())
{
return false;
}
// Perform the actual read-ahead.
JsonTokenType tokenType = reader.TokenType;
if (tokenType is JsonTokenType.StartObject or JsonTokenType.StartArray)
{
// Attempt to skip to make sure we have all the data we need.
bool complete = reader.TrySkipPartial();
// We need to restore the state in all cases as we need to be positioned back before
// the current token to either attempt to skip again or to actually read the value.
reader = restore;
if (!complete)
{
// Couldn't read to the end of the object, exit out to get more data in the buffer.
return false;
}
// Success, requeue the reader to the start token.
reader.ReadWithVerify();
Debug.Assert(tokenType == reader.TokenType);
}
return true;
}
#if !NET
/// <summary>
/// Returns <see langword="true"/> if <paramref name="value"/> is a valid Unicode scalar
/// value, i.e., is in [ U+0000..U+D7FF ], inclusive; or [ U+E000..U+10FFFF ], inclusive.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsValidUnicodeScalar(uint value)
{
// By XORing the incoming value with 0xD800, surrogate code points
// are moved to the range [ U+0000..U+07FF ], and all valid scalar
// values are clustered into the single range [ U+0800..U+10FFFF ],
// which allows performing a single fast range check.
return IsInRangeInclusive(value ^ 0xD800U, 0x800U, 0x10FFFFU);
}
#endif
/// <summary>
/// Returns <see langword="true"/> if <paramref name="value"/> is between
/// <paramref name="lowerBound"/> and <paramref name="upperBound"/>, inclusive.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsInRangeInclusive(uint value, uint lowerBound, uint upperBound)
=> (value - lowerBound) <= (upperBound - lowerBound);
/// <summary>
/// Returns <see langword="true"/> if <paramref name="value"/> is between
/// <paramref name="lowerBound"/> and <paramref name="upperBound"/>, inclusive.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsInRangeInclusive(int value, int lowerBound, int upperBound)
=> (uint)(value - lowerBound) <= (uint)(upperBound - lowerBound);
/// <summary>
/// Returns <see langword="true"/> if <paramref name="value"/> is between
/// <paramref name="lowerBound"/> and <paramref name="upperBound"/>, inclusive.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsInRangeInclusive(long value, long lowerBound, long upperBound)
=> (ulong)(value - lowerBound) <= (ulong)(upperBound - lowerBound);
/// <summary>
/// Returns <see langword="true"/> if <paramref name="value"/> is between
/// <paramref name="lowerBound"/> and <paramref name="upperBound"/>, inclusive.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsInRangeInclusive(JsonTokenType value, JsonTokenType lowerBound, JsonTokenType upperBound)
=> (value - lowerBound) <= (upperBound - lowerBound);
/// <summary>
/// Returns <see langword="true"/> if <paramref name="value"/> is in the range [0..9].
/// Otherwise, returns <see langword="false"/>.
/// </summary>
public static bool IsDigit(byte value) => (uint)(value - '0') <= '9' - '0';
/// <summary>
/// Perform a Read() with a Debug.Assert verifying the reader did not return false.
/// This should be called when the Read() return value is not used, such as non-Stream cases where there is only one buffer.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReadWithVerify(this ref Utf8JsonReader reader)
{
bool result = reader.Read();
Debug.Assert(result);
}
/// <summary>
/// Performs a TrySkip() with a Debug.Assert verifying the reader did not return false.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void SkipWithVerify(this ref Utf8JsonReader reader)
{
bool success = reader.TrySkipPartial(reader.CurrentDepth);
Debug.Assert(success, "The skipped value should have already been buffered.");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TrySkipPartial(this ref Utf8JsonReader reader)
{
return reader.TrySkipPartial(reader.CurrentDepth);
}
/// <summary>
/// Calls Encoding.UTF8.GetString that supports netstandard.
/// </summary>
/// <param name="bytes">The utf8 bytes to convert.</param>
/// <returns></returns>
public static string Utf8GetString(ReadOnlySpan<byte> bytes)
{
#if NET
return Encoding.UTF8.GetString(bytes);
#else
if (bytes.Length == 0)
{
return string.Empty;
}
unsafe
{
fixed (byte* bytesPtr = bytes)
{
return Encoding.UTF8.GetString(bytesPtr, bytes.Length);
}
}
#endif
}
public static bool TryLookupUtf8Key<TValue>(
this Dictionary<string, TValue> dictionary,
ReadOnlySpan<byte> utf8Key,
[MaybeNullWhen(false)] out TValue result)
{
#if NET9_0_OR_GREATER
Debug.Assert(dictionary.Comparer is IAlternateEqualityComparer<ReadOnlySpan<char>, string>);
Dictionary<string, TValue>.AlternateLookup<ReadOnlySpan<char>> spanLookup =
dictionary.GetAlternateLookup<ReadOnlySpan<char>>();
char[]? rentedBuffer = null;
Span<char> charBuffer = utf8Key.Length <= JsonConstants.StackallocCharThreshold ?
stackalloc char[JsonConstants.StackallocCharThreshold] :
(rentedBuffer = ArrayPool<char>.Shared.Rent(utf8Key.Length));
int charsWritten = Encoding.UTF8.GetChars(utf8Key, charBuffer);
Span<char> decodedKey = charBuffer[0..charsWritten];
bool success = spanLookup.TryGetValue(decodedKey, out result);
if (rentedBuffer != null)
{
decodedKey.Clear();
ArrayPool<char>.Shared.Return(rentedBuffer);
}
return success;
#else
string key = Utf8GetString(utf8Key);
return dictionary.TryGetValue(key, out result);
#endif
}
/// <summary>
/// Emulates Dictionary(IEnumerable{KeyValuePair}) on netstandard.
/// </summary>
public static Dictionary<TKey, TValue> CreateDictionaryFromCollection<TKey, TValue>(
IEnumerable<KeyValuePair<TKey, TValue>> collection,
IEqualityComparer<TKey> comparer)
where TKey : notnull
{
#if !NET
var dictionary = new Dictionary<TKey, TValue>(comparer);
foreach (KeyValuePair<TKey, TValue> item in collection)
{
dictionary.Add(item.Key, item.Value);
}
return dictionary;
#else
return new Dictionary<TKey, TValue>(collection: collection, comparer);
#endif
}
public static bool IsFinite(double value)
{
#if NET
return double.IsFinite(value);
#else
return !(double.IsNaN(value) || double.IsInfinity(value));
#endif
}
public static bool IsFinite(float value)
{
#if NET
return float.IsFinite(value);
#else
return !(float.IsNaN(value) || float.IsInfinity(value));
#endif
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ValidateInt32MaxArrayLength(uint length)
{
if (length > 0X7FEFFFFF) // prior to .NET 6, max array length for sizeof(T) != 1 (size == 1 is larger)
{
ThrowHelper.ThrowOutOfMemoryException(length);
}
}
#if !NET8_0_OR_GREATER
public static bool HasAllSet(this BitArray bitArray)
{
for (int i = 0; i < bitArray.Count; i++)
{
if (!bitArray[i])
{
return false;
}
}
return true;
}
#endif
/// <summary>
/// Gets a Regex instance for recognizing integer representations of enums.
/// </summary>
public static readonly Regex IntegerRegex = CreateIntegerRegex();
private const string IntegerRegexPattern = @"^\s*(?:\+|\-)?[0-9]+\s*$";
private const int IntegerRegexTimeoutMs = 200;
#if NET
[GeneratedRegex(IntegerRegexPattern, RegexOptions.None, matchTimeoutMilliseconds: IntegerRegexTimeoutMs)]
private static partial Regex CreateIntegerRegex();
#else
private static Regex CreateIntegerRegex() => new(IntegerRegexPattern, RegexOptions.Compiled, TimeSpan.FromMilliseconds(IntegerRegexTimeoutMs));
#endif
/// <summary>
/// Compares two valid UTF-8 encoded JSON numbers for decimal equality.
/// </summary>
public static bool AreEqualJsonNumbers(ReadOnlySpan<byte> left, ReadOnlySpan<byte> right)
{
Debug.Assert(left.Length > 0 && right.Length > 0);
ParseNumber(left,
out bool leftIsNegative,
out ReadOnlySpan<byte> leftIntegral,
out ReadOnlySpan<byte> leftFractional,
out int leftExponent);
ParseNumber(right,
out bool rightIsNegative,
out ReadOnlySpan<byte> rightIntegral,
out ReadOnlySpan<byte> rightFractional,
out int rightExponent);
int nDigits;
if (leftIsNegative != rightIsNegative ||
leftExponent != rightExponent ||
(nDigits = (leftIntegral.Length + leftFractional.Length)) !=
rightIntegral.Length + rightFractional.Length)
{
return false;
}
// Need to check that the concatenated integral and fractional parts are equal;
// break each representation into three parts such that their lengths exactly match.
ReadOnlySpan<byte> leftFirst;
ReadOnlySpan<byte> leftMiddle;
ReadOnlySpan<byte> leftLast;
ReadOnlySpan<byte> rightFirst;
ReadOnlySpan<byte> rightMiddle;
ReadOnlySpan<byte> rightLast;
int diff = leftIntegral.Length - rightIntegral.Length;
switch (diff)
{
case < 0:
leftFirst = leftIntegral;
leftMiddle = leftFractional.Slice(0, -diff);
leftLast = leftFractional.Slice(-diff);
int rightOffset = rightIntegral.Length + diff;
rightFirst = rightIntegral.Slice(0, rightOffset);
rightMiddle = rightIntegral.Slice(rightOffset);
rightLast = rightFractional;
break;
case 0:
leftFirst = leftIntegral;
leftMiddle = default;
leftLast = leftFractional;
rightFirst = rightIntegral;
rightMiddle = default;
rightLast = rightFractional;
break;
case > 0:
int leftOffset = leftIntegral.Length - diff;
leftFirst = leftIntegral.Slice(0, leftOffset);
leftMiddle = leftIntegral.Slice(leftOffset);
leftLast = leftFractional;
rightFirst = rightIntegral;
rightMiddle = rightFractional.Slice(0, diff);
rightLast = rightFractional.Slice(diff);
break;
}
Debug.Assert(leftFirst.Length == rightFirst.Length);
Debug.Assert(leftMiddle.Length == rightMiddle.Length);
Debug.Assert(leftLast.Length == rightLast.Length);
return leftFirst.SequenceEqual(rightFirst) &&
leftMiddle.SequenceEqual(rightMiddle) &&
leftLast.SequenceEqual(rightLast);
static void ParseNumber(
ReadOnlySpan<byte> span,
out bool isNegative,
out ReadOnlySpan<byte> integral,
out ReadOnlySpan<byte> 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 * <integral + fractional> * 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<byte> intg;
ReadOnlySpan<byte> 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<byte> 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<byte> 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
}
}
}
}
}
|