diff options
author | Tanner Gooding <tagoo@outlook.com> | 2018-11-08 17:58:24 -0800 |
---|---|---|
committer | Jan Kotas <jkotas@microsoft.com> | 2018-11-09 06:14:46 -0800 |
commit | 0fccc78cfea93bafbba07cc4a84a32582a3af88f (patch) | |
tree | dc73e9450660f41ed8d225804f48a30322f85086 /src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser | |
parent | 00f5934a3e34977c7a1502da604f2dae90040888 (diff) | |
download | coreclr-0fccc78cfea93bafbba07cc4a84a32582a3af88f.tar.gz coreclr-0fccc78cfea93bafbba07cc4a84a32582a3af88f.tar.bz2 coreclr-0fccc78cfea93bafbba07cc4a84a32582a3af88f.zip |
Moving the Utf8Parser/Utf8Formatter to be shared (dotnet/corefx#33348)
Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com>
Diffstat (limited to 'src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser')
24 files changed, 4862 insertions, 0 deletions
diff --git a/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/ParserHelpers.cs b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/ParserHelpers.cs new file mode 100644 index 0000000000..b527433a7d --- /dev/null +++ b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/ParserHelpers.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + +namespace System.Buffers.Text +{ + internal static class ParserHelpers + { + public const int ByteOverflowLength = 3; + public const int ByteOverflowLengthHex = 2; + public const int UInt16OverflowLength = 5; + public const int UInt16OverflowLengthHex = 4; + public const int UInt32OverflowLength = 10; + public const int UInt32OverflowLengthHex = 8; + public const int UInt64OverflowLength = 20; + public const int UInt64OverflowLengthHex = 16; + + public const int SByteOverflowLength = 3; + public const int SByteOverflowLengthHex = 2; + public const int Int16OverflowLength = 5; + public const int Int16OverflowLengthHex = 4; + public const int Int32OverflowLength = 10; + public const int Int32OverflowLengthHex = 8; + public const int Int64OverflowLength = 19; + public const int Int64OverflowLengthHex = 16; + + public static readonly byte[] s_hexLookup = + { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 15 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 31 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 47 + 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 63 + 0xFF, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 79 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 95 + 0xFF, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 111 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 127 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 143 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 159 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 175 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 191 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 207 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 223 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 239 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // 255 + }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsDigit(int i) + { + return (uint)(i - '0') <= ('9' - '0'); + } + } +} diff --git a/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Boolean.cs b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Boolean.cs new file mode 100644 index 0000000000..41c57143a8 --- /dev/null +++ b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Boolean.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Buffers.Binary; + +namespace System.Buffers.Text +{ + public static partial class Utf8Parser + { + /// <summary> + /// Parses a Boolean at the start of a Utf8 string. + /// </summary> + /// <param name="source">The Utf8 string to parse</param> + /// <param name="value">Receives the parsed value</param> + /// <param name="bytesConsumed">On a successful parse, receives the length in bytes of the substring that was parsed </param> + /// <param name="standardFormat">Expected format of the Utf8 string</param> + /// <returns> + /// true for success. "bytesConsumed" contains the length in bytes of the substring that was parsed. + /// false if the string was not syntactically valid or an overflow or underflow occurred. "bytesConsumed" is set to 0. + /// </returns> + /// <remarks> + /// Formats supported: + /// G (default) True/False + /// l true/false + /// </remarks> + /// <exceptions> + /// <cref>System.FormatException</cref> if the format is not valid for this data type. + /// </exceptions> + public static bool TryParse(ReadOnlySpan<byte> source, out bool value, out int bytesConsumed, char standardFormat = default) + { + if (!(standardFormat == default(char) || standardFormat == 'G' || standardFormat == 'l')) + return ThrowHelper.TryParseThrowFormatException(out value, out bytesConsumed); + + if (source.Length >= 4) + { + int dw = BinaryPrimitives.ReadInt32LittleEndian(source) & ~0x20202020; + if (dw == 0x45555254 /* 'EURT' */) + { + bytesConsumed = 4; + value = true; + return true; + } + + if (source.Length >= 5) + { + if (dw == 0x534c4146 /* 'SLAF' */ && (source[4] & ~0x20) == 'E') + { + bytesConsumed = 5; + value = false; + return true; + } + } + } + + bytesConsumed = 0; + value = default; + return false; + } + } +} diff --git a/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Date.Default.cs b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Date.Default.cs new file mode 100644 index 0000000000..73578ea88c --- /dev/null +++ b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Date.Default.cs @@ -0,0 +1,102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Buffers.Text +{ + public static partial class Utf8Parser + { + // + // ToString() format for DateTimeOffset. Does not have a corresponding format symbol but it + // is the "G" format postpended with the UTC offset. + // + // 01234567890123456789012345 + // -------------------------- + // 05/25/2017 10:30:15 -08:00 + // + private static bool TryParseDateTimeOffsetDefault(ReadOnlySpan<byte> source, out DateTimeOffset value, out int bytesConsumed) + { + if (source.Length < 26) + { + bytesConsumed = 0; + value = default; + return false; + } + + if (!TryParseDateTimeG(source, out DateTime dateTime, out _, out _)) + { + bytesConsumed = 0; + value = default; + return false; + } + + if (source[19] != Utf8Constants.Space) + { + bytesConsumed = 0; + value = default; + return false; + } + + byte sign = source[20]; + if (sign != Utf8Constants.Plus && sign != Utf8Constants.Minus) + { + bytesConsumed = 0; + value = default; + return false; + } + + int offsetHours; + { + uint digit1 = source[21] - 48u; // '0' + uint digit2 = source[22] - 48u; // '0' + + if (digit1 > 9 || digit2 > 9) + { + bytesConsumed = 0; + value = default; + return false; + } + + offsetHours = (int)(digit1 * 10 + digit2); + } + + if (source[23] != Utf8Constants.Colon) + { + bytesConsumed = 0; + value = default; + return false; + } + + int offsetMinutes; + { + uint digit1 = source[24] - 48u; // '0' + uint digit2 = source[25] - 48u; // '0' + + if (digit1 > 9 || digit2 > 9) + { + bytesConsumed = 0; + value = default; + return false; + } + + offsetMinutes = (int)(digit1 * 10 + digit2); + } + + TimeSpan offset = new TimeSpan(hours: offsetHours, minutes: offsetMinutes, seconds: 0); + if (sign == Utf8Constants.Minus) + { + offset = -offset; + } + + if (!TryCreateDateTimeOffset(dateTime: dateTime, offsetNegative: sign == Utf8Constants.Minus, offsetHours: offsetHours, offsetMinutes: offsetMinutes, out value)) + { + bytesConsumed = 0; + value = default; + return false; + } + + bytesConsumed = 26; + return true; + } + } +} diff --git a/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Date.G.cs b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Date.G.cs new file mode 100644 index 0000000000..6e8edbcbdf --- /dev/null +++ b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Date.G.cs @@ -0,0 +1,177 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Buffers.Text +{ + public static partial class Utf8Parser + { + // + // 'G' format for DateTime. + // + // 0123456789012345678 + // --------------------------------- + // 05/25/2017 10:30:15 + // + private static bool TryParseDateTimeG(ReadOnlySpan<byte> source, out DateTime value, out DateTimeOffset valueAsOffset, out int bytesConsumed) + { + if (source.Length < 19) + { + bytesConsumed = 0; + value = default; + valueAsOffset = default; + return false; + } + + int month; + { + uint digit1 = source[0] - 48u; // '0' + uint digit2 = source[1] - 48u; // '0' + + if (digit1 > 9 || digit2 > 9) + { + bytesConsumed = 0; + value = default; + valueAsOffset = default; + return false; + } + + month = (int)(digit1 * 10 + digit2); + } + + if (source[2] != Utf8Constants.Slash) + { + bytesConsumed = 0; + value = default; + valueAsOffset = default; + return false; + } + + int day; + { + uint digit1 = source[3] - 48u; // '0' + uint digit2 = source[4] - 48u; // '0' + + if (digit1 > 9 || digit2 > 9) + { + bytesConsumed = 0; + value = default; + valueAsOffset = default; + return false; + } + + day = (int)(digit1 * 10 + digit2); + } + + if (source[5] != Utf8Constants.Slash) + { + bytesConsumed = 0; + value = default; + valueAsOffset = default; + return false; + } + + int year; + { + uint digit1 = source[6] - 48u; // '0' + uint digit2 = source[7] - 48u; // '0' + uint digit3 = source[8] - 48u; // '0' + uint digit4 = source[9] - 48u; // '0' + + if (digit1 > 9 || digit2 > 9 || digit3 > 9 || digit4 > 9) + { + bytesConsumed = 0; + value = default; + valueAsOffset = default; + return false; + } + + year = (int)(digit1 * 1000 + digit2 * 100 + digit3 * 10 + digit4); + } + + if (source[10] != Utf8Constants.Space) + { + bytesConsumed = 0; + value = default; + valueAsOffset = default; + return false; + } + + int hour; + { + uint digit1 = source[11] - 48u; // '0' + uint digit2 = source[12] - 48u; // '0' + + if (digit1 > 9 || digit2 > 9) + { + bytesConsumed = 0; + value = default; + valueAsOffset = default; + return false; + } + + hour = (int)(digit1 * 10 + digit2); + } + + if (source[13] != Utf8Constants.Colon) + { + bytesConsumed = 0; + value = default; + valueAsOffset = default; + return false; + } + + int minute; + { + uint digit1 = source[14] - 48u; // '0' + uint digit2 = source[15] - 48u; // '0' + + if (digit1 > 9 || digit2 > 9) + { + bytesConsumed = 0; + value = default; + valueAsOffset = default; + return false; + } + + minute = (int)(digit1 * 10 + digit2); + } + + if (source[16] != Utf8Constants.Colon) + { + bytesConsumed = 0; + value = default; + valueAsOffset = default; + return false; + } + + int second; + { + uint digit1 = source[17] - 48u; // '0' + uint digit2 = source[18] - 48u; // '0' + + if (digit1 > 9 || digit2 > 9) + { + bytesConsumed = 0; + value = default; + valueAsOffset = default; + return false; + } + + second = (int)(digit1 * 10 + digit2); + } + + if (!TryCreateDateTimeOffsetInterpretingDataAsLocalTime(year: year, month: month, day: day, hour: hour, minute: minute, second: second, fraction: 0, out valueAsOffset)) + { + bytesConsumed = 0; + value = default; + valueAsOffset = default; + return false; + } + + bytesConsumed = 19; + value = valueAsOffset.DateTime; + return true; + } + } +} diff --git a/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Date.Helpers.cs b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Date.Helpers.cs new file mode 100644 index 0000000000..d2fb06829a --- /dev/null +++ b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Date.Helpers.cs @@ -0,0 +1,162 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; + +namespace System.Buffers.Text +{ + public static partial class Utf8Parser + { + /// <summary> + /// Overflow-safe DateTimeOffset factory. + /// </summary> + private static bool TryCreateDateTimeOffset(DateTime dateTime, bool offsetNegative, int offsetHours, int offsetMinutes, out DateTimeOffset value) + { + if (((uint)offsetHours) > Utf8Constants.DateTimeMaxUtcOffsetHours) + { + value = default; + return false; + } + + if (((uint)offsetMinutes) > 59) + { + value = default; + return false; + } + + if (offsetHours == Utf8Constants.DateTimeMaxUtcOffsetHours && offsetMinutes != 0) + { + value = default; + return false; + } + + long offsetTicks = (((long)offsetHours) * 3600 + ((long)offsetMinutes) * 60) * TimeSpan.TicksPerSecond; + if (offsetNegative) + { + offsetTicks = -offsetTicks; + } + + try + { + value = new DateTimeOffset(ticks: dateTime.Ticks, offset: new TimeSpan(ticks: offsetTicks)); + } + catch (ArgumentOutOfRangeException) + { + // If we got here, the combination of the DateTime + UTC offset strayed outside the 1..9999 year range. This case seems rare enough + // that it's better to catch the exception rather than replicate DateTime's range checking (which it's going to do anyway.) + value = default; + return false; + } + + return true; + } + + /// <summary> + /// Overflow-safe DateTimeOffset factory. + /// </summary> + private static bool TryCreateDateTimeOffset(int year, int month, int day, int hour, int minute, int second, int fraction, bool offsetNegative, int offsetHours, int offsetMinutes, out DateTimeOffset value) + { + if (!TryCreateDateTime(year: year, month: month, day: day, hour: hour, minute: minute, second: second, fraction: fraction, kind: DateTimeKind.Unspecified, out DateTime dateTime)) + { + value = default; + return false; + } + + if (!TryCreateDateTimeOffset(dateTime: dateTime, offsetNegative: offsetNegative, offsetHours: offsetHours, offsetMinutes: offsetMinutes, out value)) + { + value = default; + return false; + } + + return true; + } + + /// <summary> + /// Overflow-safe DateTimeOffset/Local time conversion factory. + /// </summary> + private static bool TryCreateDateTimeOffsetInterpretingDataAsLocalTime(int year, int month, int day, int hour, int minute, int second, int fraction, out DateTimeOffset value) + { + if (!TryCreateDateTime(year: year, month: month, day: day, hour: hour, minute: minute, second: second, fraction: fraction, DateTimeKind.Local, out DateTime dateTime)) + { + value = default; + return false; + } + + try + { + value = new DateTimeOffset(dateTime); + } + catch (ArgumentOutOfRangeException) + { + // If we got here, the combination of the DateTime + UTC offset strayed outside the 1..9999 year range. This case seems rare enough + // that it's better to catch the exception rather than replicate DateTime's range checking (which it's going to do anyway.) + + value = default; + return false; + } + + return true; + } + + /// <summary> + /// Overflow-safe DateTime factory. + /// </summary> + private static bool TryCreateDateTime(int year, int month, int day, int hour, int minute, int second, int fraction, DateTimeKind kind, out DateTime value) + { + if (year == 0) + { + value = default; + return false; + } + + Debug.Assert(year <= 9999); // All of our callers to date parse the year from fixed 4-digit fields so this value is trusted. + + if ((((uint)month) - 1) >= 12) + { + value = default; + return false; + } + + uint dayMinusOne = ((uint)day) - 1; + if (dayMinusOne >= 28 && dayMinusOne >= DateTime.DaysInMonth(year, month)) + { + value = default; + return false; + } + + if (((uint)hour) > 23) + { + value = default; + return false; + } + + if (((uint)minute) > 59) + { + value = default; + return false; + } + + if (((uint)second) > 59) + { + value = default; + return false; + } + + Debug.Assert(fraction >= 0 && fraction <= Utf8Constants.MaxDateTimeFraction); // All of our callers to date parse the fraction from fixed 7-digit fields so this value is trusted. + + int[] days = DateTime.IsLeapYear(year) ? s_daysToMonth366 : s_daysToMonth365; + int yearMinusOne = year - 1; + int totalDays = (yearMinusOne * 365) + (yearMinusOne / 4) - (yearMinusOne / 100) + (yearMinusOne / 400) + days[month - 1] + day - 1; + long ticks = totalDays * TimeSpan.TicksPerDay; + int totalSeconds = (hour * 3600) + (minute * 60) + second; + ticks += totalSeconds * TimeSpan.TicksPerSecond; + ticks += fraction; + value = new DateTime(ticks: ticks, kind: kind); + return true; + } + + private static readonly int[] s_daysToMonth365 = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 }; + private static readonly int[] s_daysToMonth366 = { 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 }; + } +} diff --git a/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Date.O.cs b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Date.O.cs new file mode 100644 index 0000000000..8d2c681f68 --- /dev/null +++ b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Date.O.cs @@ -0,0 +1,290 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; + +namespace System.Buffers.Text +{ + public static partial class Utf8Parser + { + // + // Roundtrippable format. One of + // + // 012345678901234567890123456789012 + // --------------------------------- + // 2017-06-12T05:30:45.7680000-07:00 + // 2017-06-12T05:30:45.7680000Z (Z is short for "+00:00" but also distinguishes DateTimeKind.Utc from DateTimeKind.Local) + // 2017-06-12T05:30:45.7680000 (interpreted as local time wrt to current time zone) + // + private static bool TryParseDateTimeOffsetO(ReadOnlySpan<byte> source, out DateTimeOffset value, out int bytesConsumed, out DateTimeKind kind) + { + if (source.Length < 27) + { + value = default; + bytesConsumed = 0; + kind = default; + return false; + } + + int year; + { + uint digit1 = source[0] - 48u; // '0' + uint digit2 = source[1] - 48u; // '0' + uint digit3 = source[2] - 48u; // '0' + uint digit4 = source[3] - 48u; // '0' + + if (digit1 > 9 || digit2 > 9 || digit3 > 9 || digit4 > 9) + { + value = default; + bytesConsumed = 0; + kind = default; + return false; + } + + year = (int)(digit1 * 1000 + digit2 * 100 + digit3 * 10 + digit4); + } + + if (source[4] != Utf8Constants.Hyphen) + { + value = default; + bytesConsumed = 0; + kind = default; + return false; + } + + int month; + { + uint digit1 = source[5] - 48u; // '0' + uint digit2 = source[6] - 48u; // '0' + + if (digit1 > 9 || digit2 > 9) + { + value = default; + bytesConsumed = 0; + kind = default; + return false; + } + + month = (int)(digit1 * 10 + digit2); + } + + if (source[7] != Utf8Constants.Hyphen) + { + value = default; + bytesConsumed = 0; + kind = default; + return false; + } + + int day; + { + uint digit1 = source[8] - 48u; // '0' + uint digit2 = source[9] - 48u; // '0' + + if (digit1 > 9 || digit2 > 9) + { + value = default; + bytesConsumed = 0; + kind = default; + return false; + } + + day = (int)(digit1 * 10 + digit2); + } + + if (source[10] != 'T') + { + value = default; + bytesConsumed = 0; + kind = default; + return false; + } + + int hour; + { + uint digit1 = source[11] - 48u; // '0' + uint digit2 = source[12] - 48u; // '0' + + if (digit1 > 9 || digit2 > 9) + { + value = default; + bytesConsumed = 0; + kind = default; + return false; + } + + hour = (int)(digit1 * 10 + digit2); + } + + if (source[13] != Utf8Constants.Colon) + { + value = default; + bytesConsumed = 0; + kind = default; + return false; + } + + int minute; + { + uint digit1 = source[14] - 48u; // '0' + uint digit2 = source[15] - 48u; // '0' + + if (digit1 > 9 || digit2 > 9) + { + value = default; + bytesConsumed = 0; + kind = default; + return false; + } + + minute = (int)(digit1 * 10 + digit2); + } + + if (source[16] != Utf8Constants.Colon) + { + value = default; + bytesConsumed = 0; + kind = default; + return false; + } + + int second; + { + uint digit1 = source[17] - 48u; // '0' + uint digit2 = source[18] - 48u; // '0' + + if (digit1 > 9 || digit2 > 9) + { + value = default; + bytesConsumed = 0; + kind = default; + return false; + } + + second = (int)(digit1 * 10 + digit2); + } + + if (source[19] != Utf8Constants.Period) + { + value = default; + bytesConsumed = 0; + kind = default; + return false; + } + + int fraction; + { + uint digit1 = source[20] - 48u; // '0' + uint digit2 = source[21] - 48u; // '0' + uint digit3 = source[22] - 48u; // '0' + uint digit4 = source[23] - 48u; // '0' + uint digit5 = source[24] - 48u; // '0' + uint digit6 = source[25] - 48u; // '0' + uint digit7 = source[26] - 48u; // '0' + + if (digit1 > 9 || digit2 > 9 || digit3 > 9 || digit4 > 9 || digit5 > 9 || digit6 > 9 || digit7 > 9) + { + value = default; + bytesConsumed = 0; + kind = default; + return false; + } + + fraction = (int)(digit1 * 1000000 + digit2 * 100000 + digit3 * 10000 + digit4 * 1000 + digit5 * 100 + digit6 * 10 + digit7); + } + + byte offsetChar = (source.Length <= 27) ? default : source[27]; + if (offsetChar != 'Z' && offsetChar != Utf8Constants.Plus && offsetChar != Utf8Constants.Minus) + { + if (!TryCreateDateTimeOffsetInterpretingDataAsLocalTime(year: year, month: month, day: day, hour: hour, minute: minute, second: second, fraction: fraction, out value)) + { + value = default; + bytesConsumed = 0; + kind = default; + return false; + } + bytesConsumed = 27; + kind = DateTimeKind.Unspecified; + return true; + } + + if (offsetChar == 'Z') + { + // Same as specifying an offset of "+00:00", except that DateTime's Kind gets set to UTC rather than Local + if (!TryCreateDateTimeOffset(year: year, month: month, day: day, hour: hour, minute: minute, second: second, fraction: fraction, offsetNegative: false, offsetHours: 0, offsetMinutes: 0, out value)) + { + value = default; + bytesConsumed = 0; + kind = default; + return false; + } + + bytesConsumed = 28; + kind = DateTimeKind.Utc; + return true; + } + + Debug.Assert(offsetChar == Utf8Constants.Plus || offsetChar == Utf8Constants.Minus); + if (source.Length < 33) + { + value = default; + bytesConsumed = 0; + kind = default; + return false; + } + + int offsetHours; + { + uint digit1 = source[28] - 48u; // '0' + uint digit2 = source[29] - 48u; // '0' + + if (digit1 > 9 || digit2 > 9) + { + value = default; + bytesConsumed = 0; + kind = default; + return false; + } + + offsetHours = (int)(digit1 * 10 + digit2); + } + + if (source[30] != Utf8Constants.Colon) + { + value = default; + bytesConsumed = 0; + kind = default; + return false; + } + + int offsetMinutes; + { + uint digit1 = source[31] - 48u; // '0' + uint digit2 = source[32] - 48u; // '0' + + if (digit1 > 9 || digit2 > 9) + { + value = default; + bytesConsumed = 0; + kind = default; + return false; + } + + offsetMinutes = (int)(digit1 * 10 + digit2); + } + + if (!TryCreateDateTimeOffset(year: year, month: month, day: day, hour: hour, minute: minute, second: second, fraction: fraction, offsetNegative: offsetChar == Utf8Constants.Minus, offsetHours: offsetHours, offsetMinutes: offsetMinutes, out value)) + { + value = default; + bytesConsumed = 0; + kind = default; + return false; + } + + bytesConsumed = 33; + kind = DateTimeKind.Local; + return true; + } + } +} diff --git a/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Date.R.cs b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Date.R.cs new file mode 100644 index 0000000000..316bee01b4 --- /dev/null +++ b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Date.R.cs @@ -0,0 +1,221 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Buffers.Text +{ + public static partial class Utf8Parser + { + // + // Parse an RFC1123 date string. + // + // 01234567890123456789012345678 + // ----------------------------- + // Tue, 03 Jan 2017 08:08:05 GMT + // + private static bool TryParseDateTimeOffsetR(ReadOnlySpan<byte> source, uint caseFlipXorMask, out DateTimeOffset dateTimeOffset, out int bytesConsumed) + { + if (source.Length < 29) + { + bytesConsumed = 0; + dateTimeOffset = default; + return false; + } + + DayOfWeek dayOfWeek; + { + uint dow0 = source[0] ^ caseFlipXorMask; + uint dow1 = source[1]; + uint dow2 = source[2]; + uint comma = source[3]; + uint dowString = (dow0 << 24) | (dow1 << 16) | (dow2 << 8) | comma; + switch (dowString) + { + case 0x53756E2c /* 'Sun,' */: dayOfWeek = DayOfWeek.Sunday; break; + case 0x4d6f6e2c /* 'Mon,' */: dayOfWeek = DayOfWeek.Monday; break; + case 0x5475652c /* 'Tue,' */: dayOfWeek = DayOfWeek.Tuesday; break; + case 0x5765642c /* 'Wed,' */: dayOfWeek = DayOfWeek.Wednesday; break; + case 0x5468752c /* 'Thu,' */: dayOfWeek = DayOfWeek.Thursday; break; + case 0x4672692c /* 'Fri,' */: dayOfWeek = DayOfWeek.Friday; break; + case 0x5361742c /* 'Sat,' */: dayOfWeek = DayOfWeek.Saturday; break; + default: + bytesConsumed = 0; + dateTimeOffset = default; + return false; + } + } + + if (source[4] != Utf8Constants.Space) + { + bytesConsumed = 0; + dateTimeOffset = default; + return false; + } + + int day; + { + uint digit1 = source[5] - 48u; // '0' + uint digit2 = source[6] - 48u; // '0' + + if (digit1 > 9 || digit2 > 9) + { + bytesConsumed = 0; + dateTimeOffset = default; + return false; + } + + day = (int)(digit1 * 10 + digit2); + } + + if (source[7] != Utf8Constants.Space) + { + bytesConsumed = 0; + dateTimeOffset = default; + return false; + } + + int month; + { + uint mon0 = source[8] ^ caseFlipXorMask; + uint mon1 = source[9]; + uint mon2 = source[10]; + uint space = source[11]; + uint monthString = (mon0 << 24) | (mon1 << 16) | (mon2 << 8) | space; + switch (monthString) + { + case 0x4a616e20 /* 'Jan ' */ : month = 1; break; + case 0x46656220 /* 'Feb ' */ : month = 2; break; + case 0x4d617220 /* 'Mar ' */ : month = 3; break; + case 0x41707220 /* 'Apr ' */ : month = 4; break; + case 0x4d617920 /* 'May ' */ : month = 5; break; + case 0x4a756e20 /* 'Jun ' */ : month = 6; break; + case 0x4a756c20 /* 'Jul ' */ : month = 7; break; + case 0x41756720 /* 'Aug ' */ : month = 8; break; + case 0x53657020 /* 'Sep ' */ : month = 9; break; + case 0x4f637420 /* 'Oct ' */ : month = 10; break; + case 0x4e6f7620 /* 'Nov ' */ : month = 11; break; + case 0x44656320 /* 'Dec ' */ : month = 12; break; + default: + bytesConsumed = 0; + dateTimeOffset = default; + return false; + } + } + + int year; + { + uint digit1 = source[12] - 48u; // '0' + uint digit2 = source[13] - 48u; // '0' + uint digit3 = source[14] - 48u; // '0' + uint digit4 = source[15] - 48u; // '0' + + if (digit1 > 9 || digit2 > 9 || digit3 > 9 || digit4 > 9) + { + bytesConsumed = 0; + dateTimeOffset = default; + return false; + } + + year = (int)(digit1 * 1000 + digit2 * 100 + digit3 * 10 + digit4); + } + + if (source[16] != Utf8Constants.Space) + { + bytesConsumed = 0; + dateTimeOffset = default; + return false; + } + + int hour; + { + uint digit1 = source[17] - 48u; // '0' + uint digit2 = source[18] - 48u; // '0' + + if (digit1 > 9 || digit2 > 9) + { + bytesConsumed = 0; + dateTimeOffset = default; + return false; + } + + hour = (int)(digit1 * 10 + digit2); + } + + if (source[19] != Utf8Constants.Colon) + { + bytesConsumed = 0; + dateTimeOffset = default; + return false; + } + + int minute; + { + uint digit1 = source[20] - 48u; // '0' + uint digit2 = source[21] - 48u; // '0' + + if (digit1 > 9 || digit2 > 9) + { + bytesConsumed = 0; + dateTimeOffset = default; + return false; + } + + minute = (int)(digit1 * 10 + digit2); + } + + if (source[22] != Utf8Constants.Colon) + { + bytesConsumed = 0; + dateTimeOffset = default; + return false; + } + + int second; + { + uint digit1 = source[23] - 48u; // '0' + uint digit2 = source[24] - 48u; // '0' + + if (digit1 > 9 || digit2 > 9) + { + bytesConsumed = 0; + dateTimeOffset = default; + return false; + } + + second = (int)(digit1 * 10 + digit2); + } + + { + uint space = source[25]; + uint g = source[26] ^ caseFlipXorMask; + uint m = source[27] ^ caseFlipXorMask; + uint t = source[28] ^ caseFlipXorMask; + uint gmtString = (space << 24) | (g << 16) | (m << 8) | t; + if (gmtString != 0x20474d54 /* ' GMT' */) + { + bytesConsumed = 0; + dateTimeOffset = default; + return false; + } + } + + if (!TryCreateDateTimeOffset(year: year, month: month, day: day, hour: hour, minute: minute, second: second, fraction: 0, offsetNegative: false, offsetHours: 0, offsetMinutes: 0, out dateTimeOffset)) + { + bytesConsumed = 0; + dateTimeOffset = default; + return false; + } + + if (dayOfWeek != dateTimeOffset.DayOfWeek) + { + // If we got here, the day of week did not match the actual date. + bytesConsumed = 0; + dateTimeOffset = default; + return false; + } + + bytesConsumed = 29; + return true; + } + } +} diff --git a/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Date.cs b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Date.cs new file mode 100644 index 0000000000..f103492461 --- /dev/null +++ b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Date.cs @@ -0,0 +1,149 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; + +namespace System.Buffers.Text +{ + public static partial class Utf8Parser + { + /// <summary> + /// Parses a DateTime at the start of a Utf8 string. + /// </summary> + /// <param name="source">The Utf8 string to parse</param> + /// <param name="value">Receives the parsed value</param> + /// <param name="bytesConsumed">On a successful parse, receives the length in bytes of the substring that was parsed </param> + /// <param name="standardFormat">Expected format of the Utf8 string</param> + /// <returns> + /// true for success. "bytesConsumed" contains the length in bytes of the substring that was parsed. + /// false if the string was not syntactically valid or an overflow or underflow occurred. "bytesConsumed" is set to 0. + /// </returns> + /// <remarks> + /// Formats supported: + /// default 05/25/2017 10:30:15 -08:00 + /// G 05/25/2017 10:30:15 + /// R Tue, 03 Jan 2017 08:08:05 GMT (RFC 1123) + /// l tue, 03 jan 2017 08:08:05 gmt (Lowercase RFC 1123) + /// O 2017-06-12T05:30:45.7680000-07:00 (Round-trippable) + /// </remarks> + /// <exceptions> + /// <cref>System.FormatException</cref> if the format is not valid for this data type. + /// </exceptions> + public static bool TryParse(ReadOnlySpan<byte> source, out DateTime value, out int bytesConsumed, char standardFormat = default) + { + switch (standardFormat) + { + case 'R': + { + if (!TryParseDateTimeOffsetR(source, NoFlipCase, out DateTimeOffset dateTimeOffset, out bytesConsumed)) + { + value = default; + return false; + } + value = dateTimeOffset.DateTime; // (returns a DateTimeKind.Unspecified to match DateTime.ParseExact(). Maybe better to return UtcDateTime instead?) + return true; + } + + case 'l': + { + if (!TryParseDateTimeOffsetR(source, FlipCase, out DateTimeOffset dateTimeOffset, out bytesConsumed)) + { + value = default; + return false; + } + value = dateTimeOffset.DateTime; // (returns a DateTimeKind.Unspecified to match DateTime.ParseExact(). Maybe better to return UtcDateTime instead?) + return true; + } + + case 'O': + { + // Emulates DateTime.ParseExact(text, "O", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind) + // In particular, the formatted string "encodes" the DateTimeKind according to the following table: + // + // 2017-06-12T05:30:45.7680000 - Unspecified + // 2017-06-12T05:30:45.7680000+00:00 - Local + // 2017-06-12T05:30:45.7680000Z - Utc + + if (!TryParseDateTimeOffsetO(source, out DateTimeOffset dateTimeOffset, out bytesConsumed, out DateTimeKind kind)) + { + value = default; + bytesConsumed = 0; + return false; + } + + switch (kind) + { + case DateTimeKind.Local: + value = dateTimeOffset.LocalDateTime; + break; + case DateTimeKind.Utc: + value = dateTimeOffset.UtcDateTime; + break; + default: + Debug.Assert(kind == DateTimeKind.Unspecified); + value = dateTimeOffset.DateTime; + break; + } + + return true; + } + + case default(char): + case 'G': + return TryParseDateTimeG(source, out value, out _, out bytesConsumed); + + default: + return ThrowHelper.TryParseThrowFormatException(out value, out bytesConsumed); + } + } + + /// <summary> + /// Parses a DateTimeOffset at the start of a Utf8 string. + /// </summary> + /// <param name="source">The Utf8 string to parse</param> + /// <param name="value">Receives the parsed value</param> + /// <param name="bytesConsumed">On a successful parse, receives the length in bytes of the substring that was parsed </param> + /// <param name="standardFormat">Expected format of the Utf8 string</param> + /// <returns> + /// true for success. "bytesConsumed" contains the length in bytes of the substring that was parsed. + /// false if the string was not syntactically valid or an overflow or underflow occurred. "bytesConsumed" is set to 0. + /// </returns> + /// <remarks> + /// Formats supported: + /// G (default) 05/25/2017 10:30:15 + /// R Tue, 03 Jan 2017 08:08:05 GMT (RFC 1123) + /// l tue, 03 jan 2017 08:08:05 gmt (Lowercase RFC 1123) + /// O 2017-06-12T05:30:45.7680000-07:00 (Round-trippable) + /// </remarks> + /// <exceptions> + /// <cref>System.FormatException</cref> if the format is not valid for this data type. + /// </exceptions> + public static bool TryParse(ReadOnlySpan<byte> source, out DateTimeOffset value, out int bytesConsumed, char standardFormat = default) + { + switch (standardFormat) + { + case 'R': + return TryParseDateTimeOffsetR(source, NoFlipCase, out value, out bytesConsumed); + + case 'l': + return TryParseDateTimeOffsetR(source, FlipCase, out value, out bytesConsumed); + + case 'O': + return TryParseDateTimeOffsetO(source, out value, out bytesConsumed, out _); + + case default(char): + return TryParseDateTimeOffsetDefault(source, out value, out bytesConsumed); + + case 'G': + return TryParseDateTimeG(source, out DateTime _, out value, out bytesConsumed); + + default: + return ThrowHelper.TryParseThrowFormatException(out value, out bytesConsumed); + } + } + + private const uint FlipCase = 0x00000020u; // XOR mask to flip the case of a letter. + private const uint NoFlipCase = 0x00000000u; + } +} diff --git a/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Decimal.cs b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Decimal.cs new file mode 100644 index 0000000000..c0f1e0c040 --- /dev/null +++ b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Decimal.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Buffers.Text +{ + public static partial class Utf8Parser + { + /// <summary> + /// Parses a Decimal at the start of a Utf8 string. + /// </summary> + /// <param name="source">The Utf8 string to parse</param> + /// <param name="value">Receives the parsed value</param> + /// <param name="bytesConsumed">On a successful parse, receives the length in bytes of the substring that was parsed </param> + /// <param name="standardFormat">Expected format of the Utf8 string</param> + /// <returns> + /// true for success. "bytesConsumed" contains the length in bytes of the substring that was parsed. + /// false if the string was not syntactically valid or an overflow or underflow occurred. "bytesConsumed" is set to 0. + /// </returns> + /// <remarks> + /// Formats supported: + /// G/g (default) + /// F/f 12.45 Fixed point + /// E/e 1.245000e1 Exponential + /// </remarks> + /// <exceptions> + /// <cref>System.FormatException</cref> if the format is not valid for this data type. + /// </exceptions> + public static bool TryParse(ReadOnlySpan<byte> source, out decimal value, out int bytesConsumed, char standardFormat = default) + { + ParseNumberOptions options; + switch (standardFormat) + { + case default(char): + case 'G': + case 'g': + case 'E': + case 'e': + options = ParseNumberOptions.AllowExponent; + break; + + case 'F': + case 'f': + options = default; + break; + + default: + return ThrowHelper.TryParseThrowFormatException(out value, out bytesConsumed); + } + + NumberBuffer number = default; + if (!TryParseNumber(source, ref number, out bytesConsumed, options, out bool textUsedExponentNotation)) + { + value = default; + return false; + } + + if ((!textUsedExponentNotation) && (standardFormat == 'E' || standardFormat == 'e')) + { + value = default; + bytesConsumed = 0; + return false; + } + + // More compat with .NET behavior - whether or not a 0 keeps the negative sign depends on whether it an "integer" 0 or a "fractional" 0 + if (number.Digits[0] == 0 && number.Scale == 0) + { + number.IsNegative = false; + } + + value = default; + if (!Number.NumberBufferToDecimal(ref number, ref value)) + { + value = default; + bytesConsumed = 0; + return false; + } + + return true; + } + } +} diff --git a/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Float.cs b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Float.cs new file mode 100644 index 0000000000..1bdc59d237 --- /dev/null +++ b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Float.cs @@ -0,0 +1,148 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Buffers.Text +{ + public static partial class Utf8Parser + { + /// <summary> + /// Parses a Single at the start of a Utf8 string. + /// </summary> + /// <param name="source">The Utf8 string to parse</param> + /// <param name="value">Receives the parsed value</param> + /// <param name="bytesConsumed">On a successful parse, receives the length in bytes of the substring that was parsed </param> + /// <param name="standardFormat">Expected format of the Utf8 string</param> + /// <returns> + /// true for success. "bytesConsumed" contains the length in bytes of the substring that was parsed. + /// false if the string was not syntactically valid or an overflow or underflow occurred. "bytesConsumed" is set to 0. + /// </returns> + /// <remarks> + /// Formats supported: + /// G/g (default) + /// F/f 12.45 Fixed point + /// E/e 1.245000e1 Exponential + /// </remarks> + /// <exceptions> + /// <cref>System.FormatException</cref> if the format is not valid for this data type. + /// </exceptions> + public static bool TryParse(ReadOnlySpan<byte> source, out float value, out int bytesConsumed, char standardFormat = default) + { + if (TryParseNormalAsFloatingPoint(source, out double d, out bytesConsumed, standardFormat)) + { + value = (float)(d); + return true; + } + + return TryParseAsSpecialFloatingPoint(source, float.PositiveInfinity, float.NegativeInfinity, float.NaN, out value, out bytesConsumed); + } + + /// <summary> + /// Parses a Double at the start of a Utf8 string. + /// </summary> + /// <param name="source">The Utf8 string to parse</param> + /// <param name="value">Receives the parsed value</param> + /// <param name="bytesConsumed">On a successful parse, receives the length in bytes of the substring that was parsed </param> + /// <param name="standardFormat">Expected format of the Utf8 string</param> + /// <returns> + /// true for success. "bytesConsumed" contains the length in bytes of the substring that was parsed. + /// false if the string was not syntactically valid or an overflow or underflow occurred. "bytesConsumed" is set to 0. + /// </returns> + /// <remarks> + /// Formats supported: + /// G/g (default) + /// F/f 12.45 Fixed point + /// E/e 1.245000e1 Exponential + /// </remarks> + /// <exceptions> + /// <cref>System.FormatException</cref> if the format is not valid for this data type. + /// </exceptions> + public static bool TryParse(ReadOnlySpan<byte> source, out double value, out int bytesConsumed, char standardFormat = default) + { + if (TryParseNormalAsFloatingPoint(source, out value, out bytesConsumed, standardFormat)) + return true; + + return TryParseAsSpecialFloatingPoint(source, double.PositiveInfinity, double.NegativeInfinity, double.NaN, out value, out bytesConsumed); + } + + // + // Attempt to parse the regular floating points (the ones without names like "Infinity" and "NaN") + // + private static bool TryParseNormalAsFloatingPoint(ReadOnlySpan<byte> source, out double value, out int bytesConsumed, char standardFormat) + { + ParseNumberOptions options; + switch (standardFormat) + { + case default(char): + case 'G': + case 'g': + case 'E': + case 'e': + options = ParseNumberOptions.AllowExponent; + break; + + case 'F': + case 'f': + options = default; + break; + + default: + return ThrowHelper.TryParseThrowFormatException(out value, out bytesConsumed); + } + + NumberBuffer number = default; + if (!TryParseNumber(source, ref number, out bytesConsumed, options, out bool textUsedExponentNotation)) + { + value = default; + return false; + } + + if ((!textUsedExponentNotation) && (standardFormat == 'E' || standardFormat == 'e')) + { + value = default; + bytesConsumed = 0; + return false; + } + + value = Number.NumberBufferToDouble(ref number); + return true; + } + + // + // Assuming the text doesn't look like a normal floating point, we attempt to parse it as one the special floating point values. + // + private static bool TryParseAsSpecialFloatingPoint<T>(ReadOnlySpan<byte> source, T positiveInfinity, T negativeInfinity, T nan, out T value, out int bytesConsumed) + { + if (source.Length >= 8 && + source[0] == 'I' && source[1] == 'n' && source[2] == 'f' && source[3] == 'i' && + source[4] == 'n' && source[5] == 'i' && source[6] == 't' && source[7] == 'y') + { + value = positiveInfinity; + bytesConsumed = 8; + return true; + } + + if (source.Length >= 9 && + source[0] == Utf8Constants.Minus && + source[1] == 'I' && source[2] == 'n' && source[3] == 'f' && source[4] == 'i' && + source[5] == 'n' && source[6] == 'i' && source[7] == 't' && source[8] == 'y') + { + value = negativeInfinity; + bytesConsumed = 9; + return true; + } + + if (source.Length >= 3 && + source[0] == 'N' && source[1] == 'a' && source[2] == 'N') + { + value = nan; + bytesConsumed = 3; + return true; + } + + value = default; + bytesConsumed = 0; + return false; + } + } +} diff --git a/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Guid.cs b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Guid.cs new file mode 100644 index 0000000000..17dec828bc --- /dev/null +++ b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Guid.cs @@ -0,0 +1,243 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Buffers.Text +{ + public static partial class Utf8Parser + { + /// <summary> + /// Parses a Guid at the start of a Utf8 string. + /// </summary> + /// <param name="source">The Utf8 string to parse</param> + /// <param name="value">Receives the parsed value</param> + /// <param name="bytesConsumed">On a successful parse, receives the length in bytes of the substring that was parsed </param> + /// <param name="standardFormat">Expected format of the Utf8 string</param> + /// <returns> + /// true for success. "bytesConsumed" contains the length in bytes of the substring that was parsed. + /// false if the string was not syntactically valid or an overflow or underflow occurred. "bytesConsumed" is set to 0. + /// </returns> + /// <remarks> + /// Formats supported: + /// D (default) nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn + /// B {nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn} + /// P (nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn) + /// N nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn + /// </remarks> + /// <exceptions> + /// <cref>System.FormatException</cref> if the format is not valid for this data type. + /// </exceptions> + public static bool TryParse(ReadOnlySpan<byte> source, out Guid value, out int bytesConsumed, char standardFormat = default) + { + switch (standardFormat) + { + case default(char): + case 'D': + return TryParseGuidCore(source, false, ' ', ' ', out value, out bytesConsumed); + case 'B': + return TryParseGuidCore(source, true, '{', '}', out value, out bytesConsumed); + case 'P': + return TryParseGuidCore(source, true, '(', ')', out value, out bytesConsumed); + case 'N': + return TryParseGuidN(source, out value, out bytesConsumed); + default: + return ThrowHelper.TryParseThrowFormatException(out value, out bytesConsumed); + } + } + + // nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn (not very Guid-like, but the format is what it is...) + private static bool TryParseGuidN(ReadOnlySpan<byte> text, out Guid value, out int bytesConsumed) + { + if (text.Length < 32) + { + value = default; + bytesConsumed = 0; + return false; + } + + if (!TryParseUInt32X(text.Slice(0, 8), out uint i1, out int justConsumed) || justConsumed != 8) + { + value = default; + bytesConsumed = 0; + return false; // 8 digits + } + + if (!TryParseUInt16X(text.Slice(8, 4), out ushort i2, out justConsumed) || justConsumed != 4) + { + value = default; + bytesConsumed = 0; + return false; // next 4 digits + } + + if (!TryParseUInt16X(text.Slice(12, 4), out ushort i3, out justConsumed) || justConsumed != 4) + { + value = default; + bytesConsumed = 0; + return false; // next 4 digits + } + + if (!TryParseUInt16X(text.Slice(16, 4), out ushort i4, out justConsumed) || justConsumed != 4) + { + value = default; + bytesConsumed = 0; + return false; // next 4 digits + } + + if (!TryParseUInt64X(text.Slice(20), out ulong i5, out justConsumed) || justConsumed != 12) + { + value = default; + bytesConsumed = 0; + return false; // next 4 digits + } + + bytesConsumed = 32; + value = new Guid((int)i1, (short)i2, (short)i3, (byte)(i4 >> 8), (byte)i4, + (byte)(i5 >> 40), (byte)(i5 >> 32), (byte)(i5 >> 24), (byte)(i5 >> 16), (byte)(i5 >> 8), (byte)i5); + return true; + } + + // {8-4-4-4-12}, where number is the number of hex digits, and {/} are ends. + private static bool TryParseGuidCore(ReadOnlySpan<byte> source, bool ends, char begin, char end, out Guid value, out int bytesConsumed) + { + int expectedCodingUnits = 36 + (ends ? 2 : 0); // 32 hex digits + 4 delimiters + 2 optional ends + + if (source.Length < expectedCodingUnits) + { + value = default; + bytesConsumed = 0; + return false; + } + + if (ends) + { + if (source[0] != begin) + { + value = default; + bytesConsumed = 0; + return false; + } + + source = source.Slice(1); // skip begining + } + + if (!TryParseUInt32X(source, out uint i1, out int justConsumed)) + { + value = default; + bytesConsumed = 0; + return false; + } + + if (justConsumed != 8) + { + value = default; + bytesConsumed = 0; + return false; // 8 digits + } + + if (source[justConsumed] != '-') + { + value = default; + bytesConsumed = 0; + return false; + } + + source = source.Slice(9); // justConsumed + 1 for delimiter + + if (!TryParseUInt16X(source, out ushort i2, out justConsumed)) + { + value = default; + bytesConsumed = 0; + return false; + } + + if (justConsumed != 4) + { + value = default; + bytesConsumed = 0; + return false; // 4 digits + } + + if (source[justConsumed] != '-') + { + value = default; + bytesConsumed = 0; + return false; + } + + source = source.Slice(5); // justConsumed + 1 for delimiter + + if (!TryParseUInt16X(source, out ushort i3, out justConsumed)) + { + value = default; + bytesConsumed = 0; + return false; + } + + if (justConsumed != 4) + { + value = default; + bytesConsumed = 0; + return false; // 4 digits + } + + if (source[justConsumed] != '-') + { + value = default; + bytesConsumed = 0; + return false; + } + + source = source.Slice(5); // justConsumed + 1 for delimiter + + if (!TryParseUInt16X(source, out ushort i4, out justConsumed)) + { + value = default; + bytesConsumed = 0; + return false; + } + + if (justConsumed != 4) + { + value = default; + bytesConsumed = 0; + return false; // 4 digits + } + + if (source[justConsumed] != '-') + { + value = default; + bytesConsumed = 0; + return false; + } + + source = source.Slice(5);// justConsumed + 1 for delimiter + + if (!TryParseUInt64X(source, out ulong i5, out justConsumed)) + { + value = default; + bytesConsumed = 0; + return false; + } + + if (justConsumed != 12) + { + value = default; + bytesConsumed = 0; + return false; // 12 digits + } + + if (ends && source[justConsumed] != end) + { + value = default; + bytesConsumed = 0; + return false; + } + + bytesConsumed = expectedCodingUnits; + value = new Guid((int)i1, (short)i2, (short)i3, (byte)(i4 >> 8), (byte)i4, + (byte)(i5 >> 40), (byte)(i5 >> 32), (byte)(i5 >> 24), (byte)(i5 >> 16), (byte)(i5 >> 8), (byte)i5); + + return true; + } + } +} diff --git a/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Integer.Signed.D.cs b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Integer.Signed.D.cs new file mode 100644 index 0000000000..bf1871a1c9 --- /dev/null +++ b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Integer.Signed.D.cs @@ -0,0 +1,443 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Buffers.Text +{ + public static partial class Utf8Parser + { + private static bool TryParseSByteD(ReadOnlySpan<byte> source, out sbyte value, out int bytesConsumed) + { + if (source.Length < 1) + goto FalseExit; + + int sign = 1; + int index = 0; + int num = source[index]; + if (num == '-') + { + sign = -1; + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + num = source[index]; + } + else if (num == '+') + { + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + num = source[index]; + } + + int answer = 0; + + if (ParserHelpers.IsDigit(num)) + { + if (num == '0') + { + do + { + index++; + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + } while (num == '0'); + if (!ParserHelpers.IsDigit(num)) + goto Done; + } + + answer = num - '0'; + index++; + + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = 10 * answer + num - '0'; + + // Potential overflow + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = answer * 10 + num - '0'; + // if sign < 0, (-1 * sign + 1) / 2 = 1 + // else, (-1 * sign + 1) / 2 = 0 + if ((uint)answer > (uint)sbyte.MaxValue + (-1 * sign + 1) / 2) + goto FalseExit; // Overflow + + if ((uint)index >= (uint)source.Length) + goto Done; + if (!ParserHelpers.IsDigit(source[index])) + goto Done; + + // Guaranteed overflow + goto FalseExit; + } + + FalseExit: + bytesConsumed = default; + value = default; + return false; + + Done: + bytesConsumed = index; + value = (sbyte)(answer * sign); + return true; + } + + private static bool TryParseInt16D(ReadOnlySpan<byte> source, out short value, out int bytesConsumed) + { + if (source.Length < 1) + goto FalseExit; + + int sign = 1; + int index = 0; + int num = source[index]; + if (num == '-') + { + sign = -1; + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + num = source[index]; + } + else if (num == '+') + { + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + num = source[index]; + } + + int answer = 0; + + if (ParserHelpers.IsDigit(num)) + { + if (num == '0') + { + do + { + index++; + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + } while (num == '0'); + if (!ParserHelpers.IsDigit(num)) + goto Done; + } + + answer = num - '0'; + index++; + + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = 10 * answer + num - '0'; + + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = 10 * answer + num - '0'; + + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = 10 * answer + num - '0'; + + // Potential overflow + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = answer * 10 + num - '0'; + // if sign < 0, (-1 * sign + 1) / 2 = 1 + // else, (-1 * sign + 1) / 2 = 0 + if ((uint)answer > (uint)short.MaxValue + (-1 * sign + 1) / 2) + goto FalseExit; // Overflow + + if ((uint)index >= (uint)source.Length) + goto Done; + if (!ParserHelpers.IsDigit(source[index])) + goto Done; + + // Guaranteed overflow + goto FalseExit; + } + + FalseExit: + bytesConsumed = default; + value = default; + return false; + + Done: + bytesConsumed = index; + value = (short)(answer * sign); + return true; + } + + private static bool TryParseInt32D(ReadOnlySpan<byte> source, out int value, out int bytesConsumed) + { + if (source.Length < 1) + goto FalseExit; + + int sign = 1; + int index = 0; + int num = source[index]; + if (num == '-') + { + sign = -1; + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + num = source[index]; + } + else if (num == '+') + { + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + num = source[index]; + } + + int answer = 0; + + if (ParserHelpers.IsDigit(num)) + { + if (num == '0') + { + do + { + index++; + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + } while (num == '0'); + if (!ParserHelpers.IsDigit(num)) + goto Done; + } + + answer = num - '0'; + index++; + + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = 10 * answer + num - '0'; + + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = 10 * answer + num - '0'; + + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = 10 * answer + num - '0'; + + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = 10 * answer + num - '0'; + + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = 10 * answer + num - '0'; + + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = 10 * answer + num - '0'; + + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = 10 * answer + num - '0'; + + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = 10 * answer + num - '0'; + + // Potential overflow + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + if (answer > int.MaxValue / 10) + goto FalseExit; // Overflow + answer = answer * 10 + num - '0'; + // if sign < 0, (-1 * sign + 1) / 2 = 1 + // else, (-1 * sign + 1) / 2 = 0 + if ((uint)answer > (uint)int.MaxValue + (-1 * sign + 1) / 2) + goto FalseExit; // Overflow + + if ((uint)index >= (uint)source.Length) + goto Done; + if (!ParserHelpers.IsDigit(source[index])) + goto Done; + + // Guaranteed overflow + goto FalseExit; + } + + FalseExit: + bytesConsumed = default; + value = default; + return false; + + Done: + bytesConsumed = index; + value = answer * sign; + return true; + } + + private static bool TryParseInt64D(ReadOnlySpan<byte> source, out long value, out int bytesConsumed) + { + if (source.Length < 1) + { + bytesConsumed = 0; + value = default; + return false; + } + + int indexOfFirstDigit = 0; + int sign = 1; + if (source[0] == '-') + { + indexOfFirstDigit = 1; + sign = -1; + + if (source.Length <= indexOfFirstDigit) + { + bytesConsumed = 0; + value = default; + return false; + } + } + else if (source[0] == '+') + { + indexOfFirstDigit = 1; + + if (source.Length <= indexOfFirstDigit) + { + bytesConsumed = 0; + value = default; + return false; + } + } + + int overflowLength = ParserHelpers.Int64OverflowLength + indexOfFirstDigit; + + // Parse the first digit separately. If invalid here, we need to return false. + long firstDigit = source[indexOfFirstDigit] - 48; // '0' + if (firstDigit < 0 || firstDigit > 9) + { + bytesConsumed = 0; + value = default; + return false; + } + ulong parsedValue = (ulong)firstDigit; + + if (source.Length < overflowLength) + { + // Length is less than Parsers.Int64OverflowLength; overflow is not possible + for (int index = indexOfFirstDigit + 1; index < source.Length; index++) + { + long nextDigit = source[index] - 48; // '0' + if (nextDigit < 0 || nextDigit > 9) + { + bytesConsumed = index; + value = ((long)parsedValue) * sign; + return true; + } + parsedValue = parsedValue * 10 + (ulong)nextDigit; + } + } + else + { + // Length is greater than Parsers.Int64OverflowLength; overflow is only possible after Parsers.Int64OverflowLength + // digits. There may be no overflow after Parsers.Int64OverflowLength if there are leading zeroes. + for (int index = indexOfFirstDigit + 1; index < overflowLength - 1; index++) + { + long nextDigit = source[index] - 48; // '0' + if (nextDigit < 0 || nextDigit > 9) + { + bytesConsumed = index; + value = ((long)parsedValue) * sign; + return true; + } + parsedValue = parsedValue * 10 + (ulong)nextDigit; + } + for (int index = overflowLength - 1; index < source.Length; index++) + { + long nextDigit = source[index] - 48; // '0' + if (nextDigit < 0 || nextDigit > 9) + { + bytesConsumed = index; + value = ((long)parsedValue) * sign; + return true; + } + // If parsedValue > (long.MaxValue / 10), any more appended digits will cause overflow. + // if parsedValue == (long.MaxValue / 10), any nextDigit greater than 7 or 8 (depending on sign) implies overflow. + bool positive = sign > 0; + bool nextDigitTooLarge = nextDigit > 8 || (positive && nextDigit > 7); + if (parsedValue > long.MaxValue / 10 || parsedValue == long.MaxValue / 10 && nextDigitTooLarge) + { + bytesConsumed = 0; + value = default; + return false; + } + parsedValue = parsedValue * 10 + (ulong)nextDigit; + } + } + + bytesConsumed = source.Length; + value = ((long)parsedValue) * sign; + return true; + } + } +} diff --git a/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Integer.Signed.N.cs b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Integer.Signed.N.cs new file mode 100644 index 0000000000..fd8ce572f2 --- /dev/null +++ b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Integer.Signed.N.cs @@ -0,0 +1,383 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Buffers.Text +{ + public static partial class Utf8Parser + { + private static bool TryParseSByteN(ReadOnlySpan<byte> source, out sbyte value, out int bytesConsumed) + { + if (source.Length < 1) + goto FalseExit; + + int sign = 1; + int index = 0; + int c = source[index]; + if (c == '-') + { + sign = -1; + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + c = source[index]; + } + else if (c == '+') + { + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + c = source[index]; + } + + int answer; + + // Handle the first digit (or period) as a special case. This ensures some compatible edge-case behavior with the classic parse routines + // (at least one digit must precede any commas, and a string without any digits prior to the decimal point must have at least + // one digit after the decimal point.) + if (c == Utf8Constants.Period) + goto FractionalPartWithoutLeadingDigits; + if (!ParserHelpers.IsDigit(c)) + goto FalseExit; + answer = c - '0'; + + for (; ; ) + { + index++; + if ((uint)index >= (uint)source.Length) + goto Done; + + c = source[index]; + if (c == Utf8Constants.Comma) + continue; + + if (c == Utf8Constants.Period) + goto FractionalDigits; + + if (!ParserHelpers.IsDigit(c)) + goto Done; + + answer = answer * 10 + c - '0'; + + // if sign < 0, (-1 * sign + 1) / 2 = 1 + // else, (-1 * sign + 1) / 2 = 0 + if (answer > sbyte.MaxValue + (-1 * sign + 1) / 2) + goto FalseExit; // Overflow + } + + FractionalPartWithoutLeadingDigits: // If we got here, we found a decimal point before we found any digits. This is legal as long as there's at least one zero after the decimal point. + answer = 0; + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + if (source[index] != '0') + goto FalseExit; + + FractionalDigits: // "N" format allows a fractional portion despite being an integer format but only if the post-fraction digits are all 0. + do + { + index++; + if ((uint)index >= (uint)source.Length) + goto Done; + c = source[index]; + } + while (c == '0'); + + if (ParserHelpers.IsDigit(c)) + goto FalseExit; // The fractional portion contained a non-zero digit. Treat this as an error, not an early termination. + goto Done; + + FalseExit: + bytesConsumed = default; + value = default; + return false; + + Done: + bytesConsumed = index; + value = (sbyte)(answer * sign); + return true; + } + + private static bool TryParseInt16N(ReadOnlySpan<byte> source, out short value, out int bytesConsumed) + { + if (source.Length < 1) + goto FalseExit; + + int sign = 1; + int index = 0; + int c = source[index]; + if (c == '-') + { + sign = -1; + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + c = source[index]; + } + else if (c == '+') + { + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + c = source[index]; + } + + int answer; + + // Handle the first digit (or period) as a special case. This ensures some compatible edge-case behavior with the classic parse routines + // (at least one digit must precede any commas, and a string without any digits prior to the decimal point must have at least + // one digit after the decimal point.) + if (c == Utf8Constants.Period) + goto FractionalPartWithoutLeadingDigits; + if (!ParserHelpers.IsDigit(c)) + goto FalseExit; + answer = c - '0'; + + for (; ; ) + { + index++; + if ((uint)index >= (uint)source.Length) + goto Done; + + c = source[index]; + if (c == Utf8Constants.Comma) + continue; + + if (c == Utf8Constants.Period) + goto FractionalDigits; + + if (!ParserHelpers.IsDigit(c)) + goto Done; + + answer = answer * 10 + c - '0'; + + // if sign < 0, (-1 * sign + 1) / 2 = 1 + // else, (-1 * sign + 1) / 2 = 0 + if (answer > short.MaxValue + (-1 * sign + 1) / 2) + goto FalseExit; // Overflow + } + + FractionalPartWithoutLeadingDigits: // If we got here, we found a decimal point before we found any digits. This is legal as long as there's at least one zero after the decimal point. + answer = 0; + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + if (source[index] != '0') + goto FalseExit; + + FractionalDigits: // "N" format allows a fractional portion despite being an integer format but only if the post-fraction digits are all 0. + do + { + index++; + if ((uint)index >= (uint)source.Length) + goto Done; + c = source[index]; + } + while (c == '0'); + + if (ParserHelpers.IsDigit(c)) + goto FalseExit; // The fractional portion contained a non-zero digit. Treat this as an error, not an early termination. + goto Done; + + FalseExit: + bytesConsumed = default; + value = default; + return false; + + Done: + bytesConsumed = index; + value = (short)(answer * sign); + return true; + } + + private static bool TryParseInt32N(ReadOnlySpan<byte> source, out int value, out int bytesConsumed) + { + if (source.Length < 1) + goto FalseExit; + + int sign = 1; + int index = 0; + int c = source[index]; + if (c == '-') + { + sign = -1; + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + c = source[index]; + } + else if (c == '+') + { + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + c = source[index]; + } + + int answer; + + // Handle the first digit (or period) as a special case. This ensures some compatible edge-case behavior with the classic parse routines + // (at least one digit must precede any commas, and a string without any digits prior to the decimal point must have at least + // one digit after the decimal point.) + if (c == Utf8Constants.Period) + goto FractionalPartWithoutLeadingDigits; + if (!ParserHelpers.IsDigit(c)) + goto FalseExit; + answer = c - '0'; + + for (; ; ) + { + index++; + if ((uint)index >= (uint)source.Length) + goto Done; + + c = source[index]; + if (c == Utf8Constants.Comma) + continue; + + if (c == Utf8Constants.Period) + goto FractionalDigits; + + if (!ParserHelpers.IsDigit(c)) + goto Done; + + if (((uint)answer) > int.MaxValue / 10) + goto FalseExit; + + answer = answer * 10 + c - '0'; + + // if sign < 0, (-1 * sign + 1) / 2 = 1 + // else, (-1 * sign + 1) / 2 = 0 + if ((uint)answer > (uint)int.MaxValue + (-1 * sign + 1) / 2) + goto FalseExit; // Overflow + } + + FractionalPartWithoutLeadingDigits: // If we got here, we found a decimal point before we found any digits. This is legal as long as there's at least one zero after the decimal point. + answer = 0; + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + if (source[index] != '0') + goto FalseExit; + + FractionalDigits: // "N" format allows a fractional portion despite being an integer format but only if the post-fraction digits are all 0. + do + { + index++; + if ((uint)index >= (uint)source.Length) + goto Done; + c = source[index]; + } + while (c == '0'); + + if (ParserHelpers.IsDigit(c)) + goto FalseExit; // The fractional portion contained a non-zero digit. Treat this as an error, not an early termination. + goto Done; + + FalseExit: + bytesConsumed = default; + value = default; + return false; + + Done: + bytesConsumed = index; + value = answer * sign; + return true; + } + + private static bool TryParseInt64N(ReadOnlySpan<byte> source, out long value, out int bytesConsumed) + { + if (source.Length < 1) + goto FalseExit; + + int sign = 1; + int index = 0; + int c = source[index]; + if (c == '-') + { + sign = -1; + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + c = source[index]; + } + else if (c == '+') + { + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + c = source[index]; + } + + long answer; + + // Handle the first digit (or period) as a special case. This ensures some compatible edge-case behavior with the classic parse routines + // (at least one digit must precede any commas, and a string without any digits prior to the decimal point must have at least + // one digit after the decimal point.) + if (c == Utf8Constants.Period) + goto FractionalPartWithoutLeadingDigits; + if (!ParserHelpers.IsDigit(c)) + goto FalseExit; + answer = c - '0'; + + for (; ; ) + { + index++; + if ((uint)index >= (uint)source.Length) + goto Done; + + c = source[index]; + if (c == Utf8Constants.Comma) + continue; + + if (c == Utf8Constants.Period) + goto FractionalDigits; + + if (!ParserHelpers.IsDigit(c)) + goto Done; + + if (((ulong)answer) > long.MaxValue / 10) + goto FalseExit; + + answer = answer * 10 + c - '0'; + + // if sign < 0, (-1 * sign + 1) / 2 = 1 + // else, (-1 * sign + 1) / 2 = 0 + if ((ulong)answer > (ulong)(long.MaxValue + (-1 * sign + 1) / 2)) + goto FalseExit; // Overflow + } + + FractionalPartWithoutLeadingDigits: // If we got here, we found a decimal point before we found any digits. This is legal as long as there's at least one zero after the decimal point. + answer = 0; + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + if (source[index] != '0') + goto FalseExit; + + FractionalDigits: // "N" format allows a fractional portion despite being an integer format but only if the post-fraction digits are all 0. + do + { + index++; + if ((uint)index >= (uint)source.Length) + goto Done; + c = source[index]; + } + while (c == '0'); + + if (ParserHelpers.IsDigit(c)) + goto FalseExit; // The fractional portion contained a non-zero digit. Treat this as an error, not an early termination. + goto Done; + + FalseExit: + bytesConsumed = default; + value = default; + return false; + + Done: + bytesConsumed = index; + value = answer * sign; + return true; + } + } +} diff --git a/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Integer.Signed.cs b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Integer.Signed.cs new file mode 100644 index 0000000000..b30291c6f2 --- /dev/null +++ b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Integer.Signed.cs @@ -0,0 +1,199 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Internal.Runtime.CompilerServices; + +namespace System.Buffers.Text +{ + /// <summary> + /// Methods to parse common data types to Utf8 strings. + /// </summary> + public static partial class Utf8Parser + { + /// <summary> + /// Parses a SByte at the start of a Utf8 string. + /// </summary> + /// <param name="source">The Utf8 string to parse</param> + /// <param name="value">Receives the parsed value</param> + /// <param name="bytesConsumed">On a successful parse, receives the length in bytes of the substring that was parsed </param> + /// <param name="standardFormat">Expected format of the Utf8 string</param> + /// <returns> + /// true for success. "bytesConsumed" contains the length in bytes of the substring that was parsed. + /// false if the string was not syntactically valid or an overflow or underflow occurred. "bytesConsumed" is set to 0. + /// </returns> + /// <remarks> + /// Formats supported: + /// G/g (default) + /// D/d 32767 + /// N/n 32,767 + /// X/x 7fff + /// </remarks> + /// <exceptions> + /// <cref>System.FormatException</cref> if the format is not valid for this data type. + /// </exceptions> + [CLSCompliant(false)] + public static bool TryParse(ReadOnlySpan<byte> source, out sbyte value, out int bytesConsumed, char standardFormat = default) + { + switch (standardFormat) + { + case default(char): + case 'g': + case 'G': + case 'd': + case 'D': + return TryParseSByteD(source, out value, out bytesConsumed); + + case 'n': + case 'N': + return TryParseSByteN(source, out value, out bytesConsumed); + + case 'x': + case 'X': + value = default; + return TryParseByteX(source, out Unsafe.As<sbyte, byte>(ref value), out bytesConsumed); + + default: + return ThrowHelper.TryParseThrowFormatException(out value, out bytesConsumed); + } + } + + /// <summary> + /// Parses an Int16 at the start of a Utf8 string. + /// </summary> + /// <param name="source">The Utf8 string to parse</param> + /// <param name="value">Receives the parsed value</param> + /// <param name="bytesConsumed">On a successful parse, receives the length in bytes of the substring that was parsed </param> + /// <param name="standardFormat">Expected format of the Utf8 string</param> + /// <returns> + /// true for success. "bytesConsumed" contains the length in bytes of the substring that was parsed. + /// false if the string was not syntactically valid or an overflow or underflow occurred. "bytesConsumed" is set to 0. + /// </returns> + /// <remarks> + /// Formats supported: + /// G/g (default) + /// D/d 32767 + /// N/n 32,767 + /// X/x 7fff + /// </remarks> + /// <exceptions> + /// <cref>System.FormatException</cref> if the format is not valid for this data type. + /// </exceptions> + public static bool TryParse(ReadOnlySpan<byte> source, out short value, out int bytesConsumed, char standardFormat = default) + { + switch (standardFormat) + { + case default(char): + case 'g': + case 'G': + case 'd': + case 'D': + return TryParseInt16D(source, out value, out bytesConsumed); + + case 'n': + case 'N': + return TryParseInt16N(source, out value, out bytesConsumed); + + case 'x': + case 'X': + value = default; + return TryParseUInt16X(source, out Unsafe.As<short, ushort>(ref value), out bytesConsumed); + + default: + return ThrowHelper.TryParseThrowFormatException(out value, out bytesConsumed); + } + } + + /// <summary> + /// Parses an Int32 at the start of a Utf8 string. + /// </summary> + /// <param name="source">The Utf8 string to parse</param> + /// <param name="value">Receives the parsed value</param> + /// <param name="bytesConsumed">On a successful parse, receives the length in bytes of the substring that was parsed </param> + /// <param name="standardFormat">Expected format of the Utf8 string</param> + /// <returns> + /// true for success. "bytesConsumed" contains the length in bytes of the substring that was parsed. + /// false if the string was not syntactically valid or an overflow or underflow occurred. "bytesConsumed" is set to 0. + /// </returns> + /// <remarks> + /// Formats supported: + /// G/g (default) + /// D/d 32767 + /// N/n 32,767 + /// X/x 7fff + /// </remarks> + /// <exceptions> + /// <cref>System.FormatException</cref> if the format is not valid for this data type. + /// </exceptions> + public static bool TryParse(ReadOnlySpan<byte> source, out int value, out int bytesConsumed, char standardFormat = default) + { + switch (standardFormat) + { + case default(char): + case 'g': + case 'G': + case 'd': + case 'D': + return TryParseInt32D(source, out value, out bytesConsumed); + + case 'n': + case 'N': + return TryParseInt32N(source, out value, out bytesConsumed); + + case 'x': + case 'X': + value = default; + return TryParseUInt32X(source, out Unsafe.As<int, uint>(ref value), out bytesConsumed); + + default: + return ThrowHelper.TryParseThrowFormatException(out value, out bytesConsumed); + } + } + + /// <summary> + /// Parses an Int64 at the start of a Utf8 string. + /// </summary> + /// <param name="source">The Utf8 string to parse</param> + /// <param name="value">Receives the parsed value</param> + /// <param name="bytesConsumed">On a successful parse, receives the length in bytes of the substring that was parsed </param> + /// <param name="standardFormat">Expected format of the Utf8 string</param> + /// <returns> + /// true for success. "bytesConsumed" contains the length in bytes of the substring that was parsed. + /// false if the string was not syntactically valid or an overflow or underflow occurred. "bytesConsumed" is set to 0. + /// </returns> + /// <remarks> + /// Formats supported: + /// G/g (default) + /// D/d 32767 + /// N/n 32,767 + /// X/x 7fff + /// </remarks> + /// <exceptions> + /// <cref>System.FormatException</cref> if the format is not valid for this data type. + /// </exceptions> + public static bool TryParse(ReadOnlySpan<byte> source, out long value, out int bytesConsumed, char standardFormat = default) + { + switch (standardFormat) + { + case default(char): + case 'g': + case 'G': + case 'd': + case 'D': + return TryParseInt64D(source, out value, out bytesConsumed); + + case 'n': + case 'N': + return TryParseInt64N(source, out value, out bytesConsumed); + + case 'x': + case 'X': + value = default; + return TryParseUInt64X(source, out Unsafe.As<long, ulong>(ref value), out bytesConsumed); + + default: + return ThrowHelper.TryParseThrowFormatException(out value, out bytesConsumed); + } + } + } +} diff --git a/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Integer.Unsigned.D.cs b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Integer.Unsigned.D.cs new file mode 100644 index 0000000000..46753f5c57 --- /dev/null +++ b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Integer.Unsigned.D.cs @@ -0,0 +1,354 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Buffers.Text +{ + public static partial class Utf8Parser + { + private static bool TryParseByteD(ReadOnlySpan<byte> source, out byte value, out int bytesConsumed) + { + if (source.Length < 1) + goto FalseExit; + + int index = 0; + int num = source[index]; + int answer = 0; + + if (ParserHelpers.IsDigit(num)) + { + if (num == '0') + { + do + { + index++; + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + } while (num == '0'); + if (!ParserHelpers.IsDigit(num)) + goto Done; + } + + answer = num - '0'; + index++; + + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = 10 * answer + num - '0'; + + // Potential overflow + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = answer * 10 + num - '0'; + if ((uint)answer > byte.MaxValue) + goto FalseExit; // Overflow + + if ((uint)index >= (uint)source.Length) + goto Done; + if (!ParserHelpers.IsDigit(source[index])) + goto Done; + + // Guaranteed overflow + goto FalseExit; + } + + FalseExit: + bytesConsumed = default; + value = default; + return false; + + Done: + bytesConsumed = index; + value = (byte)answer; + return true; + } + + private static bool TryParseUInt16D(ReadOnlySpan<byte> source, out ushort value, out int bytesConsumed) + { + if (source.Length < 1) + goto FalseExit; + + int index = 0; + int num = source[index]; + int answer = 0; + + if (ParserHelpers.IsDigit(num)) + { + if (num == '0') + { + do + { + index++; + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + } while (num == '0'); + if (!ParserHelpers.IsDigit(num)) + goto Done; + } + + answer = num - '0'; + index++; + + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = 10 * answer + num - '0'; + + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = 10 * answer + num - '0'; + + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = 10 * answer + num - '0'; + + // Potential overflow + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = answer * 10 + num - '0'; + if ((uint)answer > ushort.MaxValue) + goto FalseExit; // Overflow + + if ((uint)index >= (uint)source.Length) + goto Done; + if (!ParserHelpers.IsDigit(source[index])) + goto Done; + + // Guaranteed overflow + goto FalseExit; + } + + FalseExit: + bytesConsumed = default; + value = default; + return false; + + Done: + bytesConsumed = index; + value = (ushort)answer; + return true; + } + + private static bool TryParseUInt32D(ReadOnlySpan<byte> source, out uint value, out int bytesConsumed) + { + if (source.Length < 1) + goto FalseExit; + + int index = 0; + int num = source[index]; + int answer = 0; + + if (ParserHelpers.IsDigit(num)) + { + if (num == '0') + { + do + { + index++; + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + } while (num == '0'); + if (!ParserHelpers.IsDigit(num)) + goto Done; + } + + answer = num - '0'; + index++; + + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = 10 * answer + num - '0'; + + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = 10 * answer + num - '0'; + + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = 10 * answer + num - '0'; + + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = 10 * answer + num - '0'; + + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = 10 * answer + num - '0'; + + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = 10 * answer + num - '0'; + + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = 10 * answer + num - '0'; + + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + answer = 10 * answer + num - '0'; + + // Potential overflow + if ((uint)index >= (uint)source.Length) + goto Done; + num = source[index]; + if (!ParserHelpers.IsDigit(num)) + goto Done; + index++; + if (((uint)answer) > uint.MaxValue / 10 || (((uint)answer) == uint.MaxValue / 10 && num > '5')) + goto FalseExit; // Overflow + answer = answer * 10 + num - '0'; + + if ((uint)index >= (uint)source.Length) + goto Done; + if (!ParserHelpers.IsDigit(source[index])) + goto Done; + + // Guaranteed overflow + goto FalseExit; + } + + FalseExit: + bytesConsumed = default; + value = default; + return false; + + Done: + bytesConsumed = index; + value = (uint)answer; + return true; + } + + private static bool TryParseUInt64D(ReadOnlySpan<byte> source, out ulong value, out int bytesConsumed) + { + if (source.Length < 1) + { + bytesConsumed = 0; + value = default; + return false; + } + + // Parse the first digit separately. If invalid here, we need to return false. + ulong firstDigit = source[0] - 48u; // '0' + if (firstDigit > 9) + { + bytesConsumed = 0; + value = default; + return false; + } + ulong parsedValue = firstDigit; + + if (source.Length < ParserHelpers.Int64OverflowLength) + { + // Length is less than Parsers.Int64OverflowLength; overflow is not possible + for (int index = 1; index < source.Length; index++) + { + ulong nextDigit = source[index] - 48u; // '0' + if (nextDigit > 9) + { + bytesConsumed = index; + value = parsedValue; + return true; + } + parsedValue = parsedValue * 10 + nextDigit; + } + } + else + { + // Length is greater than Parsers.Int64OverflowLength; overflow is only possible after Parsers.Int64OverflowLength + // digits. There may be no overflow after Parsers.Int64OverflowLength if there are leading zeroes. + for (int index = 1; index < ParserHelpers.Int64OverflowLength - 1; index++) + { + ulong nextDigit = source[index] - 48u; // '0' + if (nextDigit > 9) + { + bytesConsumed = index; + value = parsedValue; + return true; + } + parsedValue = parsedValue * 10 + nextDigit; + } + for (int index = ParserHelpers.Int64OverflowLength - 1; index < source.Length; index++) + { + ulong nextDigit = source[index] - 48u; // '0' + if (nextDigit > 9) + { + bytesConsumed = index; + value = parsedValue; + return true; + } + // If parsedValue > (ulong.MaxValue / 10), any more appended digits will cause overflow. + // if parsedValue == (ulong.MaxValue / 10), any nextDigit greater than 5 implies overflow. + if (parsedValue > ulong.MaxValue / 10 || (parsedValue == ulong.MaxValue / 10 && nextDigit > 5)) + { + bytesConsumed = 0; + value = default; + return false; + } + parsedValue = parsedValue * 10 + nextDigit; + } + } + + bytesConsumed = source.Length; + value = parsedValue; + return true; + } + } +} diff --git a/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Integer.Unsigned.N.cs b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Integer.Unsigned.N.cs new file mode 100644 index 0000000000..2db20c1270 --- /dev/null +++ b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Integer.Unsigned.N.cs @@ -0,0 +1,336 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Buffers.Text +{ + // + // Parsing unsigned integers for the 'N' format. Emulating int.TryParse(NumberStyles.AllowThousands | NumberStyles.Integer | NumberStyles.AllowDecimalPoint) + // + public static partial class Utf8Parser + { + private static bool TryParseByteN(ReadOnlySpan<byte> source, out byte value, out int bytesConsumed) + { + if (source.Length < 1) + goto FalseExit; + + int index = 0; + int c = source[index]; + if (c == '+') + { + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + c = source[index]; + } + + int answer; + + // Handle the first digit (or period) as a special case. This ensures some compatible edge-case behavior with the classic parse routines + // (at least one digit must precede any commas, and a string without any digits prior to the decimal point must have at least + // one digit after the decimal point.) + if (c == Utf8Constants.Period) + goto FractionalPartWithoutLeadingDigits; + if (!ParserHelpers.IsDigit(c)) + goto FalseExit; + answer = c - '0'; + + for (; ; ) + { + index++; + if ((uint)index >= (uint)source.Length) + goto Done; + + c = source[index]; + if (c == Utf8Constants.Comma) + continue; + + if (c == Utf8Constants.Period) + goto FractionalDigits; + + if (!ParserHelpers.IsDigit(c)) + goto Done; + + answer = answer * 10 + c - '0'; + + if (answer > byte.MaxValue) + goto FalseExit; // Overflow + } + + FractionalPartWithoutLeadingDigits: // If we got here, we found a decimal point before we found any digits. This is legal as long as there's at least one zero after the decimal point. + answer = 0; + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + if (source[index] != '0') + goto FalseExit; + + FractionalDigits: // "N" format allows a fractional portion despite being an integer format but only if the post-fraction digits are all 0. + do + { + index++; + if ((uint)index >= (uint)source.Length) + goto Done; + c = source[index]; + } + while (c == '0'); + + if (ParserHelpers.IsDigit(c)) + goto FalseExit; // The fractional portion contained a non-zero digit. Treat this as an error, not an early termination. + goto Done; + + FalseExit: + bytesConsumed = default; + value = default; + return false; + + Done: + bytesConsumed = index; + value = (byte)answer; + return true; + } + + private static bool TryParseUInt16N(ReadOnlySpan<byte> source, out ushort value, out int bytesConsumed) + { + if (source.Length < 1) + goto FalseExit; + + int index = 0; + int c = source[index]; + if (c == '+') + { + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + c = source[index]; + } + + int answer; + + // Handle the first digit (or period) as a special case. This ensures some compatible edge-case behavior with the classic parse routines + // (at least one digit must precede any commas, and a string without any digits prior to the decimal point must have at least + // one digit after the decimal point.) + if (c == Utf8Constants.Period) + goto FractionalPartWithoutLeadingDigits; + if (!ParserHelpers.IsDigit(c)) + goto FalseExit; + answer = c - '0'; + + for (; ; ) + { + index++; + if ((uint)index >= (uint)source.Length) + goto Done; + + c = source[index]; + if (c == Utf8Constants.Comma) + continue; + + if (c == Utf8Constants.Period) + goto FractionalDigits; + + if (!ParserHelpers.IsDigit(c)) + goto Done; + + answer = answer * 10 + c - '0'; + + if (answer > ushort.MaxValue) + goto FalseExit; // Overflow + } + + FractionalPartWithoutLeadingDigits: // If we got here, we found a decimal point before we found any digits. This is legal as long as there's at least one zero after the decimal point. + answer = 0; + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + if (source[index] != '0') + goto FalseExit; + + FractionalDigits: // "N" format allows a fractional portion despite being an integer format but only if the post-fraction digits are all 0. + do + { + index++; + if ((uint)index >= (uint)source.Length) + goto Done; + c = source[index]; + } + while (c == '0'); + + if (ParserHelpers.IsDigit(c)) + goto FalseExit; // The fractional portion contained a non-zero digit. Treat this as an error, not an early termination. + goto Done; + + FalseExit: + bytesConsumed = default; + value = default; + return false; + + Done: + bytesConsumed = index; + value = (ushort)answer; + return true; + } + + private static bool TryParseUInt32N(ReadOnlySpan<byte> source, out uint value, out int bytesConsumed) + { + if (source.Length < 1) + goto FalseExit; + + int index = 0; + int c = source[index]; + if (c == '+') + { + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + c = source[index]; + } + + int answer; + + // Handle the first digit (or period) as a special case. This ensures some compatible edge-case behavior with the classic parse routines + // (at least one digit must precede any commas, and a string without any digits prior to the decimal point must have at least + // one digit after the decimal point.) + if (c == Utf8Constants.Period) + goto FractionalPartWithoutLeadingDigits; + if (!ParserHelpers.IsDigit(c)) + goto FalseExit; + answer = c - '0'; + + for (; ; ) + { + index++; + if ((uint)index >= (uint)source.Length) + goto Done; + + c = source[index]; + if (c == Utf8Constants.Comma) + continue; + + if (c == Utf8Constants.Period) + goto FractionalDigits; + + if (!ParserHelpers.IsDigit(c)) + goto Done; + + if (((uint)answer) > uint.MaxValue / 10 || (((uint)answer) == uint.MaxValue / 10 && c > '5')) + goto FalseExit; // Overflow + + answer = answer * 10 + c - '0'; + } + + FractionalPartWithoutLeadingDigits: // If we got here, we found a decimal point before we found any digits. This is legal as long as there's at least one zero after the decimal point. + answer = 0; + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + if (source[index] != '0') + goto FalseExit; + + FractionalDigits: // "N" format allows a fractional portion despite being an integer format but only if the post-fraction digits are all 0. + do + { + index++; + if ((uint)index >= (uint)source.Length) + goto Done; + c = source[index]; + } + while (c == '0'); + + if (ParserHelpers.IsDigit(c)) + goto FalseExit; // The fractional portion contained a non-zero digit. Treat this as an error, not an early termination. + goto Done; + + FalseExit: + bytesConsumed = default; + value = default; + return false; + + Done: + bytesConsumed = index; + value = (uint)answer; + return true; + } + + private static bool TryParseUInt64N(ReadOnlySpan<byte> source, out ulong value, out int bytesConsumed) + { + if (source.Length < 1) + goto FalseExit; + + int index = 0; + int c = source[index]; + if (c == '+') + { + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + c = source[index]; + } + + long answer; + + // Handle the first digit (or period) as a special case. This ensures some compatible edge-case behavior with the classic parse routines + // (at least one digit must precede any commas, and a string without any digits prior to the decimal point must have at least + // one digit after the decimal point.) + if (c == Utf8Constants.Period) + goto FractionalPartWithoutLeadingDigits; + if (!ParserHelpers.IsDigit(c)) + goto FalseExit; + answer = c - '0'; + + for (; ; ) + { + index++; + if ((uint)index >= (uint)source.Length) + goto Done; + + c = source[index]; + if (c == Utf8Constants.Comma) + continue; + + if (c == Utf8Constants.Period) + goto FractionalDigits; + + if (!ParserHelpers.IsDigit(c)) + goto Done; + + if (((ulong)answer) > ulong.MaxValue / 10 || (((ulong)answer) == ulong.MaxValue / 10 && c > '5')) + goto FalseExit; // Overflow + + answer = answer * 10 + c - '0'; + } + + FractionalPartWithoutLeadingDigits: // If we got here, we found a decimal point before we found any digits. This is legal as long as there's at least one zero after the decimal point. + answer = 0; + index++; + if ((uint)index >= (uint)source.Length) + goto FalseExit; + if (source[index] != '0') + goto FalseExit; + + FractionalDigits: // "N" format allows a fractional portion despite being an integer format but only if the post-fraction digits are all 0. + do + { + index++; + if ((uint)index >= (uint)source.Length) + goto Done; + c = source[index]; + } + while (c == '0'); + + if (ParserHelpers.IsDigit(c)) + goto FalseExit; // The fractional portion contained a non-zero digit. Treat this as an error, not an early termination. + goto Done; + + FalseExit: + bytesConsumed = default; + value = default; + return false; + + Done: + bytesConsumed = index; + value = (ulong)answer; + return true; + } + } +} diff --git a/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Integer.Unsigned.X.cs b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Integer.Unsigned.X.cs new file mode 100644 index 0000000000..7e7867a56f --- /dev/null +++ b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Integer.Unsigned.X.cs @@ -0,0 +1,341 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Buffers.Text +{ + public static partial class Utf8Parser + { + private static bool TryParseByteX(ReadOnlySpan<byte> source, out byte value, out int bytesConsumed) + { + if (source.Length < 1) + { + bytesConsumed = 0; + value = default; + return false; + } + byte nextCharacter; + byte nextDigit; + + // Cache Parsers.s_HexLookup in order to avoid static constructor checks + byte[] hexLookup = ParserHelpers.s_hexLookup; + + // Parse the first digit separately. If invalid here, we need to return false. + nextCharacter = source[0]; + nextDigit = hexLookup[nextCharacter]; + if (nextDigit == 0xFF) + { + bytesConsumed = 0; + value = default; + return false; + } + uint parsedValue = nextDigit; + + if (source.Length <= ParserHelpers.ByteOverflowLengthHex) + { + // Length is less than or equal to Parsers.ByteOverflowLengthHex; overflow is not possible + for (int index = 1; index < source.Length; index++) + { + nextCharacter = source[index]; + nextDigit = hexLookup[nextCharacter]; + if (nextDigit == 0xFF) + { + bytesConsumed = index; + value = (byte)(parsedValue); + return true; + } + parsedValue = (parsedValue << 4) + nextDigit; + } + } + else + { + // Length is greater than Parsers.ByteOverflowLengthHex; overflow is only possible after Parsers.ByteOverflowLengthHex + // digits. There may be no overflow after Parsers.ByteOverflowLengthHex if there are leading zeroes. + for (int index = 1; index < ParserHelpers.ByteOverflowLengthHex; index++) + { + nextCharacter = source[index]; + nextDigit = hexLookup[nextCharacter]; + if (nextDigit == 0xFF) + { + bytesConsumed = index; + value = (byte)(parsedValue); + return true; + } + parsedValue = (parsedValue << 4) + nextDigit; + } + for (int index = ParserHelpers.ByteOverflowLengthHex; index < source.Length; index++) + { + nextCharacter = source[index]; + nextDigit = hexLookup[nextCharacter]; + if (nextDigit == 0xFF) + { + bytesConsumed = index; + value = (byte)(parsedValue); + return true; + } + // If we try to append a digit to anything larger than byte.MaxValue / 0x10, there will be overflow + if (parsedValue > byte.MaxValue / 0x10) + { + bytesConsumed = 0; + value = default; + return false; + } + parsedValue = (parsedValue << 4) + nextDigit; + } + } + + bytesConsumed = source.Length; + value = (byte)(parsedValue); + return true; + } + + private static bool TryParseUInt16X(ReadOnlySpan<byte> source, out ushort value, out int bytesConsumed) + { + if (source.Length < 1) + { + bytesConsumed = 0; + value = default; + return false; + } + byte nextCharacter; + byte nextDigit; + + // Cache Parsers.s_HexLookup in order to avoid static constructor checks + byte[] hexLookup = ParserHelpers.s_hexLookup; + + // Parse the first digit separately. If invalid here, we need to return false. + nextCharacter = source[0]; + nextDigit = hexLookup[nextCharacter]; + if (nextDigit == 0xFF) + { + bytesConsumed = 0; + value = default; + return false; + } + uint parsedValue = nextDigit; + + if (source.Length <= ParserHelpers.Int16OverflowLengthHex) + { + // Length is less than or equal to Parsers.Int16OverflowLengthHex; overflow is not possible + for (int index = 1; index < source.Length; index++) + { + nextCharacter = source[index]; + nextDigit = hexLookup[nextCharacter]; + if (nextDigit == 0xFF) + { + bytesConsumed = index; + value = (ushort)(parsedValue); + return true; + } + parsedValue = (parsedValue << 4) + nextDigit; + } + } + else + { + // Length is greater than Parsers.Int16OverflowLengthHex; overflow is only possible after Parsers.Int16OverflowLengthHex + // digits. There may be no overflow after Parsers.Int16OverflowLengthHex if there are leading zeroes. + for (int index = 1; index < ParserHelpers.Int16OverflowLengthHex; index++) + { + nextCharacter = source[index]; + nextDigit = hexLookup[nextCharacter]; + if (nextDigit == 0xFF) + { + bytesConsumed = index; + value = (ushort)(parsedValue); + return true; + } + parsedValue = (parsedValue << 4) + nextDigit; + } + for (int index = ParserHelpers.Int16OverflowLengthHex; index < source.Length; index++) + { + nextCharacter = source[index]; + nextDigit = hexLookup[nextCharacter]; + if (nextDigit == 0xFF) + { + bytesConsumed = index; + value = (ushort)(parsedValue); + return true; + } + // If we try to append a digit to anything larger than ushort.MaxValue / 0x10, there will be overflow + if (parsedValue > ushort.MaxValue / 0x10) + { + bytesConsumed = 0; + value = default; + return false; + } + parsedValue = (parsedValue << 4) + nextDigit; + } + } + + bytesConsumed = source.Length; + value = (ushort)(parsedValue); + return true; + } + + private static bool TryParseUInt32X(ReadOnlySpan<byte> source, out uint value, out int bytesConsumed) + { + if (source.Length < 1) + { + bytesConsumed = 0; + value = default; + return false; + } + byte nextCharacter; + byte nextDigit; + + // Cache Parsers.s_HexLookup in order to avoid static constructor checks + byte[] hexLookup = ParserHelpers.s_hexLookup; + + // Parse the first digit separately. If invalid here, we need to return false. + nextCharacter = source[0]; + nextDigit = hexLookup[nextCharacter]; + if (nextDigit == 0xFF) + { + bytesConsumed = 0; + value = default; + return false; + } + uint parsedValue = nextDigit; + + if (source.Length <= ParserHelpers.Int32OverflowLengthHex) + { + // Length is less than or equal to Parsers.Int32OverflowLengthHex; overflow is not possible + for (int index = 1; index < source.Length; index++) + { + nextCharacter = source[index]; + nextDigit = hexLookup[nextCharacter]; + if (nextDigit == 0xFF) + { + bytesConsumed = index; + value = parsedValue; + return true; + } + parsedValue = (parsedValue << 4) + nextDigit; + } + } + else + { + // Length is greater than Parsers.Int32OverflowLengthHex; overflow is only possible after Parsers.Int32OverflowLengthHex + // digits. There may be no overflow after Parsers.Int32OverflowLengthHex if there are leading zeroes. + for (int index = 1; index < ParserHelpers.Int32OverflowLengthHex; index++) + { + nextCharacter = source[index]; + nextDigit = hexLookup[nextCharacter]; + if (nextDigit == 0xFF) + { + bytesConsumed = index; + value = parsedValue; + return true; + } + parsedValue = (parsedValue << 4) + nextDigit; + } + for (int index = ParserHelpers.Int32OverflowLengthHex; index < source.Length; index++) + { + nextCharacter = source[index]; + nextDigit = hexLookup[nextCharacter]; + if (nextDigit == 0xFF) + { + bytesConsumed = index; + value = parsedValue; + return true; + } + // If we try to append a digit to anything larger than uint.MaxValue / 0x10, there will be overflow + if (parsedValue > uint.MaxValue / 0x10) + { + bytesConsumed = 0; + value = default; + return false; + } + parsedValue = (parsedValue << 4) + nextDigit; + } + } + + bytesConsumed = source.Length; + value = parsedValue; + return true; + } + + private static bool TryParseUInt64X(ReadOnlySpan<byte> source, out ulong value, out int bytesConsumed) + { + if (source.Length < 1) + { + bytesConsumed = 0; + value = default; + return false; + } + byte nextCharacter; + byte nextDigit; + + // Cache Parsers.s_HexLookup in order to avoid static constructor checks + byte[] hexLookup = ParserHelpers.s_hexLookup; + + // Parse the first digit separately. If invalid here, we need to return false. + nextCharacter = source[0]; + nextDigit = hexLookup[nextCharacter]; + if (nextDigit == 0xFF) + { + bytesConsumed = 0; + value = default; + return false; + } + ulong parsedValue = nextDigit; + + if (source.Length <= ParserHelpers.Int64OverflowLengthHex) + { + // Length is less than or equal to Parsers.Int64OverflowLengthHex; overflow is not possible + for (int index = 1; index < source.Length; index++) + { + nextCharacter = source[index]; + nextDigit = hexLookup[nextCharacter]; + if (nextDigit == 0xFF) + { + bytesConsumed = index; + value = parsedValue; + return true; + } + parsedValue = (parsedValue << 4) + nextDigit; + } + } + else + { + // Length is greater than Parsers.Int64OverflowLengthHex; overflow is only possible after Parsers.Int64OverflowLengthHex + // digits. There may be no overflow after Parsers.Int64OverflowLengthHex if there are leading zeroes. + for (int index = 1; index < ParserHelpers.Int64OverflowLengthHex; index++) + { + nextCharacter = source[index]; + nextDigit = hexLookup[nextCharacter]; + if (nextDigit == 0xFF) + { + bytesConsumed = index; + value = parsedValue; + return true; + } + parsedValue = (parsedValue << 4) + nextDigit; + } + for (int index = ParserHelpers.Int64OverflowLengthHex; index < source.Length; index++) + { + nextCharacter = source[index]; + nextDigit = hexLookup[nextCharacter]; + if (nextDigit == 0xFF) + { + bytesConsumed = index; + value = parsedValue; + return true; + } + // If we try to append a digit to anything larger than ulong.MaxValue / 0x10, there will be overflow + if (parsedValue > ulong.MaxValue / 0x10) + { + bytesConsumed = 0; + value = default; + return false; + } + parsedValue = (parsedValue << 4) + nextDigit; + } + } + + bytesConsumed = source.Length; + value = parsedValue; + return true; + } + } +} diff --git a/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Integer.Unsigned.cs b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Integer.Unsigned.cs new file mode 100644 index 0000000000..ae23c29d04 --- /dev/null +++ b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Integer.Unsigned.cs @@ -0,0 +1,192 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Buffers.Text +{ + public static partial class Utf8Parser + { + /// <summary> + /// Parses a Byte at the start of a Utf8 string. + /// </summary> + /// <param name="source">The Utf8 string to parse</param> + /// <param name="value">Receives the parsed value</param> + /// <param name="bytesConsumed">On a successful parse, receives the length in bytes of the substring that was parsed </param> + /// <param name="standardFormat">Expected format of the Utf8 string</param> + /// <returns> + /// true for success. "bytesConsumed" contains the length in bytes of the substring that was parsed. + /// false if the string was not syntactically valid or an overflow or underflow occurred. "bytesConsumed" is set to 0. + /// </returns> + /// <remarks> + /// Formats supported: + /// G/g (default) + /// D/d 32767 + /// N/n 32,767 + /// X/x 7fff + /// </remarks> + /// <exceptions> + /// <cref>System.FormatException</cref> if the format is not valid for this data type. + /// </exceptions> + public static bool TryParse(ReadOnlySpan<byte> source, out byte value, out int bytesConsumed, char standardFormat = default) + { + switch (standardFormat) + { + case default(char): + case 'g': + case 'G': + case 'd': + case 'D': + return TryParseByteD(source, out value, out bytesConsumed); + + case 'n': + case 'N': + return TryParseByteN(source, out value, out bytesConsumed); + + case 'x': + case 'X': + return TryParseByteX(source, out value, out bytesConsumed); + + default: + return ThrowHelper.TryParseThrowFormatException(out value, out bytesConsumed); + } + } + + /// <summary> + /// Parses a UInt16 at the start of a Utf8 string. + /// </summary> + /// <param name="source">The Utf8 string to parse</param> + /// <param name="value">Receives the parsed value</param> + /// <param name="bytesConsumed">On a successful parse, receives the length in bytes of the substring that was parsed </param> + /// <param name="standardFormat">Expected format of the Utf8 string</param> + /// <returns> + /// true for success. "bytesConsumed" contains the length in bytes of the substring that was parsed. + /// false if the string was not syntactically valid or an overflow or underflow occurred. "bytesConsumed" is set to 0. + /// </returns> + /// <remarks> + /// Formats supported: + /// G/g (default) + /// D/d 32767 + /// N/n 32,767 + /// X/x 7fff + /// </remarks> + /// <exceptions> + /// <cref>System.FormatException</cref> if the format is not valid for this data type. + /// </exceptions> + [CLSCompliant(false)] + public static bool TryParse(ReadOnlySpan<byte> source, out ushort value, out int bytesConsumed, char standardFormat = default) + { + switch (standardFormat) + { + case default(char): + case 'g': + case 'G': + case 'd': + case 'D': + return TryParseUInt16D(source, out value, out bytesConsumed); + + case 'n': + case 'N': + return TryParseUInt16N(source, out value, out bytesConsumed); + + case 'x': + case 'X': + return TryParseUInt16X(source, out value, out bytesConsumed); + + default: + return ThrowHelper.TryParseThrowFormatException(out value, out bytesConsumed); + } + } + + /// <summary> + /// Parses a UInt32 at the start of a Utf8 string. + /// </summary> + /// <param name="source">The Utf8 string to parse</param> + /// <param name="value">Receives the parsed value</param> + /// <param name="bytesConsumed">On a successful parse, receives the length in bytes of the substring that was parsed </param> + /// <param name="standardFormat">Expected format of the Utf8 string</param> + /// <returns> + /// true for success. "bytesConsumed" contains the length in bytes of the substring that was parsed. + /// false if the string was not syntactically valid or an overflow or underflow occurred. "bytesConsumed" is set to 0. + /// </returns> + /// <remarks> + /// Formats supported: + /// G/g (default) + /// D/d 32767 + /// N/n 32,767 + /// X/x 7fff + /// </remarks> + /// <exceptions> + /// <cref>System.FormatException</cref> if the format is not valid for this data type. + /// </exceptions> + [CLSCompliant(false)] + public static bool TryParse(ReadOnlySpan<byte> source, out uint value, out int bytesConsumed, char standardFormat = default) + { + switch (standardFormat) + { + case default(char): + case 'g': + case 'G': + case 'd': + case 'D': + return TryParseUInt32D(source, out value, out bytesConsumed); + + case 'n': + case 'N': + return TryParseUInt32N(source, out value, out bytesConsumed); + + case 'x': + case 'X': + return TryParseUInt32X(source, out value, out bytesConsumed); + + default: + return ThrowHelper.TryParseThrowFormatException(out value, out bytesConsumed); + } + } + + /// <summary> + /// Parses a UInt64 at the start of a Utf8 string. + /// </summary> + /// <param name="source">The Utf8 string to parse</param> + /// <param name="value">Receives the parsed value</param> + /// <param name="bytesConsumed">On a successful parse, receives the length in bytes of the substring that was parsed </param> + /// <param name="standardFormat">Expected format of the Utf8 string</param> + /// <returns> + /// true for success. "bytesConsumed" contains the length in bytes of the substring that was parsed. + /// false if the string was not syntactically valid or an overflow or underflow occurred. "bytesConsumed" is set to 0. + /// </returns> + /// <remarks> + /// Formats supported: + /// G/g (default) + /// D/d 32767 + /// N/n 32,767 + /// X/x 7fff + /// </remarks> + /// <exceptions> + /// <cref>System.FormatException</cref> if the format is not valid for this data type. + /// </exceptions> + [CLSCompliant(false)] + public static bool TryParse(ReadOnlySpan<byte> source, out ulong value, out int bytesConsumed, char standardFormat = default) + { + switch (standardFormat) + { + case default(char): + case 'g': + case 'G': + case 'd': + case 'D': + return TryParseUInt64D(source, out value, out bytesConsumed); + + case 'n': + case 'N': + return TryParseUInt64N(source, out value, out bytesConsumed); + + case 'x': + case 'X': + return TryParseUInt64X(source, out value, out bytesConsumed); + + default: + return ThrowHelper.TryParseThrowFormatException(out value, out bytesConsumed); + } + } + } +} diff --git a/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Number.cs b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Number.cs new file mode 100644 index 0000000000..813a1f0a6e --- /dev/null +++ b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.Number.cs @@ -0,0 +1,246 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; + +namespace System.Buffers.Text +{ + public static partial class Utf8Parser + { + [Flags] + private enum ParseNumberOptions + { + AllowExponent = 0x00000001, + } + + private static bool TryParseNumber(ReadOnlySpan<byte> source, ref NumberBuffer number, out int bytesConsumed, ParseNumberOptions options, out bool textUsedExponentNotation) + { + Debug.Assert(number.Digits[0] == 0 && number.Scale == 0 && !number.IsNegative, "Number not initialized to default(NumberBuffer)"); + + textUsedExponentNotation = false; + + if (source.Length == 0) + { + bytesConsumed = 0; + return false; + } + + Span<byte> digits = number.Digits; + + int srcIndex = 0; + int dstIndex = 0; + + // Consume the leading sign if any. + byte c = source[srcIndex]; + switch (c) + { + case Utf8Constants.Minus: + number.IsNegative = true; + goto case Utf8Constants.Plus; + + case Utf8Constants.Plus: + srcIndex++; + if (srcIndex == source.Length) + { + bytesConsumed = 0; + return false; + } + c = source[srcIndex]; + break; + + default: + break; + } + + int startIndexDigitsBeforeDecimal = srcIndex; + + // Throw away any leading zeroes + while (srcIndex != source.Length) + { + c = source[srcIndex]; + if (c != '0') + break; + srcIndex++; + } + + if (srcIndex == source.Length) + { + digits[0] = 0; + number.Scale = 0; + number.IsNegative = false; + bytesConsumed = srcIndex; + number.CheckConsistency(); + return true; + } + + int startIndexNonLeadingDigitsBeforeDecimal = srcIndex; + while (srcIndex != source.Length) + { + c = source[srcIndex]; + if ((c - 48u) > 9) + break; + srcIndex++; + } + + int numDigitsBeforeDecimal = srcIndex - startIndexDigitsBeforeDecimal; + int numNonLeadingDigitsBeforeDecimal = srcIndex - startIndexNonLeadingDigitsBeforeDecimal; + + Debug.Assert(dstIndex == 0); + int numNonLeadingDigitsBeforeDecimalToCopy = Math.Min(numNonLeadingDigitsBeforeDecimal, NumberBuffer.BufferSize - 1); + source.Slice(startIndexNonLeadingDigitsBeforeDecimal, numNonLeadingDigitsBeforeDecimalToCopy).CopyTo(digits); + dstIndex = numNonLeadingDigitsBeforeDecimalToCopy; + number.Scale = numNonLeadingDigitsBeforeDecimal; + + if (srcIndex == source.Length) + { + bytesConsumed = srcIndex; + number.CheckConsistency(); + return true; + } + + int numDigitsAfterDecimal = 0; + if (c == Utf8Constants.Period) + { + // + // Parse the digits after the decimal point. + // + + srcIndex++; + int startIndexDigitsAfterDecimal = srcIndex; + while (srcIndex != source.Length) + { + c = source[srcIndex]; + if ((c - 48u) > 9) + break; + srcIndex++; + } + numDigitsAfterDecimal = srcIndex - startIndexDigitsAfterDecimal; + + int startIndexOfDigitsAfterDecimalToCopy = startIndexDigitsAfterDecimal; + if (dstIndex == 0) + { + // Not copied any digits to the Number struct yet. This means we must continue discarding leading zeroes even though they appeared after the decimal point. + while (startIndexOfDigitsAfterDecimalToCopy < srcIndex && source[startIndexOfDigitsAfterDecimalToCopy] == '0') + { + number.Scale--; + startIndexOfDigitsAfterDecimalToCopy++; + } + } + + int numDigitsAfterDecimalToCopy = Math.Min(srcIndex - startIndexOfDigitsAfterDecimalToCopy, NumberBuffer.BufferSize - dstIndex - 1); + source.Slice(startIndexOfDigitsAfterDecimalToCopy, numDigitsAfterDecimalToCopy).CopyTo(digits.Slice(dstIndex)); + dstIndex += numDigitsAfterDecimalToCopy; + // We "should" really NUL terminate, but there are multiple places we'd have to do this and it is a precondition that the caller pass in a fully zero=initialized Number. + + if (srcIndex == source.Length) + { + if (numDigitsBeforeDecimal == 0 && numDigitsAfterDecimal == 0) + { + // For compatibility. You can say "5." and ".5" but you can't say "." + bytesConsumed = 0; + return false; + } + + bytesConsumed = srcIndex; + number.CheckConsistency(); + return true; + } + } + + if (numDigitsBeforeDecimal == 0 && numDigitsAfterDecimal == 0) + { + bytesConsumed = 0; + return false; + } + + if ((c & ~0x20u) != 'E') + { + if ((digits[0] == 0) && (numDigitsAfterDecimal == 0)) + { + number.IsNegative = false; + } + + bytesConsumed = srcIndex; + number.CheckConsistency(); + return true; + } + + // + // Parse the exponent after the "E" + // + textUsedExponentNotation = true; + srcIndex++; + + if ((options & ParseNumberOptions.AllowExponent) == 0) + { + bytesConsumed = 0; + return false; + } + + if (srcIndex == source.Length) + { + bytesConsumed = 0; + return false; + } + + bool exponentIsNegative = false; + c = source[srcIndex]; + switch (c) + { + case Utf8Constants.Minus: + exponentIsNegative = true; + goto case Utf8Constants.Plus; + + case Utf8Constants.Plus: + srcIndex++; + if (srcIndex == source.Length) + { + bytesConsumed = 0; + return false; + } + c = source[srcIndex]; + break; + + default: + break; + } + + if (!Utf8Parser.TryParseUInt32D(source.Slice(srcIndex), out uint absoluteExponent, out int bytesConsumedByExponent)) + { + bytesConsumed = 0; + return false; + } + + srcIndex += bytesConsumedByExponent; + + if (exponentIsNegative) + { + if (number.Scale < int.MinValue + (long)absoluteExponent) + { + // A scale underflow means all non-zero digits are all so far to the right of the decimal point, no + // number format we have will be able to see them. Just pin the scale at the absolute minimum + // and let the converter produce a 0 with the max precision available for that type. + number.Scale = int.MinValue; + } + else + { + number.Scale -= (int)absoluteExponent; + } + } + else + { + if (number.Scale > int.MaxValue - (long)absoluteExponent) + { + bytesConsumed = 0; + return false; + } + number.Scale += (int)absoluteExponent; + } + + bytesConsumed = srcIndex; + number.CheckConsistency(); + return true; + } + } +} diff --git a/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.TimeSpan.BigG.cs b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.TimeSpan.BigG.cs new file mode 100644 index 0000000000..6bcb4d5277 --- /dev/null +++ b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.TimeSpan.BigG.cs @@ -0,0 +1,132 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Buffers.Text +{ + public static partial class Utf8Parser + { + private static bool TryParseTimeSpanBigG(ReadOnlySpan<byte> source, out TimeSpan value, out int bytesConsumed) + { + int srcIndex = 0; + byte c = default; + while (srcIndex != source.Length) + { + c = source[srcIndex]; + if (!(c == ' ' || c == '\t')) + break; + srcIndex++; + } + + if (srcIndex == source.Length) + { + value = default; + bytesConsumed = 0; + return false; + } + + bool isNegative = false; + if (c == Utf8Constants.Minus) + { + isNegative = true; + srcIndex++; + if (srcIndex == source.Length) + { + value = default; + bytesConsumed = 0; + return false; + } + } + + if (!TryParseUInt32D(source.Slice(srcIndex), out uint days, out int justConsumed)) + { + value = default; + bytesConsumed = 0; + return false; + } + srcIndex += justConsumed; + + if (srcIndex == source.Length || source[srcIndex++] != Utf8Constants.Colon) + { + value = default; + bytesConsumed = 0; + return false; + } + + if (!TryParseUInt32D(source.Slice(srcIndex), out uint hours, out justConsumed)) + { + value = default; + bytesConsumed = 0; + return false; + } + srcIndex += justConsumed; + + if (srcIndex == source.Length || source[srcIndex++] != Utf8Constants.Colon) + { + value = default; + bytesConsumed = 0; + return false; + } + + if (!TryParseUInt32D(source.Slice(srcIndex), out uint minutes, out justConsumed)) + { + value = default; + bytesConsumed = 0; + return false; + } + srcIndex += justConsumed; + + if (srcIndex == source.Length || source[srcIndex++] != Utf8Constants.Colon) + { + value = default; + bytesConsumed = 0; + return false; + } + + if (!TryParseUInt32D(source.Slice(srcIndex), out uint seconds, out justConsumed)) + { + value = default; + bytesConsumed = 0; + return false; + } + srcIndex += justConsumed; + + if (srcIndex == source.Length || source[srcIndex++] != Utf8Constants.Period) + { + value = default; + bytesConsumed = 0; + return false; + } + + if (!TryParseTimeSpanFraction(source.Slice(srcIndex), out uint fraction, out justConsumed)) + { + value = default; + bytesConsumed = 0; + return false; + } + + srcIndex += justConsumed; + + if (!TryCreateTimeSpan(isNegative: isNegative, days: days, hours: hours, minutes: minutes, seconds: seconds, fraction: fraction, out value)) + { + value = default; + bytesConsumed = 0; + return false; + } + + // + // There cannot legally be a sixth number. If the next character is a period or colon, treat this as a error as it's likely + // to indicate the start of a sixth number. Otherwise, treat as end of parse with data left over. + // + if (srcIndex != source.Length && (source[srcIndex] == Utf8Constants.Period || source[srcIndex] == Utf8Constants.Colon)) + { + value = default; + bytesConsumed = 0; + return false; + } + + bytesConsumed = srcIndex; + return true; + } + } +} diff --git a/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.TimeSpan.C.cs b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.TimeSpan.C.cs new file mode 100644 index 0000000000..d0a28969be --- /dev/null +++ b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.TimeSpan.C.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Buffers.Text +{ + public static partial class Utf8Parser + { + private static bool TryParseTimeSpanC(ReadOnlySpan<byte> source, out TimeSpan value, out int bytesConsumed) + { + TimeSpanSplitter s = default; + if (!s.TrySplitTimeSpan(source, periodUsedToSeparateDay: true, out bytesConsumed)) + { + value = default; + return false; + } + + bool isNegative = s.IsNegative; + + bool success; + switch (s.Separators) + { + case 0x00000000: // dd + success = TryCreateTimeSpan(isNegative: isNegative, days: s.V1, hours: 0, minutes: 0, seconds: 0, fraction: 0, out value); + break; + + case 0x01000000: // hh:mm + success = TryCreateTimeSpan(isNegative: isNegative, days: 0, hours: s.V1, minutes: s.V2, seconds: 0, fraction: 0, out value); + break; + + case 0x02010000: // dd.hh:mm + success = TryCreateTimeSpan(isNegative: isNegative, days: s.V1, hours: s.V2, minutes: s.V3, seconds: 0, fraction: 0, out value); + break; + + case 0x01010000: // hh:mm:ss + success = TryCreateTimeSpan(isNegative: isNegative, days: 0, hours: s.V1, minutes: s.V2, seconds: s.V3, fraction: 0, out value); + break; + + case 0x02010100: // dd.hh:mm:ss + success = TryCreateTimeSpan(isNegative: isNegative, days: s.V1, hours: s.V2, minutes: s.V3, seconds: s.V4, fraction: 0, out value); + break; + + case 0x01010200: // hh:mm:ss.fffffff + success = TryCreateTimeSpan(isNegative: isNegative, days: 0, hours: s.V1, minutes: s.V2, seconds: s.V3, fraction: s.V4, out value); + break; + + case 0x02010102: // dd.hh:mm:ss.fffffff + success = TryCreateTimeSpan(isNegative: isNegative, days: s.V1, hours: s.V2, minutes: s.V3, seconds: s.V4, fraction: s.V5, out value); + break; + + default: + value = default; + success = false; + break; + } + + if (!success) + { + bytesConsumed = 0; + return false; + } + + return true; + } + } +} + diff --git a/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.TimeSpan.LittleG.cs b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.TimeSpan.LittleG.cs new file mode 100644 index 0000000000..19208b9eac --- /dev/null +++ b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.TimeSpan.LittleG.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Buffers.Text +{ + public static partial class Utf8Parser + { + private static bool TryParseTimeSpanLittleG(ReadOnlySpan<byte> source, out TimeSpan value, out int bytesConsumed) + { + TimeSpanSplitter s = default; + if (!s.TrySplitTimeSpan(source, periodUsedToSeparateDay: false, out bytesConsumed)) + { + value = default; + return false; + } + + bool isNegative = s.IsNegative; + + bool success; + switch (s.Separators) + { + case 0x00000000: // dd + success = TryCreateTimeSpan(isNegative: isNegative, days: s.V1, hours: 0, minutes: 0, seconds: 0, fraction: 0, out value); + break; + + case 0x01000000: // hh:mm + success = TryCreateTimeSpan(isNegative: isNegative, days: 0, hours: s.V1, minutes: s.V2, seconds: 0, fraction: 0, out value); + break; + + case 0x01010000: // hh:mm:ss + success = TryCreateTimeSpan(isNegative: isNegative, days: 0, hours: s.V1, minutes: s.V2, seconds: s.V3, fraction: 0, out value); + break; + + case 0x01010100: // dd:hh:mm:ss + success = TryCreateTimeSpan(isNegative: isNegative, days: s.V1, hours: s.V2, minutes: s.V3, seconds: s.V4, fraction: 0, out value); + break; + + case 0x01010200: // hh:mm:ss.fffffff + success = TryCreateTimeSpan(isNegative: isNegative, days: 0, hours: s.V1, minutes: s.V2, seconds: s.V3, fraction: s.V4, out value); + break; + + case 0x01010102: // dd:hh:mm:ss.fffffff + success = TryCreateTimeSpan(isNegative: isNegative, days: s.V1, hours: s.V2, minutes: s.V3, seconds: s.V4, fraction: s.V5, out value); + break; + + default: + value = default; + success = false; + break; + } + + if (!success) + { + bytesConsumed = 0; + return false; + } + + return true; + } + } +} diff --git a/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.TimeSpan.cs b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.TimeSpan.cs new file mode 100644 index 0000000000..0ce810b392 --- /dev/null +++ b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.TimeSpan.cs @@ -0,0 +1,192 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; + +namespace System.Buffers.Text +{ + public static partial class Utf8Parser + { + /// <summary> + /// Parses a TimeSpan at the start of a Utf8 string. + /// </summary> + /// <param name="source">The Utf8 string to parse</param> + /// <param name="value">Receives the parsed value</param> + /// <param name="bytesConsumed">On a successful parse, receives the length in bytes of the substring that was parsed </param> + /// <param name="standardFormat">Expected format of the Utf8 string</param> + /// <returns> + /// true for success. "bytesConsumed" contains the length in bytes of the substring that was parsed. + /// false if the string was not syntactically valid or an overflow or underflow occurred. "bytesConsumed" is set to 0. + /// </returns> + /// <remarks> + /// Formats supported: + /// c/t/T (default) [-][d.]hh:mm:ss[.fffffff] (constant format) + /// G [-]d:hh:mm:ss.fffffff (general long) + /// g [-][d:]h:mm:ss[.f[f[f[f[f[f[f[]]]]]]] (general short) + /// </remarks> + /// <exceptions> + /// <cref>System.FormatException</cref> if the format is not valid for this data type. + /// </exceptions> + public static bool TryParse(ReadOnlySpan<byte> source, out TimeSpan value, out int bytesConsumed, char standardFormat = default) + { + switch (standardFormat) + { + case default(char): + case 'c': + case 't': + case 'T': + return TryParseTimeSpanC(source, out value, out bytesConsumed); + + case 'G': + return TryParseTimeSpanBigG(source, out value, out bytesConsumed); + + case 'g': + return TryParseTimeSpanLittleG(source, out value, out bytesConsumed); + + default: + return ThrowHelper.TryParseThrowFormatException(out value, out bytesConsumed); + } + } + + /// <summary> + /// Parse the fraction portion of a TimeSpan. Must be 1..7 digits. If fewer than 7, zeroes are implied to the right. If more than 7, the TimeSpan + /// parser rejects the string (even if the extra digits are all zeroes.) + /// </summary> + private static bool TryParseTimeSpanFraction(ReadOnlySpan<byte> source, out uint value, out int bytesConsumed) + { + int srcIndex = 0; + + if (srcIndex == source.Length) + { + value = default; + bytesConsumed = 0; + return false; + } + + uint digit = source[srcIndex] - 48u; // '0' + if (digit > 9) + { + value = default; + bytesConsumed = 0; + return false; + } + srcIndex++; + + uint fraction = digit; + int digitCount = 1; + + while (srcIndex != source.Length) + { + digit = source[srcIndex] - 48u; // '0' + if (digit > 9) + break; + srcIndex++; + digitCount++; + if (digitCount > Utf8Constants.DateTimeNumFractionDigits) + { + // Yes, TimeSpan fraction parsing is that picky. + value = default; + bytesConsumed = 0; + return false; + } + fraction = 10 * fraction + digit; + } + + switch (digitCount) + { + case 7: + break; + + case 6: + fraction *= 10; + break; + + case 5: + fraction *= 100; + break; + + case 4: + fraction *= 1000; + break; + + case 3: + fraction *= 10000; + break; + + case 2: + fraction *= 100000; + break; + + default: + Debug.Assert(digitCount == 1); + fraction *= 1000000; + break; + } + + value = fraction; + bytesConsumed = srcIndex; + return true; + } + + /// <summary> + /// Overflow-safe TryCreateTimeSpan + /// </summary> + private static bool TryCreateTimeSpan(bool isNegative, uint days, uint hours, uint minutes, uint seconds, uint fraction, out TimeSpan timeSpan) + { + const long MaxMilliSeconds = long.MaxValue / TimeSpan.TicksPerMillisecond; + const long MinMilliSeconds = long.MinValue / TimeSpan.TicksPerMillisecond; + + Debug.Assert(days >= 0 && hours >= 0 && minutes >= 0 && seconds >= 00 && fraction >= 0); + if (hours > 23 || minutes > 59 || seconds > 59) + { + timeSpan = default; + return false; + } + + Debug.Assert(fraction <= Utf8Constants.MaxDateTimeFraction); // This value comes from TryParseTimeSpanFraction() which already rejects any fraction string longer than 7 digits. + + long millisecondsWithoutFraction = (((long)days) * 3600 * 24 + ((long)hours) * 3600 + ((long)minutes) * 60 + seconds) * 1000; + + long ticks; + if (isNegative) + { + millisecondsWithoutFraction = -millisecondsWithoutFraction; + if (millisecondsWithoutFraction < MinMilliSeconds) + { + timeSpan = default; + return false; + } + + long ticksWithoutFraction = millisecondsWithoutFraction * TimeSpan.TicksPerMillisecond; + if (ticksWithoutFraction < long.MinValue + fraction) + { + timeSpan = default; + return false; + } + + ticks = ticksWithoutFraction - fraction; + } + else + { + if (millisecondsWithoutFraction > MaxMilliSeconds) + { + timeSpan = default; + return false; + } + + long ticksWithoutFraction = millisecondsWithoutFraction * TimeSpan.TicksPerMillisecond; + if (ticksWithoutFraction > long.MaxValue - fraction) + { + timeSpan = default; + return false; + } + + ticks = ticksWithoutFraction + fraction; + } + + timeSpan = new TimeSpan(ticks); + return true; + } + } +} diff --git a/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.TimeSpanSplitter.cs b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.TimeSpanSplitter.cs new file mode 100644 index 0000000000..0c72d1f3a2 --- /dev/null +++ b/src/System.Private.CoreLib/shared/System/Buffers/Text/Utf8Parser/Utf8Parser.TimeSpanSplitter.cs @@ -0,0 +1,225 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; + +namespace System.Buffers.Text +{ + public static partial class Utf8Parser + { + private enum ComponentParseResult : byte + { + // Do not change or add values in this enum unless you review every use of the TimeSpanSplitter.Separators field. That field is an "array of four + // ComponentParseResults" encoded as a 32-bit integer with each of its four bytes containing one of 0 (NoMoreData), 1 (Colon) or 2 (Period). + // (So a value of 0x01010200 means the string parsed as "nn:nn:nn.nnnnnnn") + NoMoreData = 0, + Colon = 1, + Period = 2, + ParseFailure = 3, + } + + private struct TimeSpanSplitter + { + public uint V1; + public uint V2; + public uint V3; + public uint V4; + public uint V5; + + public bool IsNegative; + + // Encodes an "array of four ComponentParseResults" as a 32-bit integer with each of its four bytes containing one of 0 (NoMoreData), 1 (Colon) or 2 (Period). + // (So a value of 0x01010200 means the string parsed as "nn:nn:nn.nnnnnnn") + public uint Separators; + + public bool TrySplitTimeSpan(ReadOnlySpan<byte> source, bool periodUsedToSeparateDay, out int bytesConsumed) + { + int srcIndex = 0; + byte c = default; + + // Unlike many other data types, TimeSpan allow leading whitespace. + while (srcIndex != source.Length) + { + c = source[srcIndex]; + if (!(c == ' ' || c == '\t')) + break; + srcIndex++; + } + + if (srcIndex == source.Length) + { + bytesConsumed = 0; + return false; + } + + // Check for an option negative sign. ('+' is not allowed.) + if (c == Utf8Constants.Minus) + { + IsNegative = true; + srcIndex++; + if (srcIndex == source.Length) + { + bytesConsumed = 0; + return false; + } + } + + // From here, we terminate on anything that's not a digit, ':' or '.' The '.' is only allowed after at least three components have + // been specified. If we see it earlier, we'll assume that's an error and fail out rather than treating it as the end of data. + + // + // Timespan has to start with a number - parse the first one. + // + if (!TryParseUInt32D(source.Slice(srcIndex), out V1, out int justConsumed)) + { + bytesConsumed = 0; + return false; + } + srcIndex += justConsumed; + + ComponentParseResult result; + + // + // Split out the second number (if any) For the 'c' format, a period might validly appear here as it;s used both to separate the day and the fraction - however, + // the fraction is always the fourth component at earliest, so if we do see a period at this stage, always parse the integer as a regular integer, not as + // a fraction. + // + result = ParseComponent(source, neverParseAsFraction: periodUsedToSeparateDay, ref srcIndex, out V2); + if (result == ComponentParseResult.ParseFailure) + { + bytesConsumed = 0; + return false; + } + else if (result == ComponentParseResult.NoMoreData) + { + bytesConsumed = srcIndex; + return true; + } + else + { + Debug.Assert(result == ComponentParseResult.Colon || result == ComponentParseResult.Period); + Separators |= ((uint)result) << 24; + } + + // + // Split out the third number (if any) + // + result = ParseComponent(source, false, ref srcIndex, out V3); + if (result == ComponentParseResult.ParseFailure) + { + bytesConsumed = 0; + return false; + } + else if (result == ComponentParseResult.NoMoreData) + { + bytesConsumed = srcIndex; + return true; + } + else + { + Debug.Assert(result == ComponentParseResult.Colon || result == ComponentParseResult.Period); + Separators |= ((uint)result) << 16; + } + + // + // Split out the fourth number (if any) + // + result = ParseComponent(source, false, ref srcIndex, out V4); + if (result == ComponentParseResult.ParseFailure) + { + bytesConsumed = 0; + return false; + } + else if (result == ComponentParseResult.NoMoreData) + { + bytesConsumed = srcIndex; + return true; + } + else + { + Debug.Assert(result == ComponentParseResult.Colon || result == ComponentParseResult.Period); + Separators |= ((uint)result) << 8; + } + + // + // Split out the fifth number (if any) + // + result = ParseComponent(source, false, ref srcIndex, out V5); + if (result == ComponentParseResult.ParseFailure) + { + bytesConsumed = 0; + return false; + } + else if (result == ComponentParseResult.NoMoreData) + { + bytesConsumed = srcIndex; + return true; + } + else + { + Debug.Assert(result == ComponentParseResult.Colon || result == ComponentParseResult.Period); + Separators |= (uint)result; + } + + // + // There cannot legally be a sixth number. If the next character is a period or colon, treat this as a error as it's likely + // to indicate the start of a sixth number. Otherwise, treat as end of parse with data left over. + // + if (srcIndex != source.Length && (source[srcIndex] == Utf8Constants.Period || source[srcIndex] == Utf8Constants.Colon)) + { + bytesConsumed = 0; + return false; + } + + bytesConsumed = srcIndex; + return true; + } + + // + // Look for a separator followed by an unsigned integer. + // + private static ComponentParseResult ParseComponent(ReadOnlySpan<byte> source, bool neverParseAsFraction, ref int srcIndex, out uint value) + { + if (srcIndex == source.Length) + { + value = default; + return ComponentParseResult.NoMoreData; + } + + byte c = source[srcIndex]; + if (c == Utf8Constants.Colon || (c == Utf8Constants.Period && neverParseAsFraction)) + { + srcIndex++; + + if (!TryParseUInt32D(source.Slice(srcIndex), out value, out int bytesConsumed)) + { + value = default; + return ComponentParseResult.ParseFailure; + } + + srcIndex += bytesConsumed; + return c == Utf8Constants.Colon ? ComponentParseResult.Colon : ComponentParseResult.Period; + } + else if (c == Utf8Constants.Period) + { + srcIndex++; + + if (!TryParseTimeSpanFraction(source.Slice(srcIndex), out value, out int bytesConsumed)) + { + value = default; + return ComponentParseResult.ParseFailure; + } + + srcIndex += bytesConsumed; + return ComponentParseResult.Period; + } + else + { + value = default; + return ComponentParseResult.NoMoreData; + } + } + } + } +} |