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;
}