From: Layomi Akinrinade Date: Tue, 16 Jul 2019 18:59:27 +0000 (-0400) Subject: Fix datetime bug (dotnet/corefx#39541) X-Git-Tag: submit/tizen/20210909.063632~11031^2~911 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=e855e92dcc3d32340ca9ac35ada74c6243284623;p=platform%2Fupstream%2Fdotnet%2Fruntime.git Fix datetime bug (dotnet/corefx#39541) Commit migrated from https://github.com/dotnet/corefx/commit/05c0f516cf20cd6ae108ba034984b0cdc740732e --- 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 8629c22..a49df5b 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 @@ -9,6 +9,21 @@ namespace System.Text.Json { internal static partial class JsonHelpers { + private struct DateTimeParseData + { + public int Year; + public int Month; + public int Day; + public int Hour; + public int Minute; + public int Second; + public int Fraction; // This value should never be greater than 9_999_999. + public int OffsetHours; + public int OffsetMinutes; + public bool OffsetNegative => OffsetToken == JsonConstants.Hyphen; + public byte OffsetToken; + } + /// /// Parse the given UTF-8 as extended ISO 8601 format. /// @@ -17,27 +32,31 @@ namespace System.Text.Json /// "true" if successfully parsed. public static bool TryParseAsISO(ReadOnlySpan source, out DateTime value) { - if (!TryParseDateTimeOffset(source, out DateTimeOffset dateTimeOffset, out DateTimeKind kind)) + DateTimeParseData parseData = new DateTimeParseData(); + + if (!TryParseDateTimeOffset(source, ref parseData)) { value = default; return false; } - switch (kind) + if (parseData.OffsetToken == JsonConstants.UtcOffsetToken) { - case DateTimeKind.Local: - value = dateTimeOffset.LocalDateTime; - break; - case DateTimeKind.Utc: - value = dateTimeOffset.UtcDateTime; - break; - default: - Debug.Assert(kind == DateTimeKind.Unspecified); - value = dateTimeOffset.DateTime; - break; + return TryCreateDateTime(ref parseData, DateTimeKind.Utc, out value); } + else if (parseData.OffsetToken == JsonConstants.Plus || parseData.OffsetToken == JsonConstants.Hyphen) + { + if (!TryCreateDateTimeOffset(ref parseData, out DateTimeOffset dateTimeOffset)) + { + value = default; + return false; + } - return true; + value = dateTimeOffset.LocalDateTime; + return true; + } + + return TryCreateDateTime(ref parseData, DateTimeKind.Unspecified, out value); } /// @@ -48,33 +67,29 @@ namespace System.Text.Json /// "true" if successfully parsed. public static bool TryParseAsISO(ReadOnlySpan source, out DateTimeOffset value) { - return TryParseDateTimeOffset(source, out value, out _); - } + DateTimeParseData parseData = new DateTimeParseData(); - private struct DateTimeParseData - { - public int Year; - public int Month; - public int Day; - public int Hour; - public int Minute; - public int Second; - public int Fraction; // This value should never be greater than 9_999_999. - public int OffsetHours; - public int OffsetMinutes; - public bool OffsetNegative => OffsetToken == JsonConstants.Hyphen; - public byte OffsetToken; + if (!TryParseDateTimeOffset(source, ref parseData)) + { + value = default; + return false; + } + + if (parseData.OffsetToken == JsonConstants.UtcOffsetToken || // Same as specifying an offset of "+00:00", except that DateTime's Kind gets set to UTC rather than Local + parseData.OffsetToken == JsonConstants.Plus || parseData.OffsetToken == JsonConstants.Hyphen) + { + return TryCreateDateTimeOffset(ref parseData, out value); + } + + // No offset, attempt to read as local time. + return TryCreateDateTimeOffsetInterpretingDataAsLocalTime(ref parseData, out value); } /// /// ISO 8601 date time parser (ISO 8601-1:2019). /// /// The date/time to parse in UTF-8 format. - /// The parsed for the given . - /// - /// The parsed for extracting the most relevant when - /// needed. - /// + /// The parsed for the given . /// /// Supports extended calendar date (5.2.2.1) and complete (5.4.2.1) calendar date/time of day /// representations with optional specification of seconds and fractional seconds. @@ -97,11 +112,8 @@ namespace System.Text.Json /// Spaces are not permitted. /// /// "true" if successfully parsed. - private static bool TryParseDateTimeOffset(ReadOnlySpan source, out DateTimeOffset value, out DateTimeKind kind) + private static bool TryParseDateTimeOffset(ReadOnlySpan source, ref DateTimeParseData parseData) { - value = default; - kind = default; - // Source does not have enough characters for YYYY-MM-DD if (source.Length < 10) { @@ -120,8 +132,6 @@ namespace System.Text.Json // just [year][“-”][month] (a) and just [year] (b), but we currently // don't permit it. - DateTimeParseData parseData = new DateTimeParseData(); - { uint digit1 = source[0] - (uint)'0'; uint digit2 = source[1] - (uint)'0'; @@ -150,7 +160,7 @@ namespace System.Text.Json if (source.Length == 10) { // Just a calendar date - return FinishParsing(ref parseData, out value, out kind); + return true; } // Parse the time of day @@ -207,7 +217,7 @@ namespace System.Text.Json Debug.Assert(source.Length >= 16); if (source.Length == 16) { - return FinishParsing(ref parseData, out value, out kind); + return true; } byte curByte = source[16]; @@ -218,13 +228,11 @@ namespace System.Text.Json { case JsonConstants.UtcOffsetToken: parseData.OffsetToken = JsonConstants.UtcOffsetToken; - return sourceIndex == source.Length - && FinishParsing(ref parseData, out value, out kind); + return sourceIndex == source.Length; case JsonConstants.Plus: case JsonConstants.Hyphen: parseData.OffsetToken = curByte; - return ParseOffset(ref parseData, source.Slice(sourceIndex)) - && FinishParsing(ref parseData, out value, out kind); + return ParseOffset(ref parseData, source.Slice(sourceIndex)); case JsonConstants.Colon: break; default: @@ -242,7 +250,7 @@ namespace System.Text.Json Debug.Assert(source.Length >= 19); if (source.Length == 19) { - return FinishParsing(ref parseData, out value, out kind); + return true; } curByte = source[19]; @@ -253,13 +261,11 @@ namespace System.Text.Json { case JsonConstants.UtcOffsetToken: parseData.OffsetToken = JsonConstants.UtcOffsetToken; - return sourceIndex == source.Length - && FinishParsing(ref parseData, out value, out kind); + return sourceIndex == source.Length; case JsonConstants.Plus: case JsonConstants.Hyphen: parseData.OffsetToken = curByte; - return ParseOffset(ref parseData, source.Slice(sourceIndex)) - && FinishParsing(ref parseData, out value, out kind); + return ParseOffset(ref parseData, source.Slice(sourceIndex)); case JsonConstants.Period: break; default: @@ -303,7 +309,7 @@ namespace System.Text.Json Debug.Assert(sourceIndex <= source.Length); if (sourceIndex == source.Length) { - return FinishParsing(ref parseData, out value, out kind); + return true; } curByte = source[sourceIndex++]; @@ -313,13 +319,12 @@ namespace System.Text.Json { case JsonConstants.UtcOffsetToken: parseData.OffsetToken = JsonConstants.UtcOffsetToken; - return sourceIndex == source.Length - && FinishParsing(ref parseData, out value, out kind); + return sourceIndex == source.Length; case JsonConstants.Plus: case JsonConstants.Hyphen: parseData.OffsetToken = curByte; return ParseOffset(ref parseData, source.Slice(sourceIndex)) - && FinishParsing(ref parseData, out value, out kind); + && true; default: return false; } @@ -351,44 +356,6 @@ namespace System.Text.Json return true; } - - static bool FinishParsing(ref DateTimeParseData parseData, out DateTimeOffset dateTimeOffset, out DateTimeKind dateTimeKind) - { - dateTimeKind = default; - - switch (parseData.OffsetToken) - { - case JsonConstants.UtcOffsetToken: - // Same as specifying an offset of "+00:00", except that DateTime's Kind gets set to UTC rather than Local - if (!TryCreateDateTimeOffset(ref parseData, out dateTimeOffset)) - { - return false; - } - - dateTimeKind = DateTimeKind.Utc; - break; - case JsonConstants.Plus: - case JsonConstants.Hyphen: - if (!TryCreateDateTimeOffset(ref parseData, out dateTimeOffset)) - { - return false; - } - - dateTimeKind = DateTimeKind.Local; - break; - default: - // No offset, attempt to read as local time. - if (!TryCreateDateTimeOffsetInterpretingDataAsLocalTime(ref parseData, out dateTimeOffset)) - { - return false; - } - - dateTimeKind = DateTimeKind.Unspecified; - break; - } - - return true; - } } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/libraries/System.Text.Json/tests/JsonDateTimeTestData.cs b/src/libraries/System.Text.Json/tests/JsonDateTimeTestData.cs index ff1ab8b..15e5b4d 100644 --- a/src/libraries/System.Text.Json/tests/JsonDateTimeTestData.cs +++ b/src/libraries/System.Text.Json/tests/JsonDateTimeTestData.cs @@ -19,9 +19,6 @@ namespace System.Text.Json.Tests yield return new object[] { "\"1997-07-16T19:20:30.45\"", "1997-07-16T19:20:30.45" }; yield return new object[] { "\"1997-07-16T19:20:30.4555555\"", "1997-07-16T19:20:30.4555555" }; - // Regression test for https://github.com/dotnet/corefx/issues/39067 - yield return new object[] { "\"0001-01-01T00:00:00\"", "0001-01-01T00:00:00" }; - // Test fractions. yield return new object[] { "\"1997-07-16T19:20:30.0\"", "1997-07-16T19:20:30" }; yield return new object[] { "\"1997-07-16T19:20:30.000\"", "1997-07-16T19:20:30" }; diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj index 40a4ad9..c60ca78 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj @@ -98,6 +98,7 @@ + diff --git a/src/libraries/System.Text.Json/tests/Utf8JsonReaderTests.TryGet.Date.cs b/src/libraries/System.Text.Json/tests/Utf8JsonReaderTests.TryGet.Date.cs new file mode 100644 index 0000000..b91d9e2 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Utf8JsonReaderTests.TryGet.Date.cs @@ -0,0 +1,206 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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.Globalization; +using Xunit; +using Newtonsoft.Json; + +namespace System.Text.Json.Tests +{ + public static partial class Utf8JsonReaderTests + { + [Theory] + [MemberData(nameof(JsonDateTimeTestData.ValidISO8601Tests), MemberType = typeof(JsonDateTimeTestData))] + public static void TestingStringsConversionToDateTime(string jsonString, string expectedString) + { + byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString); + + var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default); + while (json.Read()) + { + if (json.TokenType == JsonTokenType.String) + { + DateTime expected = DateTime.Parse(expectedString); + + Assert.True(json.TryGetDateTime(out DateTime actual)); + Assert.Equal(expected, actual); + + Assert.Equal(expected, json.GetDateTime()); + } + } + + Assert.Equal(dataUtf8.Length, json.BytesConsumed); + } + + [Theory] + [MemberData(nameof(JsonDateTimeTestData.ValidISO8601Tests), MemberType = typeof(JsonDateTimeTestData))] + public static void TestingStringsConversionToDateTimeOffset(string jsonString, string expectedString) + { + byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString); + + var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default); + while (json.Read()) + { + if (json.TokenType == JsonTokenType.String) + { + DateTimeOffset expected = DateTimeOffset.Parse(expectedString); + + Assert.True(json.TryGetDateTimeOffset(out DateTimeOffset actual)); + Assert.Equal(expected, actual); + + Assert.Equal(expected, json.GetDateTimeOffset()); + } + } + + Assert.Equal(dataUtf8.Length, json.BytesConsumed); + } + + [Theory] + [MemberData(nameof(JsonDateTimeTestData.ValidISO8601TestsWithUtcOffset), MemberType = typeof(JsonDateTimeTestData))] + public static void TestingStringsWithUTCOffsetToDateTime(string jsonString, string expectedString) + { + byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString); + + var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default); + while (json.Read()) + { + if (json.TokenType == JsonTokenType.String) + { + DateTime expected = DateTime.ParseExact(expectedString, "O", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); + + Assert.True(json.TryGetDateTime(out DateTime actual)); + Assert.Equal(expected, actual); + + Assert.Equal(expected, json.GetDateTime()); + } + } + + Assert.Equal(dataUtf8.Length, json.BytesConsumed); + } + + [Theory] + [MemberData(nameof(JsonDateTimeTestData.ValidISO8601TestsWithUtcOffset), MemberType = typeof(JsonDateTimeTestData))] + public static void TestingStringsWithUTCOffsetToDateTimeOffset(string jsonString, string expectedString) + { + byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString); + + var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default); + while (json.Read()) + { + if (json.TokenType == JsonTokenType.String) + { + DateTimeOffset expected = DateTimeOffset.ParseExact(expectedString, "O", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); + + Assert.True(json.TryGetDateTimeOffset(out DateTimeOffset actual)); + Assert.Equal(expected, actual); + + Assert.Equal(expected, json.GetDateTimeOffset()); + } + } + + Assert.Equal(dataUtf8.Length, json.BytesConsumed); + } + + [Theory] + [MemberData(nameof(JsonDateTimeTestData.InvalidISO8601Tests), MemberType = typeof(JsonDateTimeTestData))] + public static void TestingStringsInvalidConversionToDateTime(string jsonString) + { + byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString); + + var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default); + while (json.Read()) + { + Assert.False(json.TryGetDateTime(out DateTime actualDateTime)); + Assert.Equal(default, actualDateTime); + + try + { + DateTime value = json.GetDateTime(); + Assert.True(false, "Expected GetDateTime to throw FormatException due to invalid ISO 8601 input."); + } + catch (FormatException) + { } + } + } + + [Theory] + [MemberData(nameof(JsonDateTimeTestData.InvalidISO8601Tests), MemberType = typeof(JsonDateTimeTestData))] + public static void TestingStringsInvalidConversionToDateTimeOffset(string jsonString) + { + byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString); + + var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default); + while (json.Read()) + { + if (json.TokenType == JsonTokenType.String) + { + Assert.False(json.TryGetDateTimeOffset(out DateTimeOffset actualDateTime)); + Assert.Equal(default, actualDateTime); + + try + { + DateTimeOffset value = json.GetDateTimeOffset(); + Assert.True(false, "Expected GetDateTimeOffset to throw FormatException due to invalid ISO 8601 input."); + } + catch (FormatException) + { } + } + } + } + + [Fact] + public static void Regression39067_TestingDateTimeMinValue() + { + string jsonString = @"""0001-01-01T00:00:00"""; + string expectedString = "0001-01-01T00:00:00"; + byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString); + + var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default); + while (json.Read()) + { + if (json.TokenType == JsonTokenType.String) + { + DateTime expected = DateTime.Parse(expectedString); + + Assert.True(json.TryGetDateTime(out DateTime actual)); + Assert.Equal(expected, actual); + + Assert.Equal(expected, json.GetDateTime()); + } + } + + Assert.Equal(dataUtf8.Length, json.BytesConsumed); + + // Test upstream serializer. + Assert.Equal(DateTime.Parse(expectedString), JsonSerializer.Deserialize(jsonString)); + } + + [Fact] + public static void TestingDateTimeMaxValue() + { + string jsonString = @"""9999-12-31T23:59:59"""; + string expectedString = "9999-12-31T23:59:59"; + byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString); + + var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default); + while (json.Read()) + { + if (json.TokenType == JsonTokenType.String) + { + DateTime expected = DateTime.Parse(expectedString); + + Assert.True(json.TryGetDateTime(out DateTime actual)); + Assert.Equal(expected, actual); + + Assert.Equal(expected, json.GetDateTime()); + } + } + + Assert.Equal(dataUtf8.Length, json.BytesConsumed); + + // Test upstream serializer. + Assert.Equal(DateTime.Parse(expectedString), JsonSerializer.Deserialize(jsonString)); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/Utf8JsonReaderTests.TryGet.cs b/src/libraries/System.Text.Json/tests/Utf8JsonReaderTests.TryGet.cs index a6fb1ce..b8bd527 100644 --- a/src/libraries/System.Text.Json/tests/Utf8JsonReaderTests.TryGet.cs +++ b/src/libraries/System.Text.Json/tests/Utf8JsonReaderTests.TryGet.cs @@ -1350,145 +1350,6 @@ namespace System.Text.Json.Tests } [Theory] - [MemberData(nameof(JsonDateTimeTestData.ValidISO8601Tests), MemberType = typeof(JsonDateTimeTestData))] - public static void TestingStringsConversionToDateTime(string jsonString, string expectedString) - { - byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString); - - var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default); - while (json.Read()) - { - if (json.TokenType == JsonTokenType.String) - { - DateTime expected = DateTime.Parse(expectedString); - - Assert.True(json.TryGetDateTime(out DateTime actual)); - Assert.Equal(expected, actual); - - Assert.Equal(expected, json.GetDateTime()); - } - } - - Assert.Equal(dataUtf8.Length, json.BytesConsumed); - } - - [Theory] - [MemberData(nameof(JsonDateTimeTestData.ValidISO8601Tests), MemberType = typeof(JsonDateTimeTestData))] - public static void TestingStringsConversionToDateTimeOffset(string jsonString, string expectedString) - { - byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString); - - var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default); - while (json.Read()) - { - if (json.TokenType == JsonTokenType.String) - { - DateTimeOffset expected = DateTimeOffset.Parse(expectedString); - - Assert.True(json.TryGetDateTimeOffset(out DateTimeOffset actual)); - Assert.Equal(expected, actual); - - Assert.Equal(expected, json.GetDateTimeOffset()); - } - } - - Assert.Equal(dataUtf8.Length, json.BytesConsumed); - } - - [Theory] - [MemberData(nameof(JsonDateTimeTestData.ValidISO8601TestsWithUtcOffset), MemberType = typeof(JsonDateTimeTestData))] - public static void TestingStringsWithUTCOffsetToDateTime(string jsonString, string expectedString) - { - byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString); - - var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default); - while (json.Read()) - { - if (json.TokenType == JsonTokenType.String) - { - DateTime expected = DateTime.ParseExact(expectedString, "O", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); - - Assert.True(json.TryGetDateTime(out DateTime actual)); - Assert.Equal(expected, actual); - - Assert.Equal(expected, json.GetDateTime()); - } - } - - Assert.Equal(dataUtf8.Length, json.BytesConsumed); - } - - [Theory] - [MemberData(nameof(JsonDateTimeTestData.ValidISO8601TestsWithUtcOffset), MemberType = typeof(JsonDateTimeTestData))] - public static void TestingStringsWithUTCOffsetToDateTimeOffset(string jsonString, string expectedString) - { - byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString); - - var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default); - while (json.Read()) - { - if (json.TokenType == JsonTokenType.String) - { - DateTimeOffset expected = DateTimeOffset.ParseExact(expectedString, "O", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); - - Assert.True(json.TryGetDateTimeOffset(out DateTimeOffset actual)); - Assert.Equal(expected, actual); - - Assert.Equal(expected, json.GetDateTimeOffset()); - } - } - - Assert.Equal(dataUtf8.Length, json.BytesConsumed); - } - - [Theory] - [MemberData(nameof(JsonDateTimeTestData.InvalidISO8601Tests), MemberType = typeof(JsonDateTimeTestData))] - public static void TestingStringsInvalidConversionToDateTime(string jsonString) - { - byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString); - - var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default); - while (json.Read()) - { - Assert.False(json.TryGetDateTime(out DateTime actualDateTime)); - Assert.Equal(default, actualDateTime); - - try - { - DateTime value = json.GetDateTime(); - Assert.True(false, "Expected GetDateTime to throw FormatException due to invalid ISO 8601 input."); - } - catch (FormatException) - { } - } - } - - [Theory] - [MemberData(nameof(JsonDateTimeTestData.InvalidISO8601Tests), MemberType = typeof(JsonDateTimeTestData))] - public static void TestingStringsInvalidConversionToDateTimeOffset(string jsonString) - { - byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString); - - var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default); - while (json.Read()) - { - if (json.TokenType == JsonTokenType.String) - { - Assert.False(json.TryGetDateTimeOffset(out DateTimeOffset actualDateTime)); - Assert.Equal(default, actualDateTime); - - try - { - DateTimeOffset value = json.GetDateTimeOffset(); - Assert.True(false, "Expected GetDateTimeOffset to throw FormatException due to invalid ISO 8601 input."); - } - catch (FormatException) - { } - } - } - } - - [Theory] [MemberData(nameof(JsonGuidTestData.ValidGuidTests), MemberType = typeof(JsonGuidTestData))] [MemberData(nameof(JsonGuidTestData.ValidHexGuidTests), MemberType = typeof(JsonGuidTestData))] public static void TestingStringsConversionToGuid(string testString, string expectedString)