diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index f22feb4a517a7..1183395a2b433 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -900,6 +900,7 @@ public static partial class JsonMetadataServices public static System.Text.Json.Serialization.JsonConverter SByteConverter { get { throw null; } } public static System.Text.Json.Serialization.JsonConverter SingleConverter { get { throw null; } } public static System.Text.Json.Serialization.JsonConverter StringConverter { get { throw null; } } + public static System.Text.Json.Serialization.JsonConverter TimeSpanConverter { get { throw null; } } [System.CLSCompliantAttribute(false)] public static System.Text.Json.Serialization.JsonConverter UInt16Converter { get { throw null; } } [System.CLSCompliantAttribute(false)] diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index 9f73cb0ec2956..c650f6ebad8bb 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -318,6 +318,9 @@ The JSON value is not in a supported DateTimeOffset format. + + The JSON value is not in a supported TimeSpan format. + The JSON value is not in a supported Guid format. diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index 142d9f8c4fdcb..34f8e1361a41f 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -165,6 +165,7 @@ + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.Date.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.Date.cs index a4dc832223df1..7f05dfafc5173 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.Date.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.Date.cs @@ -117,12 +117,6 @@ public static bool IsValidDateTimeOffsetParseLength(int length) return IsInRangeInclusive(length, JsonConstants.MinimumDateTimeParseLength, JsonConstants.MaximumEscapedDateTimeOffsetParseLength); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsValidDateTimeOffsetParseLength(long length) - { - return IsInRangeInclusive(length, JsonConstants.MinimumDateTimeParseLength, JsonConstants.MaximumEscapedDateTimeOffsetParseLength); - } - /// /// Parse the given UTF-8 as extended ISO 8601 format. /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs index 32947d9f090ae..0af169f2e1df7 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs @@ -1073,25 +1073,25 @@ internal bool TryGetDateTimeCore(out DateTime value) { ReadOnlySpan 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 stackSpan = stackalloc byte[(int)sequenceLength]; - + Span 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; @@ -1141,25 +1141,25 @@ internal bool TryGetDateTimeOffsetCore(out DateTimeOffset value) { ReadOnlySpan 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 stackSpan = stackalloc byte[(int)sequenceLength]; - + Span 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 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 stackSpan = stackalloc byte[(int)sequenceLength]; - + Span 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; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/TimeSpanConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/TimeSpanConverter.cs new file mode 100644 index 0000000000000..3767b7ff6d451 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/TimeSpanConverter.cs @@ -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 + { + 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 source = stackalloc byte[0]; + + if (reader.HasValueSequence) + { + ReadOnlySequence valueSequence = reader.ValueSequence; + long sequenceLength = valueSequence.Length; + + if (!JsonHelpers.IsInRangeInclusive(sequenceLength, MinimumTimeSpanFormatLength, maximumLength)) + { + throw ThrowHelper.GetFormatException(DataType.TimeSpan); + } + + Span 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 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 output = stackalloc byte[MaximumTimeSpanFormatLength]; + + bool result = Utf8Formatter.TryFormat(value, output, out int bytesWritten, 'c'); + Debug.Assert(result); + + writer.WriteStringValue(output.Slice(0, bytesWritten)); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs index 95de3062e667f..a5270028def60 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs @@ -49,7 +49,7 @@ private void RootBuiltInConverters() private static Dictionary GetDefaultSimpleConverters() { - const int NumberOfSimpleConverters = 23; + const int NumberOfSimpleConverters = 24; var converters = new Dictionary(NumberOfSimpleConverters); // Use a dictionary for simple converters. @@ -72,6 +72,7 @@ private void RootBuiltInConverters() Add(JsonMetadataServices.SByteConverter); Add(JsonMetadataServices.SingleConverter); Add(JsonMetadataServices.StringConverter); + Add(JsonMetadataServices.TimeSpanConverter); Add(JsonMetadataServices.UInt16Converter); Add(JsonMetadataServices.UInt32Converter); Add(JsonMetadataServices.UInt64Converter); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Converters.cs index 0be5273425cdb..f51bc851a0a53 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Converters.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Converters.cs @@ -110,6 +110,12 @@ public static partial class JsonMetadataServices public static JsonConverter StringConverter => s_stringConverter ??= new StringConverter(); private static JsonConverter? s_stringConverter; + /// + /// Returns a instance that converts values. + /// + public static JsonConverter TimeSpanConverter => s_timeSpanConverter ??= new TimeSpanConverter(); + private static JsonConverter? s_timeSpanConverter; + /// /// Returns a instance that converts values. /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs index 2b6b85878c9ae..96296fa7b2fed 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs @@ -636,6 +636,9 @@ public static FormatException GetFormatException(DataType dateType) case DataType.DateTimeOffset: message = SR.FormatDateTimeOffset; break; + case DataType.TimeSpan: + message = SR.FormatTimeSpan; + break; case DataType.Base64String: message = SR.CannotDecodeInvalidBase64; break; @@ -723,6 +726,7 @@ internal enum DataType Boolean, DateTime, DateTimeOffset, + TimeSpan, Base64String, Guid, } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonDateTimeTestData.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonDateTimeTestData.cs index 799f2abd0b407..8d4efb5e3cb2a 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonDateTimeTestData.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonDateTimeTestData.cs @@ -189,7 +189,6 @@ public static IEnumerable 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 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\"" }; // Hex strings diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Value.ReadTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Value.ReadTests.cs index b4a8d0db76e21..75ad7d1e6bae4 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Value.ReadTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Value.ReadTests.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Runtime.CompilerServices; +using Newtonsoft.Json; using Xunit; namespace System.Text.Json.Serialization.Tests @@ -80,6 +81,7 @@ public static void ReadPrimitivesFail() Assert.Throws(() => JsonSerializer.Deserialize("\"abc\"")); Assert.Throws(() => JsonSerializer.Deserialize("\"abc\"")); + Assert.Throws(() => JsonSerializer.Deserialize("\"abc\"")); Assert.Throws(() => JsonSerializer.Deserialize("\"abc\"")); Assert.Throws(() => JsonSerializer.Deserialize("\"abc\"")); @@ -118,6 +120,7 @@ public static void ReadPrimitivesFail() [InlineData(typeof(sbyte))] [InlineData(typeof(float))] [InlineData(typeof(string))] + [InlineData(typeof(TimeSpan))] [InlineData(typeof(ushort))] [InlineData(typeof(uint))] [InlineData(typeof(ulong))] @@ -303,6 +306,9 @@ public static void ValueFail() Assert.Throws(() => JsonSerializer.Deserialize(unexpectedString)); Assert.Throws(() => JsonSerializer.Deserialize(unexpectedString)); + Assert.Throws(() => JsonSerializer.Deserialize(unexpectedString)); + Assert.Throws(() => JsonSerializer.Deserialize(unexpectedString)); + Assert.Throws(() => JsonSerializer.Deserialize("1")); Assert.Throws(() => JsonSerializer.Deserialize("1")); @@ -442,5 +448,70 @@ private static void DeserializeLongJsonString(int stringLength) string str = JsonSerializer.Deserialize(json); Assert.True(json.AsSpan(1, json.Length - 2).SequenceEqual(str.AsSpan())); } + + [Theory] + [InlineData("23:59:59")] + [InlineData("\\u002D23:59:59", "-23:59:59")] + [InlineData("\\u0032\\u0033\\u003A\\u0035\\u0039\\u003A\\u0035\\u0039", "23:59:59")] + [InlineData("23:59:59.9", "23:59:59.9000000")] + [InlineData("23:59:59.9999999")] + [InlineData("9999999.23:59:59.9999999")] + [InlineData("-9999999.23:59:59.9999999")] + [InlineData("10675199.02:48:05.4775807")] // TimeSpan.MaxValue + [InlineData("-10675199.02:48:05.4775808")] // TimeSpan.MinValue + public static void TimeSpan_Read_Success(string json, string? actual = null) + { + TimeSpan value = JsonSerializer.Deserialize($"\"{json}\""); + + Assert.Equal(TimeSpan.Parse(actual ?? json), value); + Assert.Equal(value, JsonConvert.DeserializeObject($"\"{json}\"")); + } + + [Fact] + public static void TimeSpan_Read_KnownDifferences() + { + string value = "24:00:00"; + + // 24:00:00 should be invalid because hours can only be up to 23. + Assert.Throws(() => JsonSerializer.Deserialize($"\"{value}\"")); + + TimeSpan expectedValue = TimeSpan.Parse("24.00:00:00"); + + // TimeSpan.Parse has a quirk where it treats 24:00:00 as 24.00:00:00. + Assert.Equal(expectedValue, TimeSpan.Parse(value)); + + // Newtonsoft uses TimeSpan.Parse so it is subject to the quirk. + Assert.Equal(expectedValue, JsonConvert.DeserializeObject($"\"{value}\"")); + } + + [Theory] + [InlineData("\t23:59:59")] // Otherwise valid but has invalid json character + [InlineData("\\t23:59:59")] // Otherwise valid but has leading whitespace + [InlineData("23:59:59 ")] // Otherwise valid but has trailing whitespace + [InlineData("24:00:00")] + [InlineData("\\u0032\\u0034\\u003A\\u0030\\u0030\\u003A\\u0030\\u0030")] + [InlineData("00:60:00")] + [InlineData("00:00:60")] + [InlineData("00:00:00.00000009")] + [InlineData("900000000.00:00:00")] + [InlineData("1:00:00")] // 'g' Format + [InlineData("1:2:00:00")] // 'g' Format + [InlineData("+00:00:00")] + [InlineData("2021-06-18")] + [InlineData("1$")] + [InlineData("10675199.02:48:05.4775808")] // TimeSpan.MaxValue + 1 + [InlineData("-10675199.02:48:05.4775809")] // TimeSpan.MinValue - 1 + [InlineData("1234", false)] + [InlineData("{}", false)] + [InlineData("[]", false)] + [InlineData("true", false)] + [InlineData("null", false)] + public static void TimeSpan_Read_Failure(string json, bool addQuotes = true) + { + if (addQuotes) + json = $"\"{json}\""; + + Assert.Throws(() => JsonSerializer.Deserialize(json)); + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Value.WriteTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Value.WriteTests.cs index 210cc6fbde566..f3e6bd8d3a447 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Value.WriteTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Value.WriteTests.cs @@ -112,5 +112,24 @@ public static void WritePrimitives() Assert.Equal(@"""1.2.3.4""", JsonSerializer.Serialize(version)); } } + + [Theory] + [InlineData("1:59:59", "01:59:59")] + [InlineData("23:59:59")] + [InlineData("23:59:59.9", "23:59:59.9000000")] + [InlineData("23:59:59.9999999")] + [InlineData("1.23:59:59")] + [InlineData("9999999.23:59:59.9999999")] + [InlineData("-9999999.23:59:59.9999999")] + [InlineData("10675199.02:48:05.4775807")] // TimeSpan.MaxValue + [InlineData("-10675199.02:48:05.4775808")] // TimeSpan.MinValue + public static void TimeSpan_Write_Success(string value, string? expectedValue = null) + { + TimeSpan ts = TimeSpan.Parse(value); + string json = JsonSerializer.Serialize(ts); + + Assert.Equal($"\"{expectedValue ?? value}\"", json); + Assert.Equal(json, JsonConvert.SerializeObject(ts)); + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonReaderTests.TryGet.Date.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonReaderTests.TryGet.Date.cs index 1df529baf9503..bd4cbdb864e77 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonReaderTests.TryGet.Date.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonReaderTests.TryGet.Date.cs @@ -1,6 +1,7 @@ // 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.Globalization; using Xunit; @@ -201,5 +202,55 @@ public static void TestingDateTimeMaxValue() // Test upstream serializer. Assert.Equal(DateTime.Parse(expectedString), JsonSerializer.Deserialize(jsonString)); } + + [Theory] + [MemberData(nameof(JsonDateTimeTestData.InvalidISO8601Tests), MemberType = typeof(JsonDateTimeTestData))] + public static void TryGetDateTime_HasValueSequence_False(string testString) + { + static void test(string testString, bool isFinalBlock) + { + byte[] dataUtf8 = Encoding.UTF8.GetBytes(testString); + ReadOnlySequence sequence = JsonTestHelper.GetSequence(dataUtf8, 1); + var json = new Utf8JsonReader(sequence, isFinalBlock: isFinalBlock, state: default); + + Assert.True(json.Read(), "json.Read()"); + Assert.Equal(JsonTokenType.String, json.TokenType); + Assert.True(json.HasValueSequence, "json.HasValueSequence"); + // If the string is empty, the ValueSequence is empty, because it contains all 0 bytes between the two characters + Assert.Equal(string.IsNullOrEmpty(testString), json.ValueSequence.IsEmpty); + Assert.False(json.TryGetDateTime(out DateTime actual), "json.TryGetDateTime(out DateTime actual)"); + Assert.Equal(DateTime.MinValue, actual); + + JsonTestHelper.AssertThrows(json, (jsonReader) => jsonReader.GetDateTime()); + } + + test(testString, isFinalBlock: true); + test(testString, isFinalBlock: false); + } + + [Theory] + [MemberData(nameof(JsonDateTimeTestData.InvalidISO8601Tests), MemberType = typeof(JsonDateTimeTestData))] + public static void TryGetDateTimeOffset_HasValueSequence_False(string testString) + { + static void test(string testString, bool isFinalBlock) + { + byte[] dataUtf8 = Encoding.UTF8.GetBytes(testString); + ReadOnlySequence sequence = JsonTestHelper.GetSequence(dataUtf8, 1); + var json = new Utf8JsonReader(sequence, isFinalBlock: isFinalBlock, state: default); + + Assert.True(json.Read(), "json.Read()"); + Assert.Equal(JsonTokenType.String, json.TokenType); + Assert.True(json.HasValueSequence, "json.HasValueSequence"); + // If the string is empty, the ValueSequence is empty, because it contains all 0 bytes between the two characters + Assert.Equal(string.IsNullOrEmpty(testString), json.ValueSequence.IsEmpty); + Assert.False(json.TryGetDateTimeOffset(out DateTimeOffset actual), "json.TryGetDateTimeOffset(out DateTimeOffset actual)"); + Assert.Equal(DateTimeOffset.MinValue, actual); + + JsonTestHelper.AssertThrows(json, (jsonReader) => jsonReader.GetDateTimeOffset()); + } + + test(testString, isFinalBlock: true); + test(testString, isFinalBlock: false); + } } }