| | | 1 | | // Licensed to the .NET Foundation under one or more agreements. |
| | | 2 | | // The .NET Foundation licenses this file to you under the MIT license. |
| | | 3 | | |
| | | 4 | | using System.Buffers; |
| | | 5 | | using System.Buffers.Text; |
| | | 6 | | using System.Collections; |
| | | 7 | | using System.Collections.Generic; |
| | | 8 | | using System.Diagnostics; |
| | | 9 | | using System.Diagnostics.CodeAnalysis; |
| | | 10 | | using System.Runtime.CompilerServices; |
| | | 11 | | using System.Text.Json.Nodes; |
| | | 12 | | using System.Text.RegularExpressions; |
| | | 13 | | |
| | | 14 | | namespace System.Text.Json |
| | | 15 | | { |
| | | 16 | | internal static partial class JsonHelpers |
| | | 17 | | { |
| | | 18 | | /// <summary> |
| | | 19 | | /// Returns the unescaped span for the given reader. |
| | | 20 | | /// </summary> |
| | | 21 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 22 | | public static ReadOnlySpan<byte> GetUnescapedSpan(this scoped ref Utf8JsonReader reader) |
| | 0 | 23 | | { |
| | 0 | 24 | | Debug.Assert(reader.TokenType is JsonTokenType.String or JsonTokenType.PropertyName); |
| | 0 | 25 | | ReadOnlySpan<byte> span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; |
| | 0 | 26 | | return reader.ValueIsEscaped ? JsonReaderHelper.GetUnescaped(span) : span; |
| | 0 | 27 | | } |
| | | 28 | | |
| | | 29 | | /// <summary> |
| | | 30 | | /// Attempts to perform a Read() operation and optionally checks that the full JSON value has been buffered. |
| | | 31 | | /// The reader will be reset if the operation fails. |
| | | 32 | | /// </summary> |
| | | 33 | | /// <param name="reader">The reader to advance.</param> |
| | | 34 | | /// <param name="requiresReadAhead">If reading a partial payload, read ahead to ensure that the full JSON value |
| | | 35 | | /// <returns>True if the reader has been buffered with all required data.</returns> |
| | | 36 | | // AggressiveInlining used since this method is on a hot path and short. The AdvanceWithReadAhead method should |
| | | 37 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 38 | | public static bool TryAdvanceWithOptionalReadAhead(this scoped ref Utf8JsonReader reader, bool requiresReadAhead |
| | 0 | 39 | | { |
| | | 40 | | // No read-ahead necessary if we're at the final block of JSON data. |
| | 0 | 41 | | bool readAhead = requiresReadAhead && !reader.IsFinalBlock; |
| | 0 | 42 | | return readAhead ? TryAdvanceWithReadAhead(ref reader) : reader.Read(); |
| | 0 | 43 | | } |
| | | 44 | | |
| | | 45 | | /// <summary> |
| | | 46 | | /// Attempts to read ahead to the next root-level JSON value, if it exists. |
| | | 47 | | /// </summary> |
| | | 48 | | public static bool TryAdvanceToNextRootLevelValueWithOptionalReadAhead(this scoped ref Utf8JsonReader reader, bo |
| | 0 | 49 | | { |
| | 0 | 50 | | Debug.Assert(reader.AllowMultipleValues, "only supported by readers that support multiple values."); |
| | 0 | 51 | | Debug.Assert(reader.CurrentDepth == 0, "should only invoked for top-level values."); |
| | | 52 | | |
| | 0 | 53 | | Utf8JsonReader checkpoint = reader; |
| | 0 | 54 | | if (!reader.Read()) |
| | 0 | 55 | | { |
| | | 56 | | // If the reader didn't return any tokens and it's the final block, |
| | | 57 | | // then there are no other JSON values to be read. |
| | 0 | 58 | | isAtEndOfStream = reader.IsFinalBlock; |
| | 0 | 59 | | reader = checkpoint; |
| | 0 | 60 | | return false; |
| | | 61 | | } |
| | | 62 | | |
| | | 63 | | // We found another JSON value, read ahead accordingly. |
| | 0 | 64 | | isAtEndOfStream = false; |
| | 0 | 65 | | if (requiresReadAhead && !reader.IsFinalBlock) |
| | 0 | 66 | | { |
| | | 67 | | // Perform full read-ahead to ensure the full JSON value has been buffered. |
| | 0 | 68 | | reader = checkpoint; |
| | 0 | 69 | | return TryAdvanceWithReadAhead(ref reader); |
| | | 70 | | } |
| | | 71 | | |
| | 0 | 72 | | return true; |
| | 0 | 73 | | } |
| | | 74 | | |
| | | 75 | | private static bool TryAdvanceWithReadAhead(scoped ref Utf8JsonReader reader) |
| | 0 | 76 | | { |
| | | 77 | | // When we're reading ahead we always have to save the state |
| | | 78 | | // as we don't know if the next token is a start object or array. |
| | 0 | 79 | | Utf8JsonReader restore = reader; |
| | | 80 | | |
| | 0 | 81 | | if (!reader.Read()) |
| | 0 | 82 | | { |
| | 0 | 83 | | return false; |
| | | 84 | | } |
| | | 85 | | |
| | | 86 | | // Perform the actual read-ahead. |
| | 0 | 87 | | JsonTokenType tokenType = reader.TokenType; |
| | 0 | 88 | | if (tokenType is JsonTokenType.StartObject or JsonTokenType.StartArray) |
| | 0 | 89 | | { |
| | | 90 | | // Attempt to skip to make sure we have all the data we need. |
| | 0 | 91 | | bool complete = reader.TrySkipPartial(); |
| | | 92 | | |
| | | 93 | | // We need to restore the state in all cases as we need to be positioned back before |
| | | 94 | | // the current token to either attempt to skip again or to actually read the value. |
| | 0 | 95 | | reader = restore; |
| | | 96 | | |
| | 0 | 97 | | if (!complete) |
| | 0 | 98 | | { |
| | | 99 | | // Couldn't read to the end of the object, exit out to get more data in the buffer. |
| | 0 | 100 | | return false; |
| | | 101 | | } |
| | | 102 | | |
| | | 103 | | // Success, requeue the reader to the start token. |
| | 0 | 104 | | reader.ReadWithVerify(); |
| | 0 | 105 | | Debug.Assert(tokenType == reader.TokenType); |
| | 0 | 106 | | } |
| | | 107 | | |
| | 0 | 108 | | return true; |
| | 0 | 109 | | } |
| | | 110 | | |
| | | 111 | | #if !NET |
| | | 112 | | /// <summary> |
| | | 113 | | /// Returns <see langword="true"/> if <paramref name="value"/> is a valid Unicode scalar |
| | | 114 | | /// value, i.e., is in [ U+0000..U+D7FF ], inclusive; or [ U+E000..U+10FFFF ], inclusive. |
| | | 115 | | /// </summary> |
| | | 116 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 117 | | public static bool IsValidUnicodeScalar(uint value) |
| | | 118 | | { |
| | | 119 | | // By XORing the incoming value with 0xD800, surrogate code points |
| | | 120 | | // are moved to the range [ U+0000..U+07FF ], and all valid scalar |
| | | 121 | | // values are clustered into the single range [ U+0800..U+10FFFF ], |
| | | 122 | | // which allows performing a single fast range check. |
| | | 123 | | |
| | | 124 | | return IsInRangeInclusive(value ^ 0xD800U, 0x800U, 0x10FFFFU); |
| | | 125 | | } |
| | | 126 | | #endif |
| | | 127 | | |
| | | 128 | | /// <summary> |
| | | 129 | | /// Returns <see langword="true"/> if <paramref name="value"/> is between |
| | | 130 | | /// <paramref name="lowerBound"/> and <paramref name="upperBound"/>, inclusive. |
| | | 131 | | /// </summary> |
| | | 132 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 133 | | public static bool IsInRangeInclusive(uint value, uint lowerBound, uint upperBound) |
| | 0 | 134 | | => (value - lowerBound) <= (upperBound - lowerBound); |
| | | 135 | | |
| | | 136 | | /// <summary> |
| | | 137 | | /// Returns <see langword="true"/> if <paramref name="value"/> is between |
| | | 138 | | /// <paramref name="lowerBound"/> and <paramref name="upperBound"/>, inclusive. |
| | | 139 | | /// </summary> |
| | | 140 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 141 | | public static bool IsInRangeInclusive(int value, int lowerBound, int upperBound) |
| | 0 | 142 | | => (uint)(value - lowerBound) <= (uint)(upperBound - lowerBound); |
| | | 143 | | |
| | | 144 | | /// <summary> |
| | | 145 | | /// Returns <see langword="true"/> if <paramref name="value"/> is between |
| | | 146 | | /// <paramref name="lowerBound"/> and <paramref name="upperBound"/>, inclusive. |
| | | 147 | | /// </summary> |
| | | 148 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 149 | | public static bool IsInRangeInclusive(long value, long lowerBound, long upperBound) |
| | 0 | 150 | | => (ulong)(value - lowerBound) <= (ulong)(upperBound - lowerBound); |
| | | 151 | | |
| | | 152 | | /// <summary> |
| | | 153 | | /// Returns <see langword="true"/> if <paramref name="value"/> is between |
| | | 154 | | /// <paramref name="lowerBound"/> and <paramref name="upperBound"/>, inclusive. |
| | | 155 | | /// </summary> |
| | | 156 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 157 | | public static bool IsInRangeInclusive(JsonTokenType value, JsonTokenType lowerBound, JsonTokenType upperBound) |
| | | 158 | | => (value - lowerBound) <= (upperBound - lowerBound); |
| | | 159 | | |
| | | 160 | | /// <summary> |
| | | 161 | | /// Returns <see langword="true"/> if <paramref name="value"/> is in the range [0..9]. |
| | | 162 | | /// Otherwise, returns <see langword="false"/>. |
| | | 163 | | /// </summary> |
| | 0 | 164 | | public static bool IsDigit(byte value) => (uint)(value - '0') <= '9' - '0'; |
| | | 165 | | |
| | | 166 | | /// <summary> |
| | | 167 | | /// Perform a Read() with a Debug.Assert verifying the reader did not return false. |
| | | 168 | | /// This should be called when the Read() return value is not used, such as non-Stream cases where there is only |
| | | 169 | | /// </summary> |
| | | 170 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 171 | | public static void ReadWithVerify(this ref Utf8JsonReader reader) |
| | 0 | 172 | | { |
| | 0 | 173 | | bool result = reader.Read(); |
| | 0 | 174 | | Debug.Assert(result); |
| | 0 | 175 | | } |
| | | 176 | | |
| | | 177 | | /// <summary> |
| | | 178 | | /// Performs a TrySkip() with a Debug.Assert verifying the reader did not return false. |
| | | 179 | | /// </summary> |
| | | 180 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 181 | | public static void SkipWithVerify(this ref Utf8JsonReader reader) |
| | 0 | 182 | | { |
| | 0 | 183 | | bool success = reader.TrySkipPartial(reader.CurrentDepth); |
| | 0 | 184 | | Debug.Assert(success, "The skipped value should have already been buffered."); |
| | 0 | 185 | | } |
| | | 186 | | |
| | | 187 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 188 | | public static bool TrySkipPartial(this ref Utf8JsonReader reader) |
| | 0 | 189 | | { |
| | 0 | 190 | | return reader.TrySkipPartial(reader.CurrentDepth); |
| | 0 | 191 | | } |
| | | 192 | | |
| | | 193 | | public static unsafe bool TryLookupUtf8Key<TValue>( |
| | | 194 | | this Dictionary<string, TValue> dictionary, |
| | | 195 | | ReadOnlySpan<byte> utf8Key, |
| | | 196 | | [MaybeNullWhen(false)] out TValue result) |
| | 0 | 197 | | { |
| | | 198 | | #if NET |
| | 0 | 199 | | Debug.Assert(dictionary.Comparer is IAlternateEqualityComparer<ReadOnlySpan<char>, string>); |
| | | 200 | | |
| | 0 | 201 | | Dictionary<string, TValue>.AlternateLookup<ReadOnlySpan<char>> spanLookup = |
| | 0 | 202 | | dictionary.GetAlternateLookup<ReadOnlySpan<char>>(); |
| | | 203 | | |
| | 0 | 204 | | char[]? rentedBuffer = null; |
| | | 205 | | |
| | 0 | 206 | | Span<char> charBuffer = utf8Key.Length <= JsonConstants.StackallocCharThreshold ? |
| | 0 | 207 | | stackalloc char[JsonConstants.StackallocCharThreshold] : |
| | 0 | 208 | | (rentedBuffer = ArrayPool<char>.Shared.Rent(utf8Key.Length)); |
| | | 209 | | |
| | 0 | 210 | | int charsWritten = Encoding.UTF8.GetChars(utf8Key, charBuffer); |
| | 0 | 211 | | Span<char> decodedKey = charBuffer[0..charsWritten]; |
| | | 212 | | |
| | 0 | 213 | | bool success = spanLookup.TryGetValue(decodedKey, out result); |
| | | 214 | | |
| | 0 | 215 | | if (rentedBuffer is not null) |
| | 0 | 216 | | { |
| | 0 | 217 | | decodedKey.Clear(); |
| | 0 | 218 | | ArrayPool<char>.Shared.Return(rentedBuffer); |
| | 0 | 219 | | } |
| | | 220 | | |
| | 0 | 221 | | return success; |
| | | 222 | | #else |
| | | 223 | | string key = Encoding.UTF8.GetString(utf8Key); |
| | | 224 | | return dictionary.TryGetValue(key, out result); |
| | | 225 | | #endif |
| | 0 | 226 | | } |
| | | 227 | | |
| | | 228 | | public static bool TryLookupUtf8Key<TValue>( |
| | | 229 | | this Dictionary<byte[], TValue> dictionary, |
| | | 230 | | ReadOnlySpan<byte> utf8Key, |
| | | 231 | | [MaybeNullWhen(false)] out TValue result) |
| | 0 | 232 | | { |
| | 0 | 233 | | Debug.Assert(dictionary.Comparer is ByteArrayOrdinalComparer); |
| | | 234 | | #if NET |
| | 0 | 235 | | Dictionary<byte[], TValue>.AlternateLookup<ReadOnlySpan<byte>> spanLookup = |
| | 0 | 236 | | dictionary.GetAlternateLookup<ReadOnlySpan<byte>>(); |
| | | 237 | | |
| | 0 | 238 | | return spanLookup.TryGetValue(utf8Key, out result); |
| | | 239 | | #else |
| | | 240 | | return dictionary.TryGetValue(utf8Key.ToArray(), out result); |
| | | 241 | | #endif |
| | 0 | 242 | | } |
| | | 243 | | |
| | | 244 | | /// <summary>Compares byte arrays using ordinal equality.</summary> |
| | | 245 | | internal sealed class ByteArrayOrdinalComparer : |
| | | 246 | | #if NET |
| | | 247 | | IAlternateEqualityComparer<ReadOnlySpan<byte>, byte[]>, |
| | | 248 | | #endif |
| | | 249 | | IEqualityComparer<byte[]> |
| | | 250 | | { |
| | 0 | 251 | | public static readonly ByteArrayOrdinalComparer Instance = new(); |
| | | 252 | | |
| | | 253 | | public bool Equals(byte[]? left, byte[]? right) => |
| | 0 | 254 | | ReferenceEquals(left, right) || |
| | 0 | 255 | | (left is not null && right is not null && left.AsSpan().SequenceEqual(right)); |
| | | 256 | | |
| | 0 | 257 | | public int GetHashCode(byte[] value) => ComputeHashCode(value); |
| | | 258 | | |
| | | 259 | | #if NET |
| | | 260 | | byte[] IAlternateEqualityComparer<ReadOnlySpan<byte>, byte[]>.Create(ReadOnlySpan<byte> span) => |
| | 0 | 261 | | span.ToArray(); |
| | | 262 | | |
| | | 263 | | bool IAlternateEqualityComparer<ReadOnlySpan<byte>, byte[]>.Equals( |
| | | 264 | | ReadOnlySpan<byte> span, |
| | | 265 | | byte[] target) => |
| | 0 | 266 | | span.SequenceEqual(target); |
| | | 267 | | |
| | | 268 | | int IAlternateEqualityComparer<ReadOnlySpan<byte>, byte[]>.GetHashCode(ReadOnlySpan<byte> span) => |
| | 0 | 269 | | ComputeHashCode(span); |
| | | 270 | | #endif |
| | | 271 | | |
| | | 272 | | private static int ComputeHashCode(ReadOnlySpan<byte> value) => |
| | 0 | 273 | | Marvin.ComputeHash32(value, Marvin.DefaultSeed); |
| | | 274 | | } |
| | | 275 | | |
| | | 276 | | /// <summary> |
| | | 277 | | /// Emulates Dictionary(IEnumerable{KeyValuePair}) on netstandard. |
| | | 278 | | /// </summary> |
| | | 279 | | public static Dictionary<TKey, TValue> CreateDictionaryFromCollection<TKey, TValue>( |
| | | 280 | | IEnumerable<KeyValuePair<TKey, TValue>> collection, |
| | | 281 | | IEqualityComparer<TKey> comparer) |
| | | 282 | | where TKey : notnull |
| | | 283 | | { |
| | | 284 | | #if !NET |
| | | 285 | | var dictionary = new Dictionary<TKey, TValue>(comparer); |
| | | 286 | | |
| | | 287 | | foreach (KeyValuePair<TKey, TValue> item in collection) |
| | | 288 | | { |
| | | 289 | | dictionary.Add(item.Key, item.Value); |
| | | 290 | | } |
| | | 291 | | |
| | | 292 | | return dictionary; |
| | | 293 | | #else |
| | | 294 | | return new Dictionary<TKey, TValue>(collection: collection, comparer); |
| | | 295 | | #endif |
| | | 296 | | } |
| | | 297 | | |
| | | 298 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 299 | | public static void ValidateInt32MaxArrayLength(uint length) |
| | 372 | 300 | | { |
| | 372 | 301 | | if (length > 0X7FEFFFFF) // prior to .NET 6, max array length for sizeof(T) != 1 (size == 1 is larger) |
| | 0 | 302 | | { |
| | 0 | 303 | | ThrowHelper.ThrowOutOfMemoryException(length); |
| | | 304 | | } |
| | 372 | 305 | | } |
| | | 306 | | |
| | | 307 | | /// <summary> |
| | | 308 | | /// Gets a Regex instance for recognizing integer representations of enums. |
| | | 309 | | /// </summary> |
| | | 310 | | private const string IntegerRegexPattern = @"^\s*(?:\+|\-)?[0-9]+\s*$"; |
| | | 311 | | private const int IntegerRegexTimeoutMs = 200; |
| | | 312 | | |
| | | 313 | | #if NET |
| | | 314 | | [GeneratedRegex(IntegerRegexPattern, RegexOptions.None, matchTimeoutMilliseconds: IntegerRegexTimeoutMs)] |
| | | 315 | | public static partial Regex IntegerRegex { get; } |
| | | 316 | | #else |
| | | 317 | | public static Regex IntegerRegex { get; } = new(IntegerRegexPattern, RegexOptions.Compiled, TimeSpan.FromMillise |
| | | 318 | | #endif |
| | | 319 | | |
| | | 320 | | /// <summary> |
| | | 321 | | /// Compares two valid UTF-8 encoded JSON numbers for decimal equality. |
| | | 322 | | /// </summary> |
| | | 323 | | public static bool AreEqualJsonNumbers(ReadOnlySpan<byte> left, ReadOnlySpan<byte> right) |
| | 0 | 324 | | { |
| | 0 | 325 | | Debug.Assert(left.Length > 0 && right.Length > 0); |
| | | 326 | | |
| | 0 | 327 | | ParseNumber(left, |
| | 0 | 328 | | out bool leftIsNegative, |
| | 0 | 329 | | out ReadOnlySpan<byte> leftIntegral, |
| | 0 | 330 | | out ReadOnlySpan<byte> leftFractional, |
| | 0 | 331 | | out int leftExponent); |
| | | 332 | | |
| | 0 | 333 | | ParseNumber(right, |
| | 0 | 334 | | out bool rightIsNegative, |
| | 0 | 335 | | out ReadOnlySpan<byte> rightIntegral, |
| | 0 | 336 | | out ReadOnlySpan<byte> rightFractional, |
| | 0 | 337 | | out int rightExponent); |
| | | 338 | | |
| | | 339 | | int nDigits; |
| | 0 | 340 | | if (leftIsNegative != rightIsNegative || |
| | 0 | 341 | | leftExponent != rightExponent || |
| | 0 | 342 | | (nDigits = (leftIntegral.Length + leftFractional.Length)) != |
| | 0 | 343 | | rightIntegral.Length + rightFractional.Length) |
| | 0 | 344 | | { |
| | 0 | 345 | | return false; |
| | | 346 | | } |
| | | 347 | | |
| | | 348 | | // Need to check that the concatenated integral and fractional parts are equal; |
| | | 349 | | // break each representation into three parts such that their lengths exactly match. |
| | | 350 | | ReadOnlySpan<byte> leftFirst; |
| | | 351 | | ReadOnlySpan<byte> leftMiddle; |
| | | 352 | | ReadOnlySpan<byte> leftLast; |
| | | 353 | | |
| | | 354 | | ReadOnlySpan<byte> rightFirst; |
| | | 355 | | ReadOnlySpan<byte> rightMiddle; |
| | | 356 | | ReadOnlySpan<byte> rightLast; |
| | | 357 | | |
| | 0 | 358 | | int diff = leftIntegral.Length - rightIntegral.Length; |
| | 0 | 359 | | switch (diff) |
| | | 360 | | { |
| | | 361 | | case < 0: |
| | 0 | 362 | | leftFirst = leftIntegral; |
| | 0 | 363 | | leftMiddle = leftFractional.Slice(0, -diff); |
| | 0 | 364 | | leftLast = leftFractional.Slice(-diff); |
| | 0 | 365 | | int rightOffset = rightIntegral.Length + diff; |
| | 0 | 366 | | rightFirst = rightIntegral.Slice(0, rightOffset); |
| | 0 | 367 | | rightMiddle = rightIntegral.Slice(rightOffset); |
| | 0 | 368 | | rightLast = rightFractional; |
| | 0 | 369 | | break; |
| | | 370 | | |
| | | 371 | | case 0: |
| | 0 | 372 | | leftFirst = leftIntegral; |
| | 0 | 373 | | leftMiddle = default; |
| | 0 | 374 | | leftLast = leftFractional; |
| | 0 | 375 | | rightFirst = rightIntegral; |
| | 0 | 376 | | rightMiddle = default; |
| | 0 | 377 | | rightLast = rightFractional; |
| | 0 | 378 | | break; |
| | | 379 | | |
| | | 380 | | case > 0: |
| | 0 | 381 | | int leftOffset = leftIntegral.Length - diff; |
| | 0 | 382 | | leftFirst = leftIntegral.Slice(0, leftOffset); |
| | 0 | 383 | | leftMiddle = leftIntegral.Slice(leftOffset); |
| | 0 | 384 | | leftLast = leftFractional; |
| | 0 | 385 | | rightFirst = rightIntegral; |
| | 0 | 386 | | rightMiddle = rightFractional.Slice(0, diff); |
| | 0 | 387 | | rightLast = rightFractional.Slice(diff); |
| | 0 | 388 | | break; |
| | | 389 | | } |
| | | 390 | | |
| | 0 | 391 | | Debug.Assert(leftFirst.Length == rightFirst.Length); |
| | 0 | 392 | | Debug.Assert(leftMiddle.Length == rightMiddle.Length); |
| | 0 | 393 | | Debug.Assert(leftLast.Length == rightLast.Length); |
| | 0 | 394 | | return leftFirst.SequenceEqual(rightFirst) && |
| | 0 | 395 | | leftMiddle.SequenceEqual(rightMiddle) && |
| | 0 | 396 | | leftLast.SequenceEqual(rightLast); |
| | | 397 | | |
| | | 398 | | static void ParseNumber( |
| | | 399 | | ReadOnlySpan<byte> span, |
| | | 400 | | out bool isNegative, |
| | | 401 | | out ReadOnlySpan<byte> integral, |
| | | 402 | | out ReadOnlySpan<byte> fractional, |
| | | 403 | | out int exponent) |
| | 0 | 404 | | { |
| | | 405 | | // Parses a JSON number into its integral, fractional, and exponent parts. |
| | | 406 | | // The returned components use a normal-form decimal representation: |
| | | 407 | | // |
| | | 408 | | // Number := sign * <integral + fractional> * 10^exponent |
| | | 409 | | // |
| | | 410 | | // where integral and fractional are sequences of digits whose concatenation |
| | | 411 | | // represents the significand of the number without leading or trailing zeros. |
| | | 412 | | // Two such normal-form numbers are treated as equal if and only if they have |
| | | 413 | | // equal signs, significands, and exponents. |
| | | 414 | | |
| | | 415 | | bool neg; |
| | | 416 | | ReadOnlySpan<byte> intg; |
| | | 417 | | ReadOnlySpan<byte> frac; |
| | | 418 | | int exp; |
| | | 419 | | |
| | 0 | 420 | | Debug.Assert(span.Length > 0); |
| | | 421 | | |
| | 0 | 422 | | if (span[0] == '-') |
| | 0 | 423 | | { |
| | 0 | 424 | | neg = true; |
| | 0 | 425 | | span = span.Slice(1); |
| | 0 | 426 | | } |
| | | 427 | | else |
| | 0 | 428 | | { |
| | 0 | 429 | | Debug.Assert(char.IsDigit((char)span[0]), "leading plus not allowed in valid JSON numbers."); |
| | 0 | 430 | | neg = false; |
| | 0 | 431 | | } |
| | | 432 | | |
| | 0 | 433 | | int i = span.IndexOfAny((byte)'.', (byte)'e', (byte)'E'); |
| | 0 | 434 | | if (i < 0) |
| | 0 | 435 | | { |
| | 0 | 436 | | intg = span; |
| | 0 | 437 | | frac = default; |
| | 0 | 438 | | exp = 0; |
| | 0 | 439 | | goto Normalize; |
| | | 440 | | } |
| | | 441 | | |
| | 0 | 442 | | intg = span.Slice(0, i); |
| | | 443 | | |
| | 0 | 444 | | if (span[i] == '.') |
| | 0 | 445 | | { |
| | 0 | 446 | | span = span.Slice(i + 1); |
| | 0 | 447 | | i = span.IndexOfAny((byte)'e', (byte)'E'); |
| | 0 | 448 | | if (i < 0) |
| | 0 | 449 | | { |
| | 0 | 450 | | frac = span; |
| | 0 | 451 | | exp = 0; |
| | 0 | 452 | | goto Normalize; |
| | | 453 | | } |
| | | 454 | | |
| | 0 | 455 | | frac = span.Slice(0, i); |
| | 0 | 456 | | } |
| | | 457 | | else |
| | 0 | 458 | | { |
| | 0 | 459 | | frac = default; |
| | 0 | 460 | | } |
| | | 461 | | |
| | 0 | 462 | | Debug.Assert(span[i] is (byte)'e' or (byte)'E'); |
| | 0 | 463 | | if (!Utf8Parser.TryParse(span.Slice(i + 1), out exp, out _)) |
| | 0 | 464 | | { |
| | 0 | 465 | | Debug.Assert(span.Length >= 10); |
| | 0 | 466 | | ThrowHelper.ThrowArgumentOutOfRangeException_JsonNumberExponentTooLarge(nameof(exponent)); |
| | | 467 | | } |
| | | 468 | | |
| | 0 | 469 | | Normalize: // Calculates the normal form of the number. |
| | | 470 | | |
| | 0 | 471 | | if (IndexOfFirstTrailingZero(frac) is >= 0 and int iz) |
| | 0 | 472 | | { |
| | | 473 | | // Trim trailing zeros from the fractional part. |
| | | 474 | | // e.g. 3.1400 -> 3.14 |
| | 0 | 475 | | frac = frac.Slice(0, iz); |
| | 0 | 476 | | } |
| | | 477 | | |
| | 0 | 478 | | if (intg[0] == '0') |
| | 0 | 479 | | { |
| | 0 | 480 | | Debug.Assert(intg.Length == 1, "Leading zeros not permitted in JSON numbers."); |
| | | 481 | | |
| | 0 | 482 | | if (IndexOfLastLeadingZero(frac) is >= 0 and int lz) |
| | 0 | 483 | | { |
| | | 484 | | // Trim leading zeros from the fractional part |
| | | 485 | | // and update the exponent accordingly. |
| | | 486 | | // e.g. 0.000123 -> 0.123e-3 |
| | 0 | 487 | | frac = frac.Slice(lz + 1); |
| | 0 | 488 | | exp -= lz + 1; |
| | 0 | 489 | | } |
| | | 490 | | |
| | | 491 | | // Normalize "0" to the empty span. |
| | 0 | 492 | | intg = default; |
| | 0 | 493 | | } |
| | | 494 | | |
| | 0 | 495 | | if (frac.IsEmpty && IndexOfFirstTrailingZero(intg) is >= 0 and int fz) |
| | 0 | 496 | | { |
| | | 497 | | // There is no fractional part, trim trailing zeros from |
| | | 498 | | // the integral part and increase the exponent accordingly. |
| | | 499 | | // e.g. 1000 -> 1e3 |
| | 0 | 500 | | exp += intg.Length - fz; |
| | 0 | 501 | | intg = intg.Slice(0, fz); |
| | 0 | 502 | | } |
| | | 503 | | |
| | | 504 | | // Normalize the exponent by subtracting the length of the fractional part. |
| | | 505 | | // e.g. 3.14 -> 314e-2 |
| | 0 | 506 | | exp -= frac.Length; |
| | | 507 | | |
| | 0 | 508 | | if (intg.IsEmpty && frac.IsEmpty) |
| | 0 | 509 | | { |
| | | 510 | | // Normalize zero representations. |
| | 0 | 511 | | neg = false; |
| | 0 | 512 | | exp = 0; |
| | 0 | 513 | | } |
| | | 514 | | |
| | | 515 | | // Copy to out parameters. |
| | 0 | 516 | | isNegative = neg; |
| | 0 | 517 | | integral = intg; |
| | 0 | 518 | | fractional = frac; |
| | 0 | 519 | | exponent = exp; |
| | | 520 | | |
| | | 521 | | static int IndexOfLastLeadingZero(ReadOnlySpan<byte> span) |
| | 0 | 522 | | { |
| | | 523 | | #if NET |
| | 0 | 524 | | int firstNonZero = span.IndexOfAnyExcept((byte)'0'); |
| | 0 | 525 | | return firstNonZero < 0 ? span.Length - 1 : firstNonZero - 1; |
| | | 526 | | #else |
| | | 527 | | for (int i = 0; i < span.Length; i++) |
| | | 528 | | { |
| | | 529 | | if (span[i] != '0') |
| | | 530 | | { |
| | | 531 | | return i - 1; |
| | | 532 | | } |
| | | 533 | | } |
| | | 534 | | |
| | | 535 | | return span.Length - 1; |
| | | 536 | | #endif |
| | 0 | 537 | | } |
| | | 538 | | |
| | | 539 | | static int IndexOfFirstTrailingZero(ReadOnlySpan<byte> span) |
| | 0 | 540 | | { |
| | | 541 | | #if NET |
| | 0 | 542 | | int lastNonZero = span.LastIndexOfAnyExcept((byte)'0'); |
| | 0 | 543 | | return lastNonZero == span.Length - 1 ? -1 : lastNonZero + 1; |
| | | 544 | | #else |
| | | 545 | | if (span.IsEmpty) |
| | | 546 | | { |
| | | 547 | | return -1; |
| | | 548 | | } |
| | | 549 | | |
| | | 550 | | for (int i = span.Length - 1; i >= 0; i--) |
| | | 551 | | { |
| | | 552 | | if (span[i] != '0') |
| | | 553 | | { |
| | | 554 | | return i == span.Length - 1 ? -1 : i + 1; |
| | | 555 | | } |
| | | 556 | | } |
| | | 557 | | |
| | | 558 | | return 0; |
| | | 559 | | #endif |
| | 0 | 560 | | } |
| | 0 | 561 | | } |
| | 0 | 562 | | } |
| | | 563 | | } |
| | | 564 | | } |
| | | 565 | | |