| @@ -1,4 +1,4 @@ | |||
| <Project Sdk="Microsoft.NET.Sdk"> | |||
| <Project Sdk="Microsoft.NET.Sdk"> | |||
| <Import Project="../../Discord.Net.targets" /> | |||
| <PropertyGroup> | |||
| <AssemblyName>Discord.Net.Serialization</AssemblyName> | |||
| @@ -8,11 +8,11 @@ | |||
| <AllowUnsafeBlocks>true</AllowUnsafeBlocks> | |||
| </PropertyGroup> | |||
| <ItemGroup> | |||
| <PackageReference Include="System.Buffers" Version="4.4.0-preview2-25405-01" /> | |||
| <PackageReference Include="System.Collections.Immutable" Version="1.3.1" /> | |||
| <PackageReference Include="System.Buffers" Version="4.4.0" /> | |||
| <PackageReference Include="System.Collections.Immutable" Version="1.4.0" /> | |||
| <PackageReference Include="System.Interactive.Async" Version="3.1.1" /> | |||
| <PackageReference Include="System.Memory" Version="4.4.0-preview2-25405-01" /> | |||
| <PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.4.0-preview2-25405-01" /> | |||
| <PackageReference Include="System.ValueTuple" Version="4.3.1" /> | |||
| <PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.4.0" /> | |||
| <PackageReference Include="System.ValueTuple" Version="4.4.0" /> | |||
| </ItemGroup> | |||
| </Project> | |||
| </Project> | |||
| @@ -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<byte> text, ref int index, ref int bytesConsumed, | |||
| private static bool TryParseDateParts(ReadOnlySpan<byte> 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<byte> text, ref int index, ref int bytesConsumed, | |||
| out int hour, out int minute, out int second, out int millisecond) | |||
| private static bool TryParseTimeParts(ReadOnlySpan<byte> 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<byte> text, ref int index, ref int bytesConsumed, | |||
| private static bool TryParseTimezoneParts(ReadOnlySpan<byte> 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<byte> text, ref int index, out int value, ref int bytesConsumed, int maxLength) | |||
| private static bool TryParseNumericPart(ReadOnlySpan<byte> 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; | |||
| } | |||
| @@ -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<TValue>)GetConverter(typeof(TValue), propInfo); | |||
| return new JsonPropertyMap<TModel, TValue>(this, propInfo, converter); | |||
| } | |||
| public override TModel Read<TModel>(ReadOnlyBuffer<byte> data) | |||
| public TModel Read<TModel>(Utf8String str) | |||
| => Read<TModel>(str.Bytes); | |||
| public override TModel Read<TModel>(ReadOnlySpan<byte> 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<TModel>; | |||
| return converter.Read(null, null, ref reader, false); | |||
| } | |||
| public override void Write<TModel>(ArrayFormatter stream, TModel model) | |||
| { | |||
| var writer = new JsonWriter(stream); | |||
| @@ -77,7 +77,9 @@ namespace Discord.Serialization | |||
| => _createPropertyMapMethod.MakeGenericMethod(typeof(TModel), propInfo.PropertyType).Invoke(this, new object[] { propInfo }) as PropertyMap; | |||
| protected abstract PropertyMap CreatePropertyMap<TModel, TValue>(PropertyInfo propInfo); | |||
| public abstract TModel Read<TModel>(ReadOnlyBuffer<byte> data); | |||
| public TModel Read<TModel>(ReadOnlyBuffer<byte> data) | |||
| => Read<TModel>(data.Span); | |||
| public abstract TModel Read<TModel>(ReadOnlySpan<byte> data); | |||
| public abstract void Write<TModel>(ArrayFormatter stream, TModel model); | |||
| } | |||
| } | |||
| @@ -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 | |||
| @@ -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<byte> _buffer; | |||
| private ResizableArray<byte> _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<byte>(ArrayPool<byte>.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<byte>.Shared.Rent(minNewSize + _working.Count); | |||
| var oldArray = _working.Resize(newArray); | |||
| ArrayPool<byte>.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; | |||
| } | |||