From 0c7707aee69db175f1116e4e1a8c2b9c16a3adba Mon Sep 17 00:00:00 2001 From: Tarek Mahmoud Sayed Date: Tue, 23 Mar 2021 20:29:57 +0000 Subject: [PATCH 1/5] Add more support for Julian time zones Posix rules --- .../src/System/TimeZoneInfo.Unix.cs | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs index 44c49e52de0b1b..07eb5b2aeff90d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -1296,8 +1296,7 @@ private static DateTime ParseTimeOfDay(ReadOnlySpan time) { if (date[0] != 'J') { - // should be n Julian day format which we don't support. - // + // should be n Julian day format. // This specifies the Julian day, with n between 0 and 365. February 29 is counted in leap years. // // n would be a relative number from the beginning of the year. which should handle if the @@ -1313,11 +1312,42 @@ private static DateTime ParseTimeOfDay(ReadOnlySpan time) // 0 30 31 58 59 89 334 364 // |-------Jan--------|-------Feb--------|-------Mar--------|....|-------Dec--------| // - // // For example if n is specified as 60, this means in leap year the rule will start at Mar 1, // while in non leap year the rule will start at Mar 2. // - // If we need to support n format, we'll have to have a floating adjustment rule support this case. + // This n Julian day format is very uncommon and rarely used and mostely used for convenience + // to specify dates like January 1st which we can support without any major modification to the Adjustment rules. + // We'll support this rule for day numbers less than 59 (up to Feb 28). Otherwise we'll + // skip this POSIX rule. So far we never encountered any time zone file used this format for days beyond Feb 28. + + if ((uint)(date[0] - '0') <= '9'-'0') + { + int julianDay = 0; + int index = 0; + + do + { + julianDay = julianDay * 10 + (int) (date[index] - '0'); + index++; + } while (index < date.Length && ((uint)(date[index] - '0') <= '9'-'0')); + + if (julianDay < 59) // Up to Feb 28. + { + int d, m; + if (julianDay <= 30) // January + { + m = 1; + d = julianDay + 1; + } + else // February + { + m = 2; + d = julianDay - 30; + } + + return TransitionTime.CreateFixedDateRule(ParseTimeOfDay(time), m, d); + } + } // Since we can't support this rule, return null to indicate to skip the POSIX rule. return null; From 801fa1d1189448efffb041c172e82a59fa73c8cb Mon Sep 17 00:00:00 2001 From: Tarek Mahmoud Sayed Date: Wed, 24 Mar 2021 17:56:19 +0000 Subject: [PATCH 2/5] Address the feedback --- .../src/System/TimeZoneInfo.Unix.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs index 07eb5b2aeff90d..bbf3e9528329b3 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -1315,21 +1315,21 @@ private static DateTime ParseTimeOfDay(ReadOnlySpan time) // For example if n is specified as 60, this means in leap year the rule will start at Mar 1, // while in non leap year the rule will start at Mar 2. // - // This n Julian day format is very uncommon and rarely used and mostely used for convenience - // to specify dates like January 1st which we can support without any major modification to the Adjustment rules. - // We'll support this rule for day numbers less than 59 (up to Feb 28). Otherwise we'll - // skip this POSIX rule. So far we never encountered any time zone file used this format for days beyond Feb 28. + // This n Julian day format is very uncommon and mostly used for convenience to specify dates like January 1st + // which we can support without any major modification to the Adjustment rules. We'll support this rule for day + // numbers less than 59 (up to Feb 28). Otherwise we'll skip this POSIX rule. + // We've never encountered any time zone file using this format for days beyond Feb 28. if ((uint)(date[0] - '0') <= '9'-'0') { - int julianDay = 0; - int index = 0; + int julianDay = (int) (date[0] - '0'); + int index = 1; - do + while (index < date.Length && julianDay < 59 && ((uint)(date[index] - '0') <= '9'-'0')) { julianDay = julianDay * 10 + (int) (date[index] - '0'); index++; - } while (index < date.Length && ((uint)(date[index] - '0') <= '9'-'0')); + }; if (julianDay < 59) // Up to Feb 28. { @@ -1360,7 +1360,7 @@ private static DateTime ParseTimeOfDay(ReadOnlySpan time) } /// - /// Parses a string like Jn or n into month and day values. + /// Parses a string like Jn into month and day values. /// private static void TZif_ParseJulianDay(ReadOnlySpan date, out int month, out int day) { From cab22477680527588d2992b29160df49fe146635 Mon Sep 17 00:00:00 2001 From: Tarek Mahmoud Sayed Date: Wed, 24 Mar 2021 19:27:16 +0000 Subject: [PATCH 3/5] Feedback --- .../src/System/TimeZoneInfo.Unix.cs | 34 ++++++------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs index bbf3e9528329b3..b7a2f984d7b28b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -1320,33 +1320,21 @@ private static DateTime ParseTimeOfDay(ReadOnlySpan time) // numbers less than 59 (up to Feb 28). Otherwise we'll skip this POSIX rule. // We've never encountered any time zone file using this format for days beyond Feb 28. - if ((uint)(date[0] - '0') <= '9'-'0') + if (int.TryParse(date, out int julianDay) && julianDay < 59) { - int julianDay = (int) (date[0] - '0'); - int index = 1; - - while (index < date.Length && julianDay < 59 && ((uint)(date[index] - '0') <= '9'-'0')) + int d, m; + if (julianDay <= 30) // January { - julianDay = julianDay * 10 + (int) (date[index] - '0'); - index++; - }; - - if (julianDay < 59) // Up to Feb 28. + m = 1; + d = julianDay + 1; + } + else // February { - int d, m; - if (julianDay <= 30) // January - { - m = 1; - d = julianDay + 1; - } - else // February - { - m = 2; - d = julianDay - 30; - } - - return TransitionTime.CreateFixedDateRule(ParseTimeOfDay(time), m, d); + m = 2; + d = julianDay - 30; } + + return TransitionTime.CreateFixedDateRule(ParseTimeOfDay(time), m, d); } // Since we can't support this rule, return null to indicate to skip the POSIX rule. From 7eb0f84440eeb9b840c8b8399f64658c1a18837d Mon Sep 17 00:00:00 2001 From: Tarek Mahmoud Sayed Date: Fri, 26 Mar 2021 01:26:50 +0000 Subject: [PATCH 4/5] Add test --- .../tests/System/TimeZoneInfoTests.cs | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs b/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs index 35861477d23883..ad2c7ab9b280ad 100644 --- a/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs +++ b/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; +using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.Serialization.Formatters.Binary; @@ -2353,6 +2354,74 @@ public static void GetSystemTimeZones_AllTimeZonesHaveOffsetInValidRange() } } + private static byte [] timeZoneFileContents = new byte[] + { + 0x54, 0x5A, 0x69, 0x66, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0x5A, 0x69, 0x66, + 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x0C, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xFF, 0xFF, 0xF8, 0xE4, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x10, 0x01, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x00, 0x0E, 0x10, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x4C, + 0x4D, 0x54, 0x00, 0x2B, 0x30, 0x31, 0x00, 0x2B, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + // POSIX Rule + // 0x0A, 0x3C, 0x2B, 0x30, 0x30, 0x3E, 0x30, 0x3C, 0x2B, 0x30, 0x31, + // 0x3E, 0x2C, 0x30, 0x2F, 0x30, 0x2C, 0x4A, 0x33, 0x36, 0x35, 0x2F, 0x32, 0x35, 0x0A + }; + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [PlatformSpecific(TestPlatforms.AnyUnix)] + [InlineData("<+00>0<+01>,0/0,J365/25", 1, 1, true)] + [InlineData("<+00>0<+01>,30/0,J365/25", 31, 1, true)] + [InlineData("<+00>0<+01>,31/0,J365/25", 1, 2, true)] + [InlineData("<+00>0<+01>,58/0,J365/25", 28, 2, true)] + [InlineData("<+00>0<+01>,59/0,J365/25", 0, 0, false)] + [InlineData("<+00>0<+01>,9999999/0,J365/25", 0, 0, false)] + [InlineData("<+00>0<+01>,A/0,J365/25", 0, 0, false)] + public static void NJulianRuleTest(string posixRule, int dayNumber, int monthNumber, bool shouldSucceed) + { + string zoneFilePath = Path.GetTempPath() + "dotnet_tz"; + using (FileStream fs = new FileStream(zoneFilePath, FileMode.Create)) + { + fs.Write(timeZoneFileContents.AsSpan()); + + // Append the POSIX rule + fs.WriteByte(0x0A); + foreach (char c in posixRule) + { + fs.WriteByte((byte) c); + } + fs.WriteByte(0x0A); + } + + ProcessStartInfo psi = new ProcessStartInfo() { UseShellExecute = false }; + psi.Environment.Add("TZ", zoneFilePath); + + RemoteExecutor.Invoke((day, month, succeed) => + { + bool expectedToSucceed = bool.Parse(succeed); + int d = int.Parse(day); + int m = int.Parse(month); + + TimeZoneInfo.AdjustmentRule [] rules = TimeZoneInfo.Local.GetAdjustmentRules(); + + if (expectedToSucceed) + { + Assert.Equal(1, rules.Length); + Assert.Equal(d, rules[0].DaylightTransitionStart.Day); + Assert.Equal(m, rules[0].DaylightTransitionStart.Month); + } + else + { + Assert.Equal(0, rules.Length); + } + }, dayNumber.ToString(), monthNumber.ToString(), shouldSucceed.ToString(), new RemoteInvokeOptions { StartInfo = psi}).Dispose(); + + File.Delete(zoneFilePath); + } + [Fact] public static void TimeZoneInfo_DaylightDeltaIsNoMoreThan12Hours() { From 716b0795e61a86339ad3d03b5e43b49123122ece Mon Sep 17 00:00:00 2001 From: Tarek Mahmoud Sayed Date: Fri, 26 Mar 2021 17:18:39 +0000 Subject: [PATCH 5/5] ensure the file deletion in the test --- .../tests/System/TimeZoneInfoTests.cs | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs b/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs index ad2c7ab9b280ad..47a8493ef22f4a 100644 --- a/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs +++ b/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs @@ -2396,30 +2396,35 @@ public static void NJulianRuleTest(string posixRule, int dayNumber, int monthNum fs.WriteByte(0x0A); } - ProcessStartInfo psi = new ProcessStartInfo() { UseShellExecute = false }; - psi.Environment.Add("TZ", zoneFilePath); - - RemoteExecutor.Invoke((day, month, succeed) => + try { - bool expectedToSucceed = bool.Parse(succeed); - int d = int.Parse(day); - int m = int.Parse(month); + ProcessStartInfo psi = new ProcessStartInfo() { UseShellExecute = false }; + psi.Environment.Add("TZ", zoneFilePath); - TimeZoneInfo.AdjustmentRule [] rules = TimeZoneInfo.Local.GetAdjustmentRules(); - - if (expectedToSucceed) - { - Assert.Equal(1, rules.Length); - Assert.Equal(d, rules[0].DaylightTransitionStart.Day); - Assert.Equal(m, rules[0].DaylightTransitionStart.Month); - } - else + RemoteExecutor.Invoke((day, month, succeed) => { - Assert.Equal(0, rules.Length); - } - }, dayNumber.ToString(), monthNumber.ToString(), shouldSucceed.ToString(), new RemoteInvokeOptions { StartInfo = psi}).Dispose(); + bool expectedToSucceed = bool.Parse(succeed); + int d = int.Parse(day); + int m = int.Parse(month); + + TimeZoneInfo.AdjustmentRule [] rules = TimeZoneInfo.Local.GetAdjustmentRules(); - File.Delete(zoneFilePath); + if (expectedToSucceed) + { + Assert.Equal(1, rules.Length); + Assert.Equal(d, rules[0].DaylightTransitionStart.Day); + Assert.Equal(m, rules[0].DaylightTransitionStart.Month); + } + else + { + Assert.Equal(0, rules.Length); + } + }, dayNumber.ToString(), monthNumber.ToString(), shouldSucceed.ToString(), new RemoteInvokeOptions { StartInfo = psi}).Dispose(); + } + finally + { + try { File.Delete(zoneFilePath); } catch { } // don't fail the test if we couldn't delete the file. + } } [Fact]