Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Commit

Permalink
Implement escaped-character handling in Utf8JsonReader and JsonElement (
Browse files Browse the repository at this point in the history
#36347)

* Implement escaped-character handling in (Try)GetDateTime(Offset) & (Try)GetGuid in Utf8JsonReader and JsonElement

This addresses https://github.com/dotnet/corefx/issues/36202.

* Address review comments

* Address more comments

* Simplify stackalloc logic

* Update fraction digit cap to 15

* Add (Try)GetGuid to JsonElement

* Address review feedback

* Add FormatException documentation

* Address review feedback

* Address review comments
  • Loading branch information
layomia authored Apr 2, 2019
1 parent cfaf821 commit 769c5ac
Show file tree
Hide file tree
Showing 10 changed files with 399 additions and 58 deletions.
2 changes: 2 additions & 0 deletions src/System.Text.Json/ref/System.Text.Json.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public readonly partial struct JsonElement
public System.DateTimeOffset GetDateTimeOffset() { throw null; }
public decimal GetDecimal() { throw null; }
public double GetDouble() { throw null; }
public System.Guid GetGuid() { throw null; }
public int GetInt32() { throw null; }
public long GetInt64() { throw null; }
public System.Text.Json.JsonElement GetProperty(System.ReadOnlySpan<byte> utf8PropertyName) { throw null; }
Expand All @@ -58,6 +59,7 @@ public readonly partial struct JsonElement
public bool TryGetDateTimeOffset(out System.DateTimeOffset value) { throw null; }
public bool TryGetDecimal(out decimal value) { throw null; }
public bool TryGetDouble(out double value) { throw null; }
public bool TryGetGuid(out System.Guid value) { throw null; }
public bool TryGetInt32(out int value) { throw null; }
public bool TryGetInt64(out long value) { throw null; }
public bool TryGetProperty(System.ReadOnlySpan<byte> utf8PropertyName, out System.Text.Json.JsonElement value) { throw null; }
Expand Down
70 changes: 60 additions & 10 deletions src/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -465,15 +465,24 @@ internal bool TryGetValue(int index, out DateTime value)
ReadOnlySpan<byte> data = _utf8Json.Span;
ReadOnlySpan<byte> segment = data.Slice(row.Location, row.SizeOrLength);

if (JsonHelpers.TryParseAsISO(segment, out DateTime tmp, out int bytesConsumed) &&
segment.Length == bytesConsumed)
if (!JsonReaderHelper.IsValidDateTimeOffsetParseLength(segment.Length))
{
value = tmp;
return true;
value = default;
return false;
}

// Segment needs to be unescaped
if (row.HasComplexChildren)
{
return JsonReaderHelper.TryGetEscapedDateTime(segment, out value);
}

Debug.Assert(segment.IndexOf(JsonConstants.BackSlash) == -1);

value = default;
return false;
return (segment.Length <= JsonConstants.MaximumDateTimeOffsetParseLength)
&& JsonHelpers.TryParseAsISO(segment, out value, out int bytesConsumed)
&& segment.Length == bytesConsumed;
}

/// <summary>
Expand All @@ -490,15 +499,56 @@ internal bool TryGetValue(int index, out DateTimeOffset value)
ReadOnlySpan<byte> data = _utf8Json.Span;
ReadOnlySpan<byte> segment = data.Slice(row.Location, row.SizeOrLength);

if (JsonHelpers.TryParseAsISO(segment, out DateTimeOffset tmp, out int bytesConsumed) &&
segment.Length == bytesConsumed)
if (!JsonReaderHelper.IsValidDateTimeOffsetParseLength(segment.Length))
{
value = tmp;
return true;
value = default;
return false;
}

// Segment needs to be unescaped
if (row.HasComplexChildren)
{
return JsonReaderHelper.TryGetEscapedDateTimeOffset(segment, out value);
}

Debug.Assert(segment.IndexOf(JsonConstants.BackSlash) == -1);

value = default;
return false;
return (segment.Length <= JsonConstants.MaximumDateTimeOffsetParseLength)
&& JsonHelpers.TryParseAsISO(segment, out value, out int bytesConsumed)
&& segment.Length == bytesConsumed;
}

