diff --git a/src/Discord.Net.Serialization/Discord.Net.Serialization.csproj b/src/Discord.Net.Serialization/Discord.Net.Serialization.csproj index 5d2cba89e..29df2bbda 100644 --- a/src/Discord.Net.Serialization/Discord.Net.Serialization.csproj +++ b/src/Discord.Net.Serialization/Discord.Net.Serialization.csproj @@ -1,4 +1,4 @@ - + Discord.Net.Serialization @@ -8,11 +8,11 @@ true - - + + - - + + - + \ No newline at end of file diff --git a/src/Discord.Net.Serialization/Extensions/BufferExtensions.cs b/src/Discord.Net.Serialization/Extensions/BufferExtensions.cs index 7f1bc1f71..784c8a682 100644 --- a/src/Discord.Net.Serialization/Extensions/BufferExtensions.cs +++ b/src/Discord.Net.Serialization/Extensions/BufferExtensions.cs @@ -107,14 +107,17 @@ namespace Discord.Serialization { int index = 0; bytesConsumed = 0; - if (!TryParseDateParts(text, ref index, ref bytesConsumed, out int year, out int month, out int day) || - !TryParseTimeParts(text, ref index, ref bytesConsumed, out int hour, out int min, out int sec, out int milli) || - !TryParseTimezoneParts(text, ref index, ref bytesConsumed, out var offset)) + if (!TryParseDateParts(text, ref index, out int year, out int month, out int day) || + !TryParseTimeParts(text, ref index, out int hour, out int min, out int sec, out int milli, out int milliLength) || + !TryParseTimezoneParts(text, ref index, out var offset)) { value = default; return false; } + if (milliLength == 6) + milli /= 1000; + value = new DateTime(year, month, day, hour, min, sec, milli, DateTimeKind.Utc); if (offset != TimeSpan.Zero) value -= offset; @@ -124,19 +127,23 @@ namespace Discord.Serialization { int index = 0; bytesConsumed = 0; - if (!TryParseDateParts(text, ref index, ref bytesConsumed, out int year, out int month, out int day) || - !TryParseTimeParts(text, ref index, ref bytesConsumed, out int hour, out int min, out int sec, out int milli) || - !TryParseTimezoneParts(text, ref index, ref bytesConsumed, out var offset)) + if (!TryParseDateParts(text, ref index, out int year, out int month, out int day) || + !TryParseTimeParts(text, ref index, out int hour, out int min, out int sec, out int milli, out int milliLength) || + !TryParseTimezoneParts(text, ref index, out var offset)) { value = default; return false; } + + if (milliLength == 6) + milli /= 1000; + value = new DateTimeOffset(year, month, day, hour, min, sec, milli, offset); return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryParseDateParts(ReadOnlySpan text, ref int index, ref int bytesConsumed, + private static bool TryParseDateParts(ReadOnlySpan text, ref int index, out int year, out int month, out int day) { year = 0; @@ -145,65 +152,54 @@ namespace Discord.Serialization //Format: YYYY-MM-DD if (text.Length < 10 || - !TryParseNumericPart(text, ref index, out year, ref bytesConsumed, 4) || + !TryParseNumericPart(text, ref index, out year, out var ignored, 4) || text[index++] != (byte)'-' || - !TryParseNumericPart(text, ref index, out month, ref bytesConsumed, 2) || + !TryParseNumericPart(text, ref index, out month, out ignored, 2) || text[index++] != (byte)'-' || - !TryParseNumericPart(text, ref index, out day, ref bytesConsumed, 2)) - { - bytesConsumed = 0; + !TryParseNumericPart(text, ref index, out day, out ignored, 2)) return false; - } return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryParseTimeParts(ReadOnlySpan text, ref int index, ref int bytesConsumed, - out int hour, out int minute, out int second, out int millisecond) + private static bool TryParseTimeParts(ReadOnlySpan text, ref int index, + out int hour, out int minute, out int second, out int millisecond, out int milliLength) { hour = 0; minute = 0; second = 0; millisecond = 0; + milliLength = 0; //Time (hh:mm) if (text.Length < 16 || text[index] != (byte)'T') //0001-01-01T01:01 return true; index++; - if (!TryParseNumericPart(text, ref index, out hour, ref bytesConsumed, 2) || + if (!TryParseNumericPart(text, ref index, out hour, out var ignored, 2) || text[index++] != (byte)':' || - !TryParseNumericPart(text, ref index, out minute, ref bytesConsumed, 2)) - { - bytesConsumed = 0; + !TryParseNumericPart(text, ref index, out minute, out ignored, 2)) return false; - } //Time (hh:mm:ss) if (text.Length < 19 || text[index] != (byte)':') //0001-01-01T01:01:01 return true; index++; - if (!TryParseNumericPart(text, ref index, out second, ref bytesConsumed, 2)) - { - bytesConsumed = 0; + if (!TryParseNumericPart(text, ref index, out second, out ignored, 2)) return false; - } //Time (hh:mm:ss.sss) if (text.Length < 21 || text[index] != (byte)'.') //0001-01-01T01:01:01.1 return true; index++; - - if (!TryParseNumericPart(text, ref index, out millisecond, ref bytesConsumed, 3)) - { - bytesConsumed = 0; + + if (!TryParseNumericPart(text, ref index, out millisecond, out milliLength, 6)) return false; - } return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryParseTimezoneParts(ReadOnlySpan text, ref int index, ref int bytesConsumed, + private static bool TryParseTimezoneParts(ReadOnlySpan text, ref int index, out TimeSpan offset) { offset = default; @@ -222,13 +218,10 @@ namespace Discord.Serialization return false; index++; - if (!TryParseNumericPart(text, ref index, out int hours, ref bytesConsumed, 2) || + if (!TryParseNumericPart(text, ref index, out int hours, out var ignored, 2) || text[index++] != (byte)':' || - !TryParseNumericPart(text, ref index, out int minutes, ref bytesConsumed, 2)) - { - bytesConsumed = 0; + !TryParseNumericPart(text, ref index, out int minutes, out ignored, 2)) return false; - } offset = new TimeSpan(hours, minutes, 0); if (isNegative) offset = -offset; return true; @@ -239,16 +232,17 @@ namespace Discord.Serialization //From https://github.com/dotnet/corefxlab/blob/master/src/System.Text.Primitives/System/Text/Parsing/Unsigned.cs [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryParseNumericPart(ReadOnlySpan text, ref int index, out int value, ref int bytesConsumed, int maxLength) + private static bool TryParseNumericPart(ReadOnlySpan text, ref int index, out int value, out int valueLength, int maxLength) { // Parse the first digit separately. If invalid here, we need to return false. uint firstDigit = text[index++] - 48u; // '0' if (firstDigit > 9) { - bytesConsumed = 0; + valueLength = 0; value = default; return false; } + valueLength = 1; uint parsedValue = firstDigit; for (int i = 1; i < maxLength && index < text.Length; i++, index++) @@ -256,14 +250,13 @@ namespace Discord.Serialization uint nextDigit = text[index] - 48u; // '0' if (nextDigit > 9) { - bytesConsumed = index; value = (int)(parsedValue); return true; } + valueLength++; parsedValue = parsedValue * 10 + nextDigit; } - - bytesConsumed = text.Length; + value = (int)(parsedValue); return true; } diff --git a/src/Discord.Net.Serialization/Json/JsonSerializer.cs b/src/Discord.Net.Serialization/Json/JsonSerializer.cs index 18581ed36..d195951d6 100644 --- a/src/Discord.Net.Serialization/Json/JsonSerializer.cs +++ b/src/Discord.Net.Serialization/Json/JsonSerializer.cs @@ -3,6 +3,7 @@ using System.Reflection; using System.Text; using System.Text.Formatting; using System.Text.Json; +using System.Text.Utf8; namespace Discord.Serialization.Json { @@ -22,15 +23,18 @@ namespace Discord.Serialization.Json var converter = (JsonPropertyConverter)GetConverter(typeof(TValue), propInfo); return new JsonPropertyMap(this, propInfo, converter); } - - public override TModel Read(ReadOnlyBuffer data) + + public TModel Read(Utf8String str) + => Read(str.Bytes); + public override TModel Read(ReadOnlySpan data) { - var reader = new JsonReader(data.Span, SymbolTable.InvariantUtf8); + var reader = new JsonReader(data, SymbolTable.InvariantUtf8); if (!reader.Read()) return default; var converter = GetConverter(typeof(TModel)) as JsonPropertyConverter; return converter.Read(null, null, ref reader, false); } + public override void Write(ArrayFormatter stream, TModel model) { var writer = new JsonWriter(stream); diff --git a/src/Discord.Net.Serialization/Serializer.cs b/src/Discord.Net.Serialization/Serializer.cs index 98e74164e..b58354103 100644 --- a/src/Discord.Net.Serialization/Serializer.cs +++ b/src/Discord.Net.Serialization/Serializer.cs @@ -77,7 +77,9 @@ namespace Discord.Serialization => _createPropertyMapMethod.MakeGenericMethod(typeof(TModel), propInfo.PropertyType).Invoke(this, new object[] { propInfo }) as PropertyMap; protected abstract PropertyMap CreatePropertyMap(PropertyInfo propInfo); - public abstract TModel Read(ReadOnlyBuffer data); + public TModel Read(ReadOnlyBuffer data) + => Read(data.Span); + public abstract TModel Read(ReadOnlySpan data); public abstract void Write(ArrayFormatter stream, TModel model); } } diff --git a/src/Discord.Net.Serialization/_corefxlab/System.Text.Json/System/Text/Json/JsonConstants.cs b/src/Discord.Net.Serialization/_corefxlab/System.Text.Json/System/Text/Json/JsonConstants.cs index 604f781e9..b4d5a7d06 100644 --- a/src/Discord.Net.Serialization/_corefxlab/System.Text.Json/System/Text/Json/JsonConstants.cs +++ b/src/Discord.Net.Serialization/_corefxlab/System.Text.Json/System/Text/Json/JsonConstants.cs @@ -25,6 +25,7 @@ namespace System.Text.Json public const byte ListSeperator = (byte)','; public const byte KeyValueSeperator = (byte)':'; public const byte Quote = (byte)'"'; + public const byte Backslash = (byte)'\\'; #endregion Control characters diff --git a/src/Discord.Net.Serialization/_corefxlab/System.Text.Json/System/Text/Json/JsonReader.cs b/src/Discord.Net.Serialization/_corefxlab/System.Text.Json/System/Text/Json/JsonReader.cs index 22b526db8..7d2981fab 100644 --- a/src/Discord.Net.Serialization/_corefxlab/System.Text.Json/System/Text/Json/JsonReader.cs +++ b/src/Discord.Net.Serialization/_corefxlab/System.Text.Json/System/Text/Json/JsonReader.cs @@ -1,7 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Buffers; +using System.Collections.Sequences; using System.Runtime.CompilerServices; +using System.Text.Formatting; namespace System.Text.Json { @@ -14,6 +17,7 @@ namespace System.Text.Json private readonly SymbolTable _symbolTable; private ReadOnlySpan _buffer; + private ResizableArray _working; // Depth tracks the recursive depth of the nested objects / arrays within the JSON data. internal int _depth; @@ -71,6 +75,7 @@ namespace System.Text.Json _symbolTable = symbolTable; _depth = 0; _containerMask = 0; + _working = default; if (_symbolTable == SymbolTable.InvariantUtf8) _encoderState = JsonEncoderState.UseFastUtf8; @@ -660,17 +665,89 @@ namespace System.Text.Json // If we are in this method, the first char is already known to be a JSON quote character. // Skip through the bytes until we find the closing JSON quote character. int idx = 1; - while (idx < length && Unsafe.Add(ref src, idx++) != JsonConstants.Quote) ; + int start = idx; + bool hasEscapes = false; - // If we hit the end of the source and never saw an ending quote, then fail. - if (idx == length && Unsafe.Add(ref src, idx - 1) != JsonConstants.Quote) - throw new JsonReaderException(); + while (idx < length) + { + byte c = Unsafe.Add(ref src, idx++); + if (c == JsonConstants.Quote) + break; + else if (c == JsonConstants.Backslash) + { + hasEscapes = true; + break; + } + } - // Calculate the real start of the property name based on our current buffer location. - // Also, skip the opening JSON quote character. - int startIndex = (int)Unsafe.ByteOffset(ref _buffer.DangerousGetPinnableReference(), ref src) + 1; + if (!hasEscapes) //Fast route + { + // If we hit the end of the source and never saw an ending quote, then fail. + if (idx == length && Unsafe.Add(ref src, idx - 1) != JsonConstants.Quote) + throw new JsonReaderException(); + + // Calculate the real start of the property name based on our current buffer location. + // Also, skip the opening JSON quote character. + int startIndex = (int)Unsafe.ByteOffset(ref _buffer.DangerousGetPinnableReference(), ref src) + 1; - Value = _buffer.Slice(startIndex, idx - 2); // -2 to exclude the quote characters. + Value = _buffer.Slice(startIndex, idx - 2); // -2 to exclude the quote characters. + } + else //Slow route + { + if (_working.Items == null) + _working = new ResizableArray(ArrayPool.Shared.Rent(128)); + _working.Clear(); + + int arrLength = idx - start; + idx = start; + + bool isEscaping = false; + bool success = false; + while (idx < length) + { + byte c = Unsafe.Add(ref src, idx); + if (isEscaping) + isEscaping = false; + else if (c == JsonConstants.Backslash || c == JsonConstants.Quote) + { + int segmentLength = idx - start; + if (segmentLength != 0) + { + //Ensure we have enough space in the buffer + int remaining = _working.Capacity - _working.Count; + if (segmentLength > remaining) + { + int doubleSize = _working.Free.Count * 2; + int minNewSize = _working.Capacity + segmentLength; + int newSize = minNewSize > doubleSize ? minNewSize : doubleSize; + var newArray = ArrayPool.Shared.Rent(minNewSize + _working.Count); + var oldArray = _working.Resize(newArray); + ArrayPool.Shared.Return(oldArray); + } + + //Copy all data before the backslash + var span = _working.Free.AsSpan(); + Unsafe.CopyBlock(ref span.DangerousGetPinnableReference(), ref Unsafe.Add(ref src, start), (uint)segmentLength); + _working.Count += segmentLength; + } + start = idx + 1; + isEscaping = true; + + if (c == JsonConstants.Quote) + { + idx++; + success = true; + break; + } + } + idx++; + } + + if (!success) + throw new JsonReaderException(); + + Value = _working.Full; + } ValueType = JsonValueType.String; return idx; }