From 7f78562d4496d6881e3f60504473127656b98554 Mon Sep 17 00:00:00 2001 From: MaxKot Date: Sun, 15 May 2022 17:43:37 +0300 Subject: [PATCH] Add support to pre-epoch Unix time (#2614) Co-authored-by: Max A. Kiselev Co-authored-by: James Newton-King --- .../Converters/UnixDateTimeConverterTests.cs | 87 +++++++++++++++++++ .../Converters/UnixDateTimeConverter.cs | 35 +++++++- 2 files changed, 120 insertions(+), 2 deletions(-) diff --git a/Src/Newtonsoft.Json.Tests/Converters/UnixDateTimeConverterTests.cs b/Src/Newtonsoft.Json.Tests/Converters/UnixDateTimeConverterTests.cs index 638c7484..ed8ee1f9 100644 --- a/Src/Newtonsoft.Json.Tests/Converters/UnixDateTimeConverterTests.cs +++ b/Src/Newtonsoft.Json.Tests/Converters/UnixDateTimeConverterTests.cs @@ -25,6 +25,7 @@ using System; using System.Collections.Generic; +using System.Globalization; #if DNXCORE50 using Xunit; using Test = Xunit.FactAttribute; @@ -71,6 +72,17 @@ namespace Newtonsoft.Json.Tests.Converters ); } + [Test] + public void SerializeDateBeforeEpoch() + { + DateTime date = new DateTime(1964, 2, 7); + long dateSeconds = (long)(date.ToUniversalTime() - new DateTime(1970, 1, 1)).TotalSeconds; + + string result = JsonConvert.SerializeObject(date, new UnixDateTimeConverter { AllowPreEpoch = true }); + + Assert.AreEqual(dateSeconds.ToString(CultureInfo.InvariantCulture), result); + } + [Test] public void WriteJsonInvalidType() { @@ -159,6 +171,19 @@ namespace Newtonsoft.Json.Tests.Converters "Cannot convert invalid value to System.DateTimeOffset. Path '', line 1, position 5." ); } + + [Test] + public void DeserializeDateTimeOffsetBeforeEpoch() + { + UnixDateTimeConverter converter = new UnixDateTimeConverter(true); + DateTimeOffset d = new DateTimeOffset(1969, 2, 1, 20, 6, 18, TimeSpan.Zero); + + string json = JsonConvert.SerializeObject(d, converter); + + DateTimeOffset result = JsonConvert.DeserializeObject(json, converter); + + Assert.AreEqual(new DateTimeOffset(1969, 2, 1, 20, 6, 18, TimeSpan.Zero), result); + } #endif [Test] @@ -186,6 +211,14 @@ namespace Newtonsoft.Json.Tests.Converters ); } + [Test] + public void DeserializeNegativeIntegerToDateTimeBeforeEpoch() + { + DateTime result = JsonConvert.DeserializeObject("-1514840476", new UnixDateTimeConverter(true)); + + Assert.AreEqual(new DateTime(1921, 12, 31, 02, 58, 44, DateTimeKind.Utc), result); + } + [Test] public void DeserializeInvalidValueType() { @@ -263,6 +296,49 @@ namespace Newtonsoft.Json.Tests.Converters Assert.IsNull(obj2.Object2); Assert.AreEqual(new DateTime(2018, 1, 1, 21, 1, 16, DateTimeKind.Utc), obj2.ObjectNotHandled); } + +#if !NET20 + [Test] + public void ConverterObjectWithDatesBeforeEpoch() + { + PreEpochUnixConverterObject obj1 = new PreEpochUnixConverterObject + { + Date1 = new DateTime(1969, 1, 1, 0, 0, 3, DateTimeKind.Utc), + Date2 = new DateTimeOffset(1969, 1, 1, 0, 0, 3, TimeSpan.Zero) + }; + + string json = JsonConvert.SerializeObject(obj1, Formatting.Indented); + StringAssert.AreEqual(@"{ + ""Date1"": -31535997, + ""Date2"": -31535997 +}", json); + + PreEpochUnixConverterObject obj2 = JsonConvert.DeserializeObject(json); + Assert.IsNotNull(obj2); + + Assert.AreEqual(new DateTime(1969, 1, 1, 0, 0, 3, DateTimeKind.Utc), obj2.Date1); + Assert.AreEqual(new DateTimeOffset(1969, 1, 1, 0, 0, 3, TimeSpan.Zero), obj2.Date2); + } +#else + [Test] + public void ConverterObjectWithDatesBeforeEpoch() + { + PreEpochUnixConverterObject obj1 = new PreEpochUnixConverterObject + { + Date1 = new DateTime(1969, 1, 1, 0, 0, 3, DateTimeKind.Utc) + }; + + string json = JsonConvert.SerializeObject(obj1, Formatting.Indented); + StringAssert.AreEqual(@"{ + ""Date1"": -31535997 +}", json); + + PreEpochUnixConverterObject obj2 = JsonConvert.DeserializeObject(json); + Assert.IsNotNull(obj2); + + Assert.AreEqual(new DateTime(1969, 1, 1, 0, 0, 3, DateTimeKind.Utc), obj2.Date1); + } +#endif } [JsonArray(ItemConverterType = typeof(UnixDateTimeConverter))] @@ -281,4 +357,15 @@ namespace Newtonsoft.Json.Tests.Converters [JsonConverter(typeof(UnixDateTimeConverter))] public object ObjectNotHandled { get; set; } } + + public class PreEpochUnixConverterObject + { + [JsonConverter(typeof(UnixDateTimeConverter), true)] + public DateTime Date1 { get; set; } + +#if !NET20 + [JsonConverter (typeof(UnixDateTimeConverter), true)] + public DateTimeOffset Date2 { get; set; } +#endif + } } \ No newline at end of file diff --git a/Src/Newtonsoft.Json/Converters/UnixDateTimeConverter.cs b/Src/Newtonsoft.Json/Converters/UnixDateTimeConverter.cs index d80be6ed..a211813d 100644 --- a/Src/Newtonsoft.Json/Converters/UnixDateTimeConverter.cs +++ b/Src/Newtonsoft.Json/Converters/UnixDateTimeConverter.cs @@ -36,6 +36,37 @@ namespace Newtonsoft.Json.Converters { internal static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + /// + /// Gets or sets a value indicating whether the dates before Unix epoch + /// should converted to and from JSON. + /// + /// + /// true to allow converting dates before Unix epoch to and from JSON; + /// false to throw an exception when a date being converted to or from JSON + /// occurred before Unix epoch. The default value is false. + /// + public bool AllowPreEpoch { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public UnixDateTimeConverter() : this(false) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// true to allow converting dates before Unix epoch to and from JSON; + /// false to throw an exception when a date being converted to or from JSON + /// occurred before Unix epoch. The default value is false. + /// + public UnixDateTimeConverter(bool allowPreEpoch) + { + AllowPreEpoch = allowPreEpoch; + } + /// /// Writes the JSON representation of the object. /// @@ -61,7 +92,7 @@ namespace Newtonsoft.Json.Converters throw new JsonSerializationException("Expected date object value."); } - if (seconds < 0) + if (!AllowPreEpoch && seconds < 0) { throw new JsonSerializationException("Cannot convert date value that is before Unix epoch of 00:00:00 UTC on 1 January 1970."); } @@ -108,7 +139,7 @@ namespace Newtonsoft.Json.Converters throw JsonSerializationException.Create(reader, "Unexpected token parsing date. Expected Integer or String, got {0}.".FormatWith(CultureInfo.InvariantCulture, reader.TokenType)); } - if (seconds >= 0) + if (AllowPreEpoch || seconds >= 0) { DateTime d = UnixEpoch.AddSeconds(seconds);