/// <summary>
/// This is an implementation detail and MUST NOT be called by source-package consumers.
/// </summary>
internal bool TryGetValue(int index, out Guid value)
{
CheckNotDisposed();

DbRow row = _parsedData.Get(index);

CheckExpectedType(JsonTokenType.String, row.TokenType);

ReadOnlySpan<byte> data = _utf8Json.Span;
ReadOnlySpan<byte> segment = data.Slice(row.Location, row.SizeOrLength);

if (segment.Length > JsonConstants.MaximumEscapedGuidLength)
{
value = default;
return false;
}

// Segment needs to be unescaped
if (row.HasComplexChildren)
{
return JsonReaderHelper.TryGetEscapedGuid(segment, out value);
}

Debug.Assert(segment.IndexOf(JsonConstants.BackSlash) == -1);

value = default;
return (segment.Length == JsonConstants.MaximumFormatGuidLength) && Utf8Parser.TryParse(segment, out value, out _, 'D');
}

/// <summary>
Expand Down
57 changes: 57 additions & 0 deletions src/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,9 @@ public bool TryGetDateTime(out DateTime value)
/// <exception cref="InvalidOperationException">
/// This value's <see cref="Type"/> is not <see cref="JsonValueType.String"/>.
/// </exception>
/// <exception cref="FormatException">
/// The value cannot be represented as a <see cref="DateTime"/>.
/// </exception>
/// <exception cref="ObjectDisposedException">
/// The parent <see cref="JsonDocument"/> has been disposed.
/// </exception>
Expand Down Expand Up @@ -822,6 +825,9 @@ public bool TryGetDateTimeOffset(out DateTimeOffset value)
/// <exception cref="InvalidOperationException">
/// This value's <see cref="Type"/> is not <see cref="JsonValueType.String"/>.
/// </exception>
/// <exception cref="FormatException">
/// The value cannot be represented as a <see cref="DateTimeOffset"/>.
/// </exception>
/// <exception cref="ObjectDisposedException">
/// The parent <see cref="JsonDocument"/> has been disposed.
/// </exception>
Expand All @@ -836,6 +842,57 @@ public DateTimeOffset GetDateTimeOffset()
throw new FormatException();
}

/// <summary>
/// Attempts to represent the current JSON string as a <see cref="Guid"/>.
/// </summary>
/// <param name="value">Receives the value.</param>
/// <remarks>
/// This method does not create a Guid representation of values other than JSON strings.
/// </remarks>
/// <returns>
/// <see langword="true"/> if the string can be represented as a <see cref="Guid"/>,
/// <see langword="false"/> otherwise.
/// </returns>
/// <exception cref="InvalidOperationException">
/// This value's <see cref="Type"/> is not <see cref="JsonValueType.String"/>.
/// </exception>
/// <exception cref="ObjectDisposedException">
/// The parent <see cref="JsonDocument"/> has been disposed.
/// </exception>
public bool TryGetGuid(out Guid value)
{
CheckValidInstance();

return _parent.TryGetValue(_idx, out value);
}

/// <summary>
/// Gets the value of the element as a <see cref="Guid"/>.
/// </summary>
/// <remarks>
/// This method does not create a Guid representation of values other than JSON strings.
/// </remarks>
/// <returns>The value of the element as a <see cref="Guid"/>.</returns>
/// <exception cref="InvalidOperationException">
/// This value's <see cref="Type"/> is not <see cref="JsonValueType.String"/>.
/// </exception>
/// <exception cref="FormatException">
/// The value cannot be represented as a <see cref="Guid"/>.
/// </exception>
/// <exception cref="ObjectDisposedException">
/// The parent <see cref="JsonDocument"/> has been disposed.
/// </exception>
/// <seealso cref="ToString"/>
public Guid GetGuid()
{
if (TryGetGuid(out Guid value))
{
return value;
}

throw new FormatException();
}

