diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/TimeOnlyConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/TimeOnlyConverter.cs index 3e66dbb937b74..ac0e8ce37febe 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/TimeOnlyConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/TimeOnlyConverter.cs @@ -3,30 +3,72 @@ using System.Buffers.Text; using System.Diagnostics; -using System.Globalization; namespace System.Text.Json.Serialization.Converters { internal sealed class TimeOnlyConverter : JsonConverter { - private static readonly TimeSpanConverter s_timeSpanConverter = new TimeSpanConverter(); - private static readonly TimeSpan s_timeOnlyMaxValue = TimeOnly.MaxValue.ToTimeSpan(); + private const int MinimumTimeOnlyFormatLength = 8; // hh:mm:ss + private const int MaximumTimeOnlyFormatLength = 16; // hh:mm:ss.fffffff + private const int MaximumEscapedTimeOnlyFormatLength = JsonConstants.MaxExpansionFactorWhileEscaping * MaximumTimeOnlyFormatLength; public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - TimeSpan timespan = s_timeSpanConverter.Read(ref reader, typeToConvert, options); + if (reader.TokenType != JsonTokenType.String) + { + ThrowHelper.ThrowInvalidOperationException_ExpectedString(reader.TokenType); + } - if (timespan < TimeSpan.Zero || timespan > s_timeOnlyMaxValue) + if (!JsonHelpers.IsInRangeInclusive(reader.ValueLength, MinimumTimeOnlyFormatLength, MaximumEscapedTimeOnlyFormatLength)) { - ThrowHelper.ThrowJsonException(); + ThrowHelper.ThrowFormatException(DataType.TimeOnly); } + ReadOnlySpan source = stackalloc byte[0]; + if (!reader.HasValueSequence && !reader.ValueIsEscaped) + { + source = reader.ValueSpan; + } + else + { + Span stackSpan = stackalloc byte[MaximumEscapedTimeOnlyFormatLength]; + int bytesWritten = reader.CopyString(stackSpan); + source = stackSpan.Slice(0, bytesWritten); + } + + byte firstChar = source[0]; + int firstSeparator = source.IndexOfAny((byte)'.', (byte)':'); + if (!JsonHelpers.IsDigit(firstChar) || firstSeparator < 0 || source[firstSeparator] == (byte)'.') + { + // Note: Utf8Parser.TryParse permits leading whitespace, negative values + // and numbers of days so we need to exclude these cases here. + ThrowHelper.ThrowFormatException(DataType.TimeOnly); + } + + bool result = Utf8Parser.TryParse(source, out TimeSpan timespan, 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) + { + ThrowHelper.ThrowFormatException(DataType.TimeOnly); + } + + Debug.Assert(TimeOnly.MinValue.ToTimeSpan() <= timespan && timespan <= TimeOnly.MaxValue.ToTimeSpan()); return TimeOnly.FromTimeSpan(timespan); } public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options) { - s_timeSpanConverter.Write(writer, value.ToTimeSpan(), options); + Span output = stackalloc byte[MaximumTimeOnlyFormatLength]; + + bool result = Utf8Formatter.TryFormat(value.ToTimeSpan(), 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/ThrowHelper.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs index 0e384360934c1..e7f151dc9df2b 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 @@ -628,6 +628,7 @@ public static void ThrowFormatException(DataType dataType) case DataType.DateOnly: case DataType.DateTime: case DataType.DateTimeOffset: + case DataType.TimeOnly: case DataType.TimeSpan: case DataType.Guid: case DataType.Version: @@ -723,6 +724,7 @@ internal enum DataType DateOnly, DateTime, DateTimeOffset, + TimeOnly, TimeSpan, Base64String, Guid, 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 257d354ef2fad..031454bf99e8e 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 @@ -670,8 +670,11 @@ public static void TimeOnly_Read_Nullable_Tests() [InlineData("\\u0032\\u0034\\u003A\\u0030\\u0030\\u003A\\u0030\\u0030")] [InlineData("00:60:00")] [InlineData("00:00:60")] + [InlineData("-00:00:00")] [InlineData("00:00:00.00000009")] [InlineData("900000000.00:00:00")] + [InlineData("1.00:00:00")] + [InlineData("0.00:00:00")] [InlineData("1:00:00")] // 'g' Format [InlineData("1:2:00:00")] // 'g' Format [InlineData("+00:00:00")]