@@ -1,4 +1,4 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | |||||
<Project Sdk="Microsoft.NET.Sdk"> | |||||
<Import Project="../../Discord.Net.targets" /> | <Import Project="../../Discord.Net.targets" /> | ||||
<PropertyGroup> | <PropertyGroup> | ||||
<AssemblyName>Discord.Net.Serialization</AssemblyName> | <AssemblyName>Discord.Net.Serialization</AssemblyName> | ||||
@@ -8,11 +8,11 @@ | |||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> | <AllowUnsafeBlocks>true</AllowUnsafeBlocks> | ||||
</PropertyGroup> | </PropertyGroup> | ||||
<ItemGroup> | <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.Interactive.Async" Version="3.1.1" /> | ||||
<PackageReference Include="System.Memory" Version="4.4.0-preview2-25405-01" /> | <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> | </ItemGroup> | ||||
</Project> | |||||
</Project> |
@@ -107,14 +107,17 @@ namespace Discord.Serialization | |||||
{ | { | ||||
int index = 0; | int index = 0; | ||||
bytesConsumed = 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; | value = default; | ||||
return false; | return false; | ||||
} | } | ||||
if (milliLength == 6) | |||||
milli /= 1000; | |||||
value = new DateTime(year, month, day, hour, min, sec, milli, DateTimeKind.Utc); | value = new DateTime(year, month, day, hour, min, sec, milli, DateTimeKind.Utc); | ||||
if (offset != TimeSpan.Zero) | if (offset != TimeSpan.Zero) | ||||
value -= offset; | value -= offset; | ||||
@@ -124,19 +127,23 @@ namespace Discord.Serialization | |||||
{ | { | ||||
int index = 0; | int index = 0; | ||||
bytesConsumed = 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; | value = default; | ||||
return false; | return false; | ||||
} | } | ||||
if (milliLength == 6) | |||||
milli /= 1000; | |||||
value = new DateTimeOffset(year, month, day, hour, min, sec, milli, offset); | value = new DateTimeOffset(year, month, day, hour, min, sec, milli, offset); | ||||
return true; | return true; | ||||
} | } | ||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | [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) | out int year, out int month, out int day) | ||||
{ | { | ||||
year = 0; | year = 0; | ||||
@@ -145,65 +152,54 @@ namespace Discord.Serialization | |||||
//Format: YYYY-MM-DD | //Format: YYYY-MM-DD | ||||
if (text.Length < 10 || | 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)'-' || | text[index++] != (byte)'-' || | ||||
!TryParseNumericPart(text, ref index, out month, ref bytesConsumed, 2) || | |||||
!TryParseNumericPart(text, ref index, out month, out ignored, 2) || | |||||
text[index++] != (byte)'-' || | 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 false; | ||||
} | |||||
return true; | return true; | ||||
} | } | ||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | [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; | hour = 0; | ||||
minute = 0; | minute = 0; | ||||
second = 0; | second = 0; | ||||
millisecond = 0; | millisecond = 0; | ||||
milliLength = 0; | |||||
//Time (hh:mm) | //Time (hh:mm) | ||||
if (text.Length < 16 || text[index] != (byte)'T') //0001-01-01T01:01 | if (text.Length < 16 || text[index] != (byte)'T') //0001-01-01T01:01 | ||||
return true; | return true; | ||||
index++; | 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)':' || | 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; | return false; | ||||
} | |||||
//Time (hh:mm:ss) | //Time (hh:mm:ss) | ||||
if (text.Length < 19 || text[index] != (byte)':') //0001-01-01T01:01:01 | if (text.Length < 19 || text[index] != (byte)':') //0001-01-01T01:01:01 | ||||
return true; | return true; | ||||
index++; | 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; | return false; | ||||
} | |||||
//Time (hh:mm:ss.sss) | //Time (hh:mm:ss.sss) | ||||
if (text.Length < 21 || text[index] != (byte)'.') //0001-01-01T01:01:01.1 | if (text.Length < 21 || text[index] != (byte)'.') //0001-01-01T01:01:01.1 | ||||
return true; | return true; | ||||
index++; | 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 false; | ||||
} | |||||
return true; | return true; | ||||
} | } | ||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | [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) | out TimeSpan offset) | ||||
{ | { | ||||
offset = default; | offset = default; | ||||
@@ -222,13 +218,10 @@ namespace Discord.Serialization | |||||
return false; | return false; | ||||
index++; | 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)':' || | 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; | return false; | ||||
} | |||||
offset = new TimeSpan(hours, minutes, 0); | offset = new TimeSpan(hours, minutes, 0); | ||||
if (isNegative) offset = -offset; | if (isNegative) offset = -offset; | ||||
return true; | 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 | //From https://github.com/dotnet/corefxlab/blob/master/src/System.Text.Primitives/System/Text/Parsing/Unsigned.cs | ||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | [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. | // Parse the first digit separately. If invalid here, we need to return false. | ||||
uint firstDigit = text[index++] - 48u; // '0' | uint firstDigit = text[index++] - 48u; // '0' | ||||
if (firstDigit > 9) | if (firstDigit > 9) | ||||
{ | { | ||||
bytesConsumed = 0; | |||||
valueLength = 0; | |||||
value = default; | value = default; | ||||
return false; | return false; | ||||
} | } | ||||
valueLength = 1; | |||||
uint parsedValue = firstDigit; | uint parsedValue = firstDigit; | ||||
for (int i = 1; i < maxLength && index < text.Length; i++, index++) | for (int i = 1; i < maxLength && index < text.Length; i++, index++) | ||||
@@ -256,14 +250,13 @@ namespace Discord.Serialization | |||||
uint nextDigit = text[index] - 48u; // '0' | uint nextDigit = text[index] - 48u; // '0' | ||||
if (nextDigit > 9) | if (nextDigit > 9) | ||||
{ | { | ||||
bytesConsumed = index; | |||||
value = (int)(parsedValue); | value = (int)(parsedValue); | ||||
return true; | return true; | ||||
} | } | ||||
valueLength++; | |||||
parsedValue = parsedValue * 10 + nextDigit; | parsedValue = parsedValue * 10 + nextDigit; | ||||
} | } | ||||
bytesConsumed = text.Length; | |||||
value = (int)(parsedValue); | value = (int)(parsedValue); | ||||
return true; | return true; | ||||
} | } | ||||
@@ -3,6 +3,7 @@ using System.Reflection; | |||||
using System.Text; | using System.Text; | ||||
using System.Text.Formatting; | using System.Text.Formatting; | ||||
using System.Text.Json; | using System.Text.Json; | ||||
using System.Text.Utf8; | |||||
namespace Discord.Serialization.Json | namespace Discord.Serialization.Json | ||||
{ | { | ||||
@@ -22,15 +23,18 @@ namespace Discord.Serialization.Json | |||||
var converter = (JsonPropertyConverter<TValue>)GetConverter(typeof(TValue), propInfo); | var converter = (JsonPropertyConverter<TValue>)GetConverter(typeof(TValue), propInfo); | ||||
return new JsonPropertyMap<TModel, TValue>(this, propInfo, converter); | 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()) | if (!reader.Read()) | ||||
return default; | return default; | ||||
var converter = GetConverter(typeof(TModel)) as JsonPropertyConverter<TModel>; | var converter = GetConverter(typeof(TModel)) as JsonPropertyConverter<TModel>; | ||||
return converter.Read(null, null, ref reader, false); | return converter.Read(null, null, ref reader, false); | ||||
} | } | ||||
public override void Write<TModel>(ArrayFormatter stream, TModel model) | public override void Write<TModel>(ArrayFormatter stream, TModel model) | ||||
{ | { | ||||
var writer = new JsonWriter(stream); | 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; | => _createPropertyMapMethod.MakeGenericMethod(typeof(TModel), propInfo.PropertyType).Invoke(this, new object[] { propInfo }) as PropertyMap; | ||||
protected abstract PropertyMap CreatePropertyMap<TModel, TValue>(PropertyInfo propInfo); | 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); | 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 ListSeperator = (byte)','; | ||||
public const byte KeyValueSeperator = (byte)':'; | public const byte KeyValueSeperator = (byte)':'; | ||||
public const byte Quote = (byte)'"'; | public const byte Quote = (byte)'"'; | ||||
public const byte Backslash = (byte)'\\'; | |||||
#endregion Control characters | #endregion Control characters | ||||
@@ -1,7 +1,10 @@ | |||||
// Copyright (c) Microsoft. All rights reserved. | // Copyright (c) Microsoft. All rights reserved. | ||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information. | // 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.Runtime.CompilerServices; | ||||
using System.Text.Formatting; | |||||
namespace System.Text.Json | namespace System.Text.Json | ||||
{ | { | ||||
@@ -14,6 +17,7 @@ namespace System.Text.Json | |||||
private readonly SymbolTable _symbolTable; | private readonly SymbolTable _symbolTable; | ||||
private ReadOnlySpan<byte> _buffer; | private ReadOnlySpan<byte> _buffer; | ||||
private ResizableArray<byte> _working; | |||||
// Depth tracks the recursive depth of the nested objects / arrays within the JSON data. | // Depth tracks the recursive depth of the nested objects / arrays within the JSON data. | ||||
internal int _depth; | internal int _depth; | ||||
@@ -71,6 +75,7 @@ namespace System.Text.Json | |||||
_symbolTable = symbolTable; | _symbolTable = symbolTable; | ||||
_depth = 0; | _depth = 0; | ||||
_containerMask = 0; | _containerMask = 0; | ||||
_working = default; | |||||
if (_symbolTable == SymbolTable.InvariantUtf8) | if (_symbolTable == SymbolTable.InvariantUtf8) | ||||
_encoderState = JsonEncoderState.UseFastUtf8; | _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. | // 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. | // Skip through the bytes until we find the closing JSON quote character. | ||||
int idx = 1; | 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; | ValueType = JsonValueType.String; | ||||
return idx; | return idx; | ||||
} | } | ||||