/// <summary>
/// This is an implementation detail and MUST NOT be called by source-package consumers.
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions src/System.Text.Json/src/System/Text/Json/JsonConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ internal static class JsonConstants
public const int MaxDateTimeUtcOffsetHours = 14; // The UTC offset portion of a TimeSpan or DateTime can be no more than 14 hours and no less than -14 hours.
public const int DateTimeNumFractionDigits = 7; // TimeSpan and DateTime formats allow exactly up to many digits for specifying the fraction after the seconds.
public const int MaxDateTimeFraction = 9_999_999; // The largest fraction expressible by TimeSpan and DateTime formats
public const int DateTimeParseNumFractionDigits = 16; // The maximum number of fraction digits the Json DateTime parser allows
public const int MaximumDateTimeOffsetParseLength = (MaximumFormatDateTimeOffsetLength +
(DateTimeParseNumFractionDigits - DateTimeNumFractionDigits)); // Like StandardFormat 'O' for DateTimeOffset, but allowing 9 additional (up to 16) fraction digits.
public const int MinimumDateTimeParseLength = 10; // YYYY-MM-DD
public const int MaximumEscapedDateTimeOffsetParseLength = MaxExpansionFactorWhileEscaping * MaximumDateTimeOffsetParseLength;

internal const char ScientificNotationFormat = 'e';

