-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
System.Text.Json: Add TimeSpanConverter #54186
Changes from all commits
9fa6f46
03b10a3
9c5c1f6
bce438f
0293935
cf8d104
6041e5c
2838c26
0248a15
f875e6f
f3e49ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1073,25 +1073,25 @@ internal bool TryGetDateTimeCore(out DateTime value) | |
{ | ||
ReadOnlySpan<byte> span = stackalloc byte[0]; | ||
|
||
int maximumLength = _stringHasEscaping ? JsonConstants.MaximumEscapedDateTimeOffsetParseLength : JsonConstants.MaximumDateTimeOffsetParseLength; | ||
|
||
if (HasValueSequence) | ||
{ | ||
long sequenceLength = ValueSequence.Length; | ||
|
||
if (!JsonHelpers.IsValidDateTimeOffsetParseLength(sequenceLength)) | ||
if (!JsonHelpers.IsInRangeInclusive(sequenceLength, JsonConstants.MinimumDateTimeParseLength, maximumLength)) | ||
{ | ||
value = default; | ||
return false; | ||
} | ||
|
||
Debug.Assert(sequenceLength <= JsonConstants.MaximumEscapedDateTimeOffsetParseLength); | ||
Span<byte> stackSpan = stackalloc byte[(int)sequenceLength]; | ||
|
||
Span<byte> stackSpan = stackalloc byte[_stringHasEscaping ? JsonConstants.MaximumEscapedDateTimeOffsetParseLength : JsonConstants.MaximumDateTimeOffsetParseLength]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for these changes. Searching for "stackalloc byte" in the STJ.csproj shows there might be a few more places that would benefit from this pattern, particularly in |
||
ValueSequence.CopyTo(stackSpan); | ||
span = stackSpan; | ||
span = stackSpan.Slice(0, (int)sequenceLength); | ||
} | ||
else | ||
{ | ||
if (!JsonHelpers.IsValidDateTimeOffsetParseLength(ValueSpan.Length)) | ||
if (!JsonHelpers.IsInRangeInclusive(ValueSpan.Length, JsonConstants.MinimumDateTimeParseLength, maximumLength)) | ||
{ | ||
value = default; | ||
return false; | ||
|
@@ -1141,25 +1141,25 @@ internal bool TryGetDateTimeOffsetCore(out DateTimeOffset value) | |
{ | ||
ReadOnlySpan<byte> span = stackalloc byte[0]; | ||
|
||
int maximumLength = _stringHasEscaping ? JsonConstants.MaximumEscapedDateTimeOffsetParseLength : JsonConstants.MaximumDateTimeOffsetParseLength; | ||
|
||
if (HasValueSequence) | ||
{ | ||
long sequenceLength = ValueSequence.Length; | ||
|
||
if (!JsonHelpers.IsValidDateTimeOffsetParseLength(sequenceLength)) | ||
if (!JsonHelpers.IsInRangeInclusive(sequenceLength, JsonConstants.MinimumDateTimeParseLength, maximumLength)) | ||
{ | ||
value = default; | ||
return false; | ||
} | ||
|
||
Debug.Assert(sequenceLength <= JsonConstants.MaximumEscapedDateTimeOffsetParseLength); | ||
Span<byte> stackSpan = stackalloc byte[(int)sequenceLength]; | ||
|
||
Span<byte> stackSpan = stackalloc byte[_stringHasEscaping ? JsonConstants.MaximumEscapedDateTimeOffsetParseLength : JsonConstants.MaximumDateTimeOffsetParseLength]; | ||
ValueSequence.CopyTo(stackSpan); | ||
span = stackSpan; | ||
span = stackSpan.Slice(0, (int)sequenceLength); | ||
} | ||
else | ||
{ | ||
if (!JsonHelpers.IsValidDateTimeOffsetParseLength(ValueSpan.Length)) | ||
if (!JsonHelpers.IsInRangeInclusive(ValueSpan.Length, JsonConstants.MinimumDateTimeParseLength, maximumLength)) | ||
{ | ||
value = default; | ||
return false; | ||
|
@@ -1210,24 +1210,25 @@ internal bool TryGetGuidCore(out Guid value) | |
{ | ||
ReadOnlySpan<byte> span = stackalloc byte[0]; | ||
|
||
int maximumLength = _stringHasEscaping ? JsonConstants.MaximumEscapedGuidLength : JsonConstants.MaximumFormatGuidLength; | ||
|
||
if (HasValueSequence) | ||
{ | ||
long sequenceLength = ValueSequence.Length; | ||
if (sequenceLength > JsonConstants.MaximumEscapedGuidLength) | ||
if (sequenceLength > maximumLength) | ||
{ | ||
value = default; | ||
return false; | ||
} | ||
|
||
Debug.Assert(sequenceLength <= JsonConstants.MaximumEscapedGuidLength); | ||
Span<byte> stackSpan = stackalloc byte[(int)sequenceLength]; | ||
|
||
Span<byte> stackSpan = stackalloc byte[_stringHasEscaping ? JsonConstants.MaximumEscapedGuidLength : JsonConstants.MaximumFormatGuidLength]; | ||
ValueSequence.CopyTo(stackSpan); | ||
span = stackSpan; | ||
span = stackSpan.Slice(0, (int)sequenceLength); | ||
} | ||
else | ||
{ | ||
if (ValueSpan.Length > JsonConstants.MaximumEscapedGuidLength) | ||
if (ValueSpan.Length > maximumLength) | ||
{ | ||
value = default; | ||
return false; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Buffers; | ||
using System.Buffers.Text; | ||
using System.Diagnostics; | ||
|
||
namespace System.Text.Json.Serialization.Converters | ||
{ | ||
internal sealed class TimeSpanConverter : JsonConverter<TimeSpan> | ||
{ | ||
private const int MinimumTimeSpanFormatLength = 8; // hh:mm:ss | ||
private const int MaximumTimeSpanFormatLength = 26; // -dddddddd.hh:mm:ss.fffffff | ||
private const int MaximumEscapedTimeSpanFormatLength = JsonConstants.MaxExpansionFactorWhileEscaping * MaximumTimeSpanFormatLength; | ||
|
||
public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | ||
{ | ||
if (reader.TokenType != JsonTokenType.String) | ||
{ | ||
throw ThrowHelper.GetInvalidOperationException_ExpectedString(reader.TokenType); | ||
} | ||
|
||
bool isEscaped = reader._stringHasEscaping; | ||
int maximumLength = isEscaped ? MaximumEscapedTimeSpanFormatLength : MaximumTimeSpanFormatLength; | ||
|
||
ReadOnlySpan<byte> source = stackalloc byte[0]; | ||
|
||
if (reader.HasValueSequence) | ||
{ | ||
ReadOnlySequence<byte> valueSequence = reader.ValueSequence; | ||
long sequenceLength = valueSequence.Length; | ||
|
||
if (!JsonHelpers.IsInRangeInclusive(sequenceLength, MinimumTimeSpanFormatLength, maximumLength)) | ||
{ | ||
throw ThrowHelper.GetFormatException(DataType.TimeSpan); | ||
} | ||
|
||
Span<byte> stackSpan = stackalloc byte[isEscaped ? MaximumEscapedTimeSpanFormatLength : MaximumTimeSpanFormatLength]; | ||
valueSequence.CopyTo(stackSpan); | ||
source = stackSpan.Slice(0, (int)sequenceLength); | ||
} | ||
else | ||
{ | ||
source = reader.ValueSpan; | ||
|
||
if (!JsonHelpers.IsInRangeInclusive(source.Length, MinimumTimeSpanFormatLength, maximumLength)) | ||
{ | ||
throw ThrowHelper.GetFormatException(DataType.TimeSpan); | ||
} | ||
} | ||
|
||
if (isEscaped) | ||
{ | ||
int backslash = source.IndexOf(JsonConstants.BackSlash); | ||
Debug.Assert(backslash != -1); | ||
|
||
Span<byte> sourceUnescaped = stackalloc byte[source.Length]; | ||
|
||
JsonReaderHelper.Unescape(source, sourceUnescaped, backslash, out int written); | ||
Debug.Assert(written > 0); | ||
|
||
source = sourceUnescaped.Slice(0, written); | ||
Debug.Assert(!source.IsEmpty); | ||
} | ||
|
||
byte firstChar = source[0]; | ||
if (!JsonHelpers.IsDigit(firstChar) && firstChar != '-') | ||
{ | ||
// Note: Utf8Parser.TryParse allows for leading whitespace so we | ||
// need to exclude that case here. | ||
throw ThrowHelper.GetFormatException(DataType.TimeSpan); | ||
} | ||
|
||
bool result = Utf8Parser.TryParse(source, out TimeSpan tmpValue, out int bytesConsumed, 'c'); | ||
|
||
// Note: Utf8Parser.TryParse will return true for invalid input so | ||
// long as it starts with an integer. Example: "2021-06-18" or | ||
// "1$$$$$$$$$$". We need to check bytesConsumed to know if the | ||
// entire source was actually valid. | ||
|
||
if (result && source.Length == bytesConsumed) | ||
{ | ||
return tmpValue; | ||
} | ||
|
||
throw ThrowHelper.GetFormatException(DataType.TimeSpan); | ||
} | ||
|
||
public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options) | ||
{ | ||
Span<byte> output = stackalloc byte[MaximumTimeSpanFormatLength]; | ||
|
||
bool result = Utf8Formatter.TryFormat(value, output, out int bytesWritten, 'c'); | ||
Debug.Assert(result); | ||
|
||
writer.WriteStringValue(output.Slice(0, bytesWritten)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Non-blocking for this PR] When #54254 goes in, we should consider updating this to use the new |
||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -189,7 +189,6 @@ public static IEnumerable<object[]> InvalidISO8601Tests() | |
yield return new object[] { "\"1997-07-16T19:20:30.4555555+1400\"" }; | ||
yield return new object[] { "\"1997-07-16T19:20:30.4555555-1400\"" }; | ||
|
||
|
||
// Proper format but invalid calendar date, time, or time zone designator fields | ||
yield return new object[] { "\"1997-00-16T19:20:30.4555555\"" }; | ||
yield return new object[] { "\"1997-07-16T25:20:30.4555555\"" }; | ||
|
@@ -215,6 +214,7 @@ public static IEnumerable<object[]> InvalidISO8601Tests() | |
yield return new object[] { "\"1997-07-16T19:20:30.45555555550000000\"" }; | ||
yield return new object[] { "\"1997-07-16T19:20:30.45555555555555555\"" }; | ||
yield return new object[] { "\"1997-07-16T19:20:30.45555555555555555555\"" }; | ||
yield return new object[] { "\"1997-07-16T19:20:30.4555555555555555+01:300\"" }; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we have |
||
|
||
// Hex strings | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why delete this helper?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It wasn't being used anymore so I cleaned it up. The code that was calling it (in Utf8JsonReader.TryGet) is now calling
IsInRangeInclusive
directly so it can pass in the max length based on encoded string or not. You want me to put it back with the other feedback items for a follow-up?