Expand Down
24 changes: 13 additions & 11 deletions src/System.Text.Json/src/System/Text/Json/JsonHelpers.Date.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ private static bool TryParseDateTimeOffset(ReadOnlySpan<byte> source, out DateTi
int hour = 0;
int minute = 0;
int second = 0;
int fraction = 0;
int fraction = 0; // This value should never be greater than 9_999_999.
int offsetHours = 0;
int offsetMinutes = 0;
byte offsetToken = default;
Expand Down Expand Up @@ -215,25 +215,21 @@ private static bool TryParseDateTimeOffset(ReadOnlySpan<byte> source, out DateTi
// Source does not have enough characters for YYYY-MM-DDThh:mm:ss.s
if (source.Length < 21)
{
value = default;
bytesConsumed = 0;
kind = default;
return false;
goto ReturnFalse;
}

sourceIndex = 20;

// Parse fraction
// Parse fraction. This value should never be greater than 9_999_999
{
int numDigitsRead = 0;
int fractionEnd = Math.Min(sourceIndex + JsonConstants.DateTimeParseNumFractionDigits, source.Length);

while (sourceIndex < source.Length && IsDigit(curByte = source[sourceIndex]))
while (sourceIndex < fractionEnd && IsDigit(curByte = source[sourceIndex]))
{
int prevFractionTimesTen = fraction * 10;

if ((prevFractionTimesTen + (int)(curByte - (uint)'0') <= JsonConstants.MaxDateTimeFraction) && (numDigitsRead < JsonConstants.DateTimeNumFractionDigits))
if (numDigitsRead < JsonConstants.DateTimeNumFractionDigits)
{
fraction = prevFractionTimesTen + (int)(curByte - (uint)'0');
fraction = (fraction * 10) + (int)(curByte - (uint)'0');
numDigitsRead++;
}

Expand All @@ -258,6 +254,8 @@ private static bool TryParseDateTimeOffset(ReadOnlySpan<byte> source, out DateTi
goto FinishedParsing;
}

curByte = source[sourceIndex];

if (curByte == JsonConstants.UtcOffsetToken)
{
bytesConsumed++;
Expand All @@ -269,6 +267,10 @@ private static bool TryParseDateTimeOffset(ReadOnlySpan<byte> source, out DateTi
offsetToken = source[sourceIndex++];
goto ParseOffset;
}
else if (IsDigit(curByte))
{
goto ReturnFalse;
}

goto FinishedParsing;

Expand Down
8 changes: 8 additions & 0 deletions src/System.Text.Json/src/System/Text/Json/JsonHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ public static bool IsInRangeInclusive(byte value, byte lowerBound, byte upperBou
public static bool IsInRangeInclusive(int value, int lowerBound, int upperBound)
=> (uint)(value - lowerBound) <= (uint)(upperBound - lowerBound);

/// <summary>
/// Returns <see langword="true"/> iff <paramref name="value"/> is between
/// <paramref name="lowerBound"/> and <paramref name="upperBound"/>, inclusive.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsInRangeInclusive(long value, long lowerBound, long upperBound)
=> (ulong)(value - lowerBound) <= (ulong)(upperBound - lowerBound);

/// <summary>
/// Returns <see langword="true"/> iff <paramref name="value"/> is between
/// <paramref name="lowerBound"/> and <paramref name="upperBound"/>, inclusive.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// 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;
using System.Buffers.Text;
using System.Diagnostics;
using System.Numerics;
Expand Down Expand Up @@ -248,27 +249,75 @@ private static int LocateFirstFoundByte(ulong match)
0x02ul << 40 |
0x01ul << 48) + 1;

public static bool TryGetEscapedGuid(ReadOnlySpan<byte> span, out Guid value)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsValidDateTimeOffsetParseLength(int length)
{
return JsonHelpers.IsInRangeInclusive(length, JsonConstants.MinimumDateTimeParseLength, JsonConstants.MaximumEscapedDateTimeOffsetParseLength);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsValidDateTimeOffsetParseLength(long length)
{
return JsonHelpers.IsInRangeInclusive(length, JsonConstants.MinimumDateTimeParseLength, JsonConstants.MaximumEscapedDateTimeOffsetParseLength);
}

public static bool TryGetEscapedDateTime(ReadOnlySpan<byte> source, out DateTime value)
{
int backslash = source.IndexOf(JsonConstants.BackSlash);
Debug.Assert(backslash != -1);

Debug.Assert(source.Length <= JsonConstants.MaximumEscapedDateTimeOffsetParseLength);
Span<byte> sourceUnescaped = stackalloc byte[source.Length];

Unescape(source, sourceUnescaped, backslash, out int written);
Debug.Assert(written > 0);

sourceUnescaped = sourceUnescaped.Slice(0, written);
Debug.Assert(!sourceUnescaped.IsEmpty);

value = default;
return (sourceUnescaped.Length <= JsonConstants.MaximumDateTimeOffsetParseLength)
&& JsonHelpers.TryParseAsISO(sourceUnescaped, out value, out int bytesConsumed)
&& sourceUnescaped.Length == bytesConsumed;
}

public static bool TryGetEscapedDateTimeOffset(ReadOnlySpan<byte> source, out DateTimeOffset value)
{
Debug.Assert(span.Length <= JsonConstants.MaximumEscapedGuidLength);
int backslash = source.IndexOf(JsonConstants.BackSlash);
Debug.Assert(backslash != -1);

Debug.Assert(source.Length <= JsonConstants.MaximumEscapedDateTimeOffsetParseLength);
Span<byte> sourceUnescaped = stackalloc byte[source.Length];

Unescape(source, sourceUnescaped, backslash, out int written);
Debug.Assert(written > 0);

sourceUnescaped = sourceUnescaped.Slice(0, written);
Debug.Assert(!sourceUnescaped.IsEmpty);

int idx = span.IndexOf(JsonConstants.BackSlash);
value = default;
return (sourceUnescaped.Length <= JsonConstants.MaximumDateTimeOffsetParseLength)
&& JsonHelpers.TryParseAsISO(sourceUnescaped, out value, out int bytesConsumed)
&& sourceUnescaped.Length == bytesConsumed;
}

public static bool TryGetEscapedGuid(ReadOnlySpan<byte> source, out Guid value)
{
Debug.Assert(source.Length <= JsonConstants.MaximumEscapedGuidLength);

int idx = source.IndexOf(JsonConstants.BackSlash);
Debug.Assert(idx != -1);

Span<byte> utf8Unescaped = stackalloc byte[span.Length];
Span<byte> utf8Unescaped = stackalloc byte[source.Length];

Unescape(span, utf8Unescaped, idx, out int written);
Unescape(source, utf8Unescaped, idx, out int written);
Debug.Assert(written > 0);

utf8Unescaped = utf8Unescaped.Slice(0, written);
Debug.Assert(!utf8Unescaped.IsEmpty);

if (utf8Unescaped.Length != JsonConstants.MaximumFormatGuidLength)
{
value = default;
return false;
}
return Utf8Parser.TryParse(utf8Unescaped, out value, out int bytesConsumed, 'D') && utf8Unescaped.Length == bytesConsumed;
value = default;
return (utf8Unescaped.Length == JsonConstants.MaximumFormatGuidLength) && Utf8Parser.TryParse(utf8Unescaped, out value, out _, 'D');
}
}
}
Loading

0 comments on commit 769c5ac

Please sign in to comment.