From 1014cc6d4f7254fd4d1743b39cde440bea609e61 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Thu, 24 Jun 2021 15:27:52 -0400 Subject: [PATCH 01/81] Initial Android implementation --- .../System.Private.CoreLib.Shared.projitems | 3 +- .../src/System/TimeZoneInfo.Android.cs | 81 +++++++++++++++++++ .../src/System/TimeZoneInfo.cs | 1 - 3 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 1186b2c606f632..3e28674d8c35e4 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -2084,7 +2084,8 @@ - + + diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs new file mode 100644 index 00000000000000..685f7c20772f19 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System +{ + public sealed partial class TimeZoneInfo + { + // Mitchell - Why isn't this just instantiated in TimeZoneInfo.cs? + private static readonly TimeZoneInfo s_utcTimeZone = CreateUtcTimeZone(); + + /// + /// Returns a cloned array of AdjustmentRule objects + /// + public AdjustmentRule[] GetAdjustmentRules() + { + return Array.Empty(); + // TODO, called in tests, implemented in Unix/Win32 + } + + private static void PopulateAllSystemTimeZones(CachedData cachedData) + { + // TODO, called in TimeZoneInfo.cs, implemented in Unix/Win32 + } + + /// + /// Helper function for retrieving the local system time zone. + /// May throw COMException, TimeZoneNotFoundException, InvalidTimeZoneException. + /// Assumes cachedData lock is taken. + /// + /// A new TimeZoneInfo instance. + private static TimeZoneInfo GetLocalTimeZone(CachedData cachedData) + { + return Utc; + // TODO, called in TimeZoneInfo.cs, implemented in Unix/Win32 + } + + private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out TimeZoneInfo? value, out Exception? e) + { + value = null; + e = null; + return TimeZoneInfoResult.Success; + // TODO, called in TimeZoneInfo.cs, implemented in Unix/Win32 + } + + /// + /// Helper function for retrieving a TimeZoneInfo object by time_zone_name. + /// This function wraps the logic necessary to keep the private + /// SystemTimeZones cache in working order + /// + /// This function will either return a valid TimeZoneInfo instance or + /// it will throw 'InvalidTimeZoneException' / 'TimeZoneNotFoundException'. + /// + public static TimeZoneInfo FindSystemTimeZoneById(string id) + { + return Utc; + // TODO, called in TimeZoneInfo.cs, implemented in Unix/Win32 + } + + // DateTime.Now fast path that avoids allocating an historically accurate TimeZoneInfo.Local and just creates a 1-year (current year) accurate time zone + internal static TimeSpan GetDateTimeNowUtcOffsetFromUtc(DateTime time, out bool isAmbiguousLocalDst) + { + bool isDaylightSavings; + return GetUtcOffsetFromUtc(time, Local, out isDaylightSavings, out isAmbiguousLocalDst); + // TODO, called in DateTime.cs, implemented in Unix/Win32 + } + + // Helper function for string array search. (LINQ is not available here.) + private static bool StringArrayContains(string value, string[] source, StringComparison comparison) + { + foreach (string s in source) + { + if (string.Equals(s, value, comparison)) + { + return true; + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs index 89844b5c11c1b1..866cff7fa21594 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs @@ -51,7 +51,6 @@ private enum TimeZoneInfoResult // constants for TimeZoneInfo.Local and TimeZoneInfo.Utc private const string UtcId = "UTC"; - private const string LocalId = "Local"; private static CachedData s_cachedData = new CachedData(); From d1611056c7a84a72b38cae9f2595ae80f6019d3c Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 25 Jun 2021 11:03:42 -0400 Subject: [PATCH 02/81] Split implementations for Android specific GetTimeZoneIds --- .../System.Private.CoreLib.Shared.projitems | 1 + .../src/System/TimeZoneInfo.Android.cs | 73 +- .../src/System/TimeZoneInfo.AnyUnix.cs | 1753 +++++++++++++++++ .../src/System/TimeZoneInfo.Unix.cs | 1740 ---------------- .../src/System/TimeZoneInfo.cs | 1 + 5 files changed, 1760 insertions(+), 1808 deletions(-) create mode 100644 src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 3e28674d8c35e4..74d10709ee6deb 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -2084,6 +2084,7 @@ + diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 685f7c20772f19..9df2a6de1706f3 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -1,81 +1,18 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; + namespace System { public sealed partial class TimeZoneInfo { // Mitchell - Why isn't this just instantiated in TimeZoneInfo.cs? - private static readonly TimeZoneInfo s_utcTimeZone = CreateUtcTimeZone(); - - /// - /// Returns a cloned array of AdjustmentRule objects - /// - public AdjustmentRule[] GetAdjustmentRules() - { - return Array.Empty(); - // TODO, called in tests, implemented in Unix/Win32 - } - - private static void PopulateAllSystemTimeZones(CachedData cachedData) - { - // TODO, called in TimeZoneInfo.cs, implemented in Unix/Win32 - } - - /// - /// Helper function for retrieving the local system time zone. - /// May throw COMException, TimeZoneNotFoundException, InvalidTimeZoneException. - /// Assumes cachedData lock is taken. - /// - /// A new TimeZoneInfo instance. - private static TimeZoneInfo GetLocalTimeZone(CachedData cachedData) - { - return Utc; - // TODO, called in TimeZoneInfo.cs, implemented in Unix/Win32 - } + // private static readonly TimeZoneInfo s_utcTimeZone = CreateUtcTimeZone(); - private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out TimeZoneInfo? value, out Exception? e) + private static List GetTimeZoneIds(string timeZoneDirectory) { - value = null; - e = null; - return TimeZoneInfoResult.Success; - // TODO, called in TimeZoneInfo.cs, implemented in Unix/Win32 - } - - /// - /// Helper function for retrieving a TimeZoneInfo object by time_zone_name. - /// This function wraps the logic necessary to keep the private - /// SystemTimeZones cache in working order - /// - /// This function will either return a valid TimeZoneInfo instance or - /// it will throw 'InvalidTimeZoneException' / 'TimeZoneNotFoundException'. - /// - public static TimeZoneInfo FindSystemTimeZoneById(string id) - { - return Utc; - // TODO, called in TimeZoneInfo.cs, implemented in Unix/Win32 - } - - // DateTime.Now fast path that avoids allocating an historically accurate TimeZoneInfo.Local and just creates a 1-year (current year) accurate time zone - internal static TimeSpan GetDateTimeNowUtcOffsetFromUtc(DateTime time, out bool isAmbiguousLocalDst) - { - bool isDaylightSavings; - return GetUtcOffsetFromUtc(time, Local, out isDaylightSavings, out isAmbiguousLocalDst); - // TODO, called in DateTime.cs, implemented in Unix/Win32 - } - - // Helper function for string array search. (LINQ is not available here.) - private static bool StringArrayContains(string value, string[] source, StringComparison comparison) - { - foreach (string s in source) - { - if (string.Equals(s, value, comparison)) - { - return true; - } - } - - return false; + return new List(); } } } \ No newline at end of file diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs new file mode 100644 index 00000000000000..9d3388d3b4db1d --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs @@ -0,0 +1,1753 @@ +// 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.Binary; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Text; +using System.Threading; +using System.Security; + +namespace System +{ + public sealed partial class TimeZoneInfo + { + private const string DefaultTimeZoneDirectory = "/usr/share/zoneinfo/"; + private const string TimeZoneEnvironmentVariable = "TZ"; + private const string TimeZoneDirectoryEnvironmentVariable = "TZDIR"; + + // UTC aliases per https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml + // Hard-coded because we need to treat all aliases of UTC the same even when globalization data is not available. + // (This list is not likely to change.) + private static readonly string[] s_UtcAliases = new[] { + "Etc/UTC", + "Etc/UCT", + "Etc/Universal", + "Etc/Zulu", + "UCT", + "UTC", + "Universal", + "Zulu" + }; + + private static readonly TimeZoneInfo s_utcTimeZone = CreateUtcTimeZone(); + + private TimeZoneInfo(byte[] data, string id, bool dstDisabled) + { + _id = id; + + HasIanaId = true; + + // Handle UTC and its aliases + if (StringArrayContains(_id, s_UtcAliases, StringComparison.OrdinalIgnoreCase)) + { + _standardDisplayName = GetUtcStandardDisplayName(); + _daylightDisplayName = _standardDisplayName; + _displayName = GetUtcFullDisplayName(_id, _standardDisplayName); + _baseUtcOffset = TimeSpan.Zero; + _adjustmentRules = Array.Empty(); + return; + } + + TZifHead t; + DateTime[] dts; + byte[] typeOfLocalTime; + TZifType[] transitionType; + string zoneAbbreviations; + bool[] StandardTime; + bool[] GmtTime; + string? futureTransitionsPosixFormat; + string? standardAbbrevName = null; + string? daylightAbbrevName = null; + + // parse the raw TZif bytes; this method can throw ArgumentException when the data is malformed. + TZif_ParseRaw(data, out t, out dts, out typeOfLocalTime, out transitionType, out zoneAbbreviations, out StandardTime, out GmtTime, out futureTransitionsPosixFormat); + + // find the best matching baseUtcOffset and display strings based on the current utcNow value. + // NOTE: read the Standard and Daylight display strings from the tzfile now in case they can't be loaded later + // from the globalization data. + DateTime utcNow = DateTime.UtcNow; + for (int i = 0; i < dts.Length && dts[i] <= utcNow; i++) + { + int type = typeOfLocalTime[i]; + if (!transitionType[type].IsDst) + { + _baseUtcOffset = transitionType[type].UtcOffset; + standardAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[type].AbbreviationIndex); + } + else + { + daylightAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[type].AbbreviationIndex); + } + } + + if (dts.Length == 0) + { + // time zones like Africa/Bujumbura and Etc/GMT* have no transition times but still contain + // TZifType entries that may contain a baseUtcOffset and display strings + for (int i = 0; i < transitionType.Length; i++) + { + if (!transitionType[i].IsDst) + { + _baseUtcOffset = transitionType[i].UtcOffset; + standardAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[i].AbbreviationIndex); + } + else + { + daylightAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[i].AbbreviationIndex); + } + } + } + + // Set fallback values using abbreviations, base offset, and id + // These are expected in environments without time zone globalization data + _standardDisplayName = standardAbbrevName; + _daylightDisplayName = daylightAbbrevName ?? standardAbbrevName; + _displayName = $"(UTC{(_baseUtcOffset >= TimeSpan.Zero ? '+' : '-')}{_baseUtcOffset:hh\\:mm}) {_id}"; + + // Try to populate the display names from the globalization data + TryPopulateTimeZoneDisplayNamesFromGlobalizationData(_id, _baseUtcOffset, ref _standardDisplayName, ref _daylightDisplayName, ref _displayName); + + // TZif supports seconds-level granularity with offsets but TimeZoneInfo only supports minutes since it aligns + // with DateTimeOffset, SQL Server, and the W3C XML Specification + if (_baseUtcOffset.Ticks % TimeSpan.TicksPerMinute != 0) + { + _baseUtcOffset = new TimeSpan(_baseUtcOffset.Hours, _baseUtcOffset.Minutes, 0); + } + + if (!dstDisabled) + { + // only create the adjustment rule if DST is enabled + TZif_GenerateAdjustmentRules(out _adjustmentRules, _baseUtcOffset, dts, typeOfLocalTime, transitionType, StandardTime, GmtTime, futureTransitionsPosixFormat); + } + + ValidateTimeZoneInfo(_id, _baseUtcOffset, _adjustmentRules, out _supportsDaylightSavingTime); + } + + // The TransitionTime fields are not used when AdjustmentRule.NoDaylightTransitions == true. + // However, there are some cases in the past where DST = true, and the daylight savings offset + // now equals what the current BaseUtcOffset is. In that case, the AdjustmentRule.DaylightOffset + // is going to be TimeSpan.Zero. But we still need to return 'true' from AdjustmentRule.HasDaylightSaving. + // To ensure we always return true from HasDaylightSaving, make a "special" dstStart that will make the logic + // in HasDaylightSaving return true. + private static readonly TransitionTime s_daylightRuleMarker = TransitionTime.CreateFixedDateRule(DateTime.MinValue.AddMilliseconds(2), 1, 1); + + // Truncate the date and the time to Milliseconds precision + private static DateTime GetTimeOnlyInMillisecondsPrecision(DateTime input) => new DateTime((input.TimeOfDay.Ticks / TimeSpan.TicksPerMillisecond) * TimeSpan.TicksPerMillisecond); + + /// + /// Returns a cloned array of AdjustmentRule objects + /// + public AdjustmentRule[] GetAdjustmentRules() + { + if (_adjustmentRules == null) + { + return Array.Empty(); + } + + // The rules we use in Unix care mostly about the start and end dates but don't fill the transition start and end info. + // as the rules now is public, we should fill it properly so the caller doesn't have to know how we use it internally + // and can use it as it is used in Windows + + List rulesList = new List(_adjustmentRules.Length); + + for (int i = 0; i < _adjustmentRules.Length; i++) + { + AdjustmentRule rule = _adjustmentRules[i]; + + if (rule.NoDaylightTransitions && + rule.DaylightTransitionStart != s_daylightRuleMarker && + rule.DaylightDelta == TimeSpan.Zero && rule.BaseUtcOffsetDelta == TimeSpan.Zero) + { + // This rule has no time transition, ignore it. + continue; + } + + DateTime start = rule.DateStart.Kind == DateTimeKind.Utc ? + // At the daylight start we didn't start the daylight saving yet then we convert to Local time + // by adding the _baseUtcOffset to the UTC time + new DateTime(rule.DateStart.Ticks + _baseUtcOffset.Ticks, DateTimeKind.Unspecified) : + rule.DateStart; + DateTime end = rule.DateEnd.Kind == DateTimeKind.Utc ? + // At the daylight saving end, the UTC time is mapped to local time which is already shifted by the daylight delta + // we calculate the local time by adding _baseUtcOffset + DaylightDelta to the UTC time + new DateTime(rule.DateEnd.Ticks + _baseUtcOffset.Ticks + rule.DaylightDelta.Ticks, DateTimeKind.Unspecified) : + rule.DateEnd; + + if (start.Year == end.Year || !rule.NoDaylightTransitions) + { + // If the rule is covering only one year then the start and end transitions would occur in that year, we don't need to split the rule. + // Also, rule.NoDaylightTransitions be false in case the rule was created from a POSIX time zone string and having a DST transition. We can represent this in one rule too + TransitionTime startTransition = rule.NoDaylightTransitions ? TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(start), start.Month, start.Day) : rule.DaylightTransitionStart; + TransitionTime endTransition = rule.NoDaylightTransitions ? TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(end), end.Month, end.Day) : rule.DaylightTransitionEnd; + rulesList.Add(AdjustmentRule.CreateAdjustmentRule(start.Date, end.Date, rule.DaylightDelta, startTransition, endTransition, rule.BaseUtcOffsetDelta)); + } + else + { + // For rules spanning more than one year. The time transition inside this rule would apply for the whole time spanning these years + // and not for partial time of every year. + // AdjustmentRule cannot express such rule using the DaylightTransitionStart and DaylightTransitionEnd because + // the DaylightTransitionStart and DaylightTransitionEnd express the transition for every year. + // We split the rule into more rules. The first rule will start from the start year of the original rule and ends at the end of the same year. + // The second splitted rule would cover the middle range of the original rule and ranging from the year start+1 to + // year end-1. The transition time in this rule would start from Jan 1st to end of December. + // The last splitted rule would start from the Jan 1st of the end year of the original rule and ends at the end transition time of the original rule. + + // Add the first rule. + DateTime endForFirstRule = new DateTime(start.Year + 1, 1, 1).AddMilliseconds(-1); // At the end of the first year + TransitionTime startTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(start), start.Month, start.Day); + TransitionTime endTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(endForFirstRule), endForFirstRule.Month, endForFirstRule.Day); + rulesList.Add(AdjustmentRule.CreateAdjustmentRule(start.Date, endForFirstRule.Date, rule.DaylightDelta, startTransition, endTransition, rule.BaseUtcOffsetDelta)); + + // Check if there is range of years between the start and the end years + if (end.Year - start.Year > 1) + { + // Add the middle rule. + DateTime middleYearStart = new DateTime(start.Year + 1, 1, 1); + DateTime middleYearEnd = new DateTime(end.Year, 1, 1).AddMilliseconds(-1); + startTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(middleYearStart), middleYearStart.Month, middleYearStart.Day); + endTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(middleYearEnd), middleYearEnd.Month, middleYearEnd.Day); + rulesList.Add(AdjustmentRule.CreateAdjustmentRule(middleYearStart.Date, middleYearEnd.Date, rule.DaylightDelta, startTransition, endTransition, rule.BaseUtcOffsetDelta)); + } + + // Add the end rule. + DateTime endYearStart = new DateTime(end.Year, 1, 1); // At the beginning of the last year + startTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(endYearStart), endYearStart.Month, endYearStart.Day); + endTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(end), end.Month, end.Day); + rulesList.Add(AdjustmentRule.CreateAdjustmentRule(endYearStart.Date, end.Date, rule.DaylightDelta, startTransition, endTransition, rule.BaseUtcOffsetDelta)); + } + } + + return rulesList.ToArray(); + } + + private static void PopulateAllSystemTimeZones(CachedData cachedData) + { + Debug.Assert(Monitor.IsEntered(cachedData)); + + string timeZoneDirectory = GetTimeZoneDirectory(); + foreach (string timeZoneId in GetTimeZoneIds(timeZoneDirectory)) + { + TryGetTimeZone(timeZoneId, false, out _, out _, cachedData, alwaysFallbackToLocalMachine: true); // populate the cache + } + } + + /// + /// Helper function for retrieving the local system time zone. + /// May throw COMException, TimeZoneNotFoundException, InvalidTimeZoneException. + /// Assumes cachedData lock is taken. + /// + /// A new TimeZoneInfo instance. + private static TimeZoneInfo GetLocalTimeZone(CachedData cachedData) + { + Debug.Assert(Monitor.IsEntered(cachedData)); + + // Without Registry support, create the TimeZoneInfo from a TZ file + return GetLocalTimeZoneFromTzFile(); + } + + private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out TimeZoneInfo? value, out Exception? e) + { + value = null; + e = null; + + string timeZoneDirectory = GetTimeZoneDirectory(); + string timeZoneFilePath = Path.Combine(timeZoneDirectory, id); + byte[] rawData; + try + { + rawData = File.ReadAllBytes(timeZoneFilePath); + } + catch (UnauthorizedAccessException ex) + { + e = ex; + return TimeZoneInfoResult.SecurityException; + } + catch (FileNotFoundException ex) + { + e = ex; + return TimeZoneInfoResult.TimeZoneNotFoundException; + } + catch (DirectoryNotFoundException ex) + { + e = ex; + return TimeZoneInfoResult.TimeZoneNotFoundException; + } + catch (IOException ex) + { + e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, timeZoneFilePath), ex); + return TimeZoneInfoResult.InvalidTimeZoneException; + } + + value = GetTimeZoneFromTzData(rawData, id); + + if (value == null) + { + e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, timeZoneFilePath)); + return TimeZoneInfoResult.InvalidTimeZoneException; + } + + return TimeZoneInfoResult.Success; + } + + /// + /// Gets the tzfile raw data for the current 'local' time zone using the following rules. + /// 1. Read the TZ environment variable. If it is set, use it. + /// 2. Look for the data in /etc/localtime. + /// 3. Look for the data in GetTimeZoneDirectory()/localtime. + /// 4. Use UTC if all else fails. + /// + private static bool TryGetLocalTzFile([NotNullWhen(true)] out byte[]? rawData, [NotNullWhen(true)] out string? id) + { + rawData = null; + id = null; + string? tzVariable = GetTzEnvironmentVariable(); + + // If the env var is null, use the localtime file + if (tzVariable == null) + { + return + TryLoadTzFile("/etc/localtime", ref rawData, ref id) || + TryLoadTzFile(Path.Combine(GetTimeZoneDirectory(), "localtime"), ref rawData, ref id); + } + + // If it's empty, use UTC (TryGetLocalTzFile() should return false). + if (tzVariable.Length == 0) + { + return false; + } + + // Otherwise, use the path from the env var. If it's not absolute, make it relative + // to the system timezone directory + string tzFilePath; + if (tzVariable[0] != '/') + { + id = tzVariable; + tzFilePath = Path.Combine(GetTimeZoneDirectory(), tzVariable); + } + else + { + tzFilePath = tzVariable; + } + return TryLoadTzFile(tzFilePath, ref rawData, ref id); + } + + private static string? GetTzEnvironmentVariable() + { + string? result = Environment.GetEnvironmentVariable(TimeZoneEnvironmentVariable); + if (!string.IsNullOrEmpty(result)) + { + if (result[0] == ':') + { + // strip off the ':' prefix + result = result.Substring(1); + } + } + + return result; + } + + private static bool TryLoadTzFile(string tzFilePath, [NotNullWhen(true)] ref byte[]? rawData, [NotNullWhen(true)] ref string? id) + { + if (File.Exists(tzFilePath)) + { + try + { + rawData = File.ReadAllBytes(tzFilePath); + if (string.IsNullOrEmpty(id)) + { + id = FindTimeZoneIdUsingReadLink(tzFilePath); + + if (string.IsNullOrEmpty(id)) + { + id = FindTimeZoneId(rawData); + } + } + return true; + } + catch (IOException) { } + catch (SecurityException) { } + catch (UnauthorizedAccessException) { } + } + return false; + } + + /// + /// Finds the time zone id by using 'readlink' on the path to see if tzFilePath is + /// a symlink to a file. + /// + private static string? FindTimeZoneIdUsingReadLink(string tzFilePath) + { + string? id = null; + + string? symlinkPath = Interop.Sys.ReadLink(tzFilePath); + if (symlinkPath != null) + { + // symlinkPath can be relative path, use Path to get the full absolute path. + symlinkPath = Path.GetFullPath(symlinkPath, Path.GetDirectoryName(tzFilePath)!); + + string timeZoneDirectory = GetTimeZoneDirectory(); + if (symlinkPath.StartsWith(timeZoneDirectory, StringComparison.Ordinal)) + { + id = symlinkPath.Substring(timeZoneDirectory.Length); + } + } + + return id; + } + + private static string? GetDirectoryEntryFullPath(ref Interop.Sys.DirectoryEntry dirent, string currentPath) + { + ReadOnlySpan direntName = dirent.GetName(stackalloc char[Interop.Sys.DirectoryEntry.NameBufferSize]); + + if ((direntName.Length == 1 && direntName[0] == '.') || + (direntName.Length == 2 && direntName[0] == '.' && direntName[1] == '.')) + return null; + + return Path.Join(currentPath.AsSpan(), direntName); + } + + /// + /// Enumerate files + /// + private static unsafe void EnumerateFilesRecursively(string path, Predicate condition) + { + List? toExplore = null; // List used as a stack + + int bufferSize = Interop.Sys.GetReadDirRBufferSize(); + byte[]? dirBuffer = null; + try + { + dirBuffer = ArrayPool.Shared.Rent(bufferSize); + string currentPath = path; + + fixed (byte* dirBufferPtr = dirBuffer) + { + while (true) + { + IntPtr dirHandle = Interop.Sys.OpenDir(currentPath); + if (dirHandle == IntPtr.Zero) + { + throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo(), currentPath, isDirectory: true); + } + + try + { + // Read each entry from the enumerator + Interop.Sys.DirectoryEntry dirent; + while (Interop.Sys.ReadDirR(dirHandle, dirBufferPtr, bufferSize, out dirent) == 0) + { + string? fullPath = GetDirectoryEntryFullPath(ref dirent, currentPath); + if (fullPath == null) + continue; + + // Get from the dir entry whether the entry is a file or directory. + // We classify everything as a file unless we know it to be a directory. + bool isDir; + if (dirent.InodeType == Interop.Sys.NodeType.DT_DIR) + { + // We know it's a directory. + isDir = true; + } + else if (dirent.InodeType == Interop.Sys.NodeType.DT_LNK || dirent.InodeType == Interop.Sys.NodeType.DT_UNKNOWN) + { + // It's a symlink or unknown: stat to it to see if we can resolve it to a directory. + // If we can't (e.g. symlink to a file, broken symlink, etc.), we'll just treat it as a file. + + Interop.Sys.FileStatus fileinfo; + if (Interop.Sys.Stat(fullPath, out fileinfo) >= 0) + { + isDir = (fileinfo.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR; + } + else + { + isDir = false; + } + } + else + { + // Otherwise, treat it as a file. This includes regular files, FIFOs, etc. + isDir = false; + } + + // Yield the result if the user has asked for it. In the case of directories, + // always explore it by pushing it onto the stack, regardless of whether + // we're returning directories. + if (isDir) + { + toExplore ??= new List(); + toExplore.Add(fullPath); + } + else if (condition(fullPath)) + { + return; + } + } + } + finally + { + if (dirHandle != IntPtr.Zero) + Interop.Sys.CloseDir(dirHandle); + } + + if (toExplore == null || toExplore.Count == 0) + break; + + currentPath = toExplore[toExplore.Count - 1]; + toExplore.RemoveAt(toExplore.Count - 1); + } + } + } + finally + { + if (dirBuffer != null) + ArrayPool.Shared.Return(dirBuffer); + } + } + + /// + /// Find the time zone id by searching all the tzfiles for the one that matches rawData + /// and return its file name. + /// + private static string FindTimeZoneId(byte[] rawData) + { + // default to "Local" if we can't find the right tzfile + string id = LocalId; + string timeZoneDirectory = GetTimeZoneDirectory(); + string localtimeFilePath = Path.Combine(timeZoneDirectory, "localtime"); + string posixrulesFilePath = Path.Combine(timeZoneDirectory, "posixrules"); + byte[] buffer = new byte[rawData.Length]; + + try + { + EnumerateFilesRecursively(timeZoneDirectory, (string filePath) => + { + // skip the localtime and posixrules file, since they won't give us the correct id + if (!string.Equals(filePath, localtimeFilePath, StringComparison.OrdinalIgnoreCase) + && !string.Equals(filePath, posixrulesFilePath, StringComparison.OrdinalIgnoreCase)) + { + if (CompareTimeZoneFile(filePath, buffer, rawData)) + { + // if all bytes are the same, this must be the right tz file + id = filePath; + + // strip off the root time zone directory + if (id.StartsWith(timeZoneDirectory, StringComparison.Ordinal)) + { + id = id.Substring(timeZoneDirectory.Length); + } + return true; + } + } + return false; + }); + } + catch (IOException) { } + catch (SecurityException) { } + catch (UnauthorizedAccessException) { } + + return id; + } + + private static bool CompareTimeZoneFile(string filePath, byte[] buffer, byte[] rawData) + { + try + { + // bufferSize == 1 used to avoid unnecessary buffer in FileStream + using (FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1)) + { + if (stream.Length == rawData.Length) + { + int index = 0; + int count = rawData.Length; + + while (count > 0) + { + int n = stream.Read(buffer, index, count); + if (n == 0) + ThrowHelper.ThrowEndOfFileException(); + + int end = index + n; + for (; index < end; index++) + { + if (buffer[index] != rawData[index]) + { + return false; + } + } + + count -= n; + } + + return true; + } + } + } + catch (IOException) { } + catch (SecurityException) { } + catch (UnauthorizedAccessException) { } + + return false; + } + + /// + /// Helper function used by 'GetLocalTimeZone()' - this function wraps the call + /// for loading time zone data from computers without Registry support. + /// + /// The TryGetLocalTzFile() call returns a Byte[] containing the compiled tzfile. + /// + private static TimeZoneInfo GetLocalTimeZoneFromTzFile() + { + byte[]? rawData; + string? id; + if (TryGetLocalTzFile(out rawData, out id)) + { + TimeZoneInfo? result = GetTimeZoneFromTzData(rawData, id); + if (result != null) + { + return result; + } + } + + // if we can't find a local time zone, return UTC + return Utc; + } + + private static TimeZoneInfo? GetTimeZoneFromTzData(byte[]? rawData, string id) + { + if (rawData != null) + { + try + { + return new TimeZoneInfo(rawData, id, dstDisabled: false); // create a TimeZoneInfo instance from the TZif data w/ DST support + } + catch (ArgumentException) { } + catch (InvalidTimeZoneException) { } + + try + { + return new TimeZoneInfo(rawData, id, dstDisabled: true); // create a TimeZoneInfo instance from the TZif data w/o DST support + } + catch (ArgumentException) { } + catch (InvalidTimeZoneException) { } + } + return null; + } + + private static string GetTimeZoneDirectory() + { + string? tzDirectory = Environment.GetEnvironmentVariable(TimeZoneDirectoryEnvironmentVariable); + + if (tzDirectory == null) + { + tzDirectory = DefaultTimeZoneDirectory; + } + else if (!tzDirectory.EndsWith(Path.DirectorySeparatorChar)) + { + tzDirectory += PathInternal.DirectorySeparatorCharAsString; + } + + return tzDirectory; + } + + /// + /// Helper function for retrieving a TimeZoneInfo object by time_zone_name. + /// This function wraps the logic necessary to keep the private + /// SystemTimeZones cache in working order + /// + /// This function will either return a valid TimeZoneInfo instance or + /// it will throw 'InvalidTimeZoneException' / 'TimeZoneNotFoundException'. + /// + public static TimeZoneInfo FindSystemTimeZoneById(string id) + { + // Special case for Utc as it will not exist in the dictionary with the rest + // of the system time zones. There is no need to do this check for Local.Id + // since Local is a real time zone that exists in the dictionary cache + if (string.Equals(id, UtcId, StringComparison.OrdinalIgnoreCase)) + { + return Utc; + } + + if (id == null) + { + throw new ArgumentNullException(nameof(id)); + } + else if (id.Length == 0 || id.Contains('\0')) + { + throw new TimeZoneNotFoundException(SR.Format(SR.TimeZoneNotFound_MissingData, id)); + } + + TimeZoneInfo? value; + Exception? e; + + TimeZoneInfoResult result; + + CachedData cachedData = s_cachedData; + + lock (cachedData) + { + result = TryGetTimeZone(id, false, out value, out e, cachedData, alwaysFallbackToLocalMachine: true); + } + + if (result == TimeZoneInfoResult.Success) + { + return value!; + } + else if (result == TimeZoneInfoResult.InvalidTimeZoneException) + { + Debug.Assert(e is InvalidTimeZoneException, + "TryGetTimeZone must create an InvalidTimeZoneException when it returns TimeZoneInfoResult.InvalidTimeZoneException"); + throw e; + } + else if (result == TimeZoneInfoResult.SecurityException) + { + throw new SecurityException(SR.Format(SR.Security_CannotReadFileData, id), e); + } + else + { + throw new TimeZoneNotFoundException(SR.Format(SR.TimeZoneNotFound_MissingData, id), e); + } + } + + // DateTime.Now fast path that avoids allocating an historically accurate TimeZoneInfo.Local and just creates a 1-year (current year) accurate time zone + internal static TimeSpan GetDateTimeNowUtcOffsetFromUtc(DateTime time, out bool isAmbiguousLocalDst) + { + bool isDaylightSavings; + // Use the standard code path for Unix since there isn't a faster way of handling current-year-only time zones + return GetUtcOffsetFromUtc(time, Local, out isDaylightSavings, out isAmbiguousLocalDst); + } + + // TZFILE(5) BSD File Formats Manual TZFILE(5) + // + // NAME + // tzfile -- timezone information + // + // SYNOPSIS + // #include "/usr/src/lib/libc/stdtime/tzfile.h" + // + // DESCRIPTION + // The time zone information files used by tzset(3) begin with the magic + // characters ``TZif'' to identify them as time zone information files, fol- + // lowed by sixteen bytes reserved for future use, followed by four four- + // byte values written in a ``standard'' byte order (the high-order byte of + // the value is written first). These values are, in order: + // + // tzh_ttisgmtcnt The number of UTC/local indicators stored in the file. + // tzh_ttisstdcnt The number of standard/wall indicators stored in the + // file. + // tzh_leapcnt The number of leap seconds for which data is stored in + // the file. + // tzh_timecnt The number of ``transition times'' for which data is + // stored in the file. + // tzh_typecnt The number of ``local time types'' for which data is + // stored in the file (must not be zero). + // tzh_charcnt The number of characters of ``time zone abbreviation + // strings'' stored in the file. + // + // The above header is followed by tzh_timecnt four-byte values of type + // long, sorted in ascending order. These values are written in ``stan- + // dard'' byte order. Each is used as a transition time (as returned by + // time(3)) at which the rules for computing local time change. Next come + // tzh_timecnt one-byte values of type unsigned char; each one tells which + // of the different types of ``local time'' types described in the file is + // associated with the same-indexed transition time. These values serve as + // indices into an array of ttinfo structures that appears next in the file; + // these structures are defined as follows: + // + // struct ttinfo { + // long tt_gmtoff; + // int tt_isdst; + // unsigned int tt_abbrind; + // }; + // + // Each structure is written as a four-byte value for tt_gmtoff of type + // long, in a standard byte order, followed by a one-byte value for tt_isdst + // and a one-byte value for tt_abbrind. In each structure, tt_gmtoff gives + // the number of seconds to be added to UTC, tt_isdst tells whether tm_isdst + // should be set by localtime(3) and tt_abbrind serves as an index into the + // array of time zone abbreviation characters that follow the ttinfo struc- + // ture(s) in the file. + // + // Then there are tzh_leapcnt pairs of four-byte values, written in standard + // byte order; the first value of each pair gives the time (as returned by + // time(3)) at which a leap second occurs; the second gives the total number + // of leap seconds to be applied after the given time. The pairs of values + // are sorted in ascending order by time.b + // + // Then there are tzh_ttisstdcnt standard/wall indicators, each stored as a + // one-byte value; they tell whether the transition times associated with + // local time types were specified as standard time or wall clock time, and + // are used when a time zone file is used in handling POSIX-style time zone + // environment variables. + // + // Finally there are tzh_ttisgmtcnt UTC/local indicators, each stored as a + // one-byte value; they tell whether the transition times associated with + // local time types were specified as UTC or local time, and are used when a + // time zone file is used in handling POSIX-style time zone environment + // variables. + // + // localtime uses the first standard-time ttinfo structure in the file (or + // simply the first ttinfo structure in the absence of a standard-time + // structure) if either tzh_timecnt is zero or the time argument is less + // than the first transition time recorded in the file. + // + // SEE ALSO + // ctime(3), time2posix(3), zic(8) + // + // BSD September 13, 1994 BSD + // + // + // + // TIME(3) BSD Library Functions Manual TIME(3) + // + // NAME + // time -- get time of day + // + // LIBRARY + // Standard C Library (libc, -lc) + // + // SYNOPSIS + // #include + // + // time_t + // time(time_t *tloc); + // + // DESCRIPTION + // The time() function returns the value of time in seconds since 0 hours, 0 + // minutes, 0 seconds, January 1, 1970, Coordinated Universal Time, without + // including leap seconds. If an error occurs, time() returns the value + // (time_t)-1. + // + // The return value is also stored in *tloc, provided that tloc is non-null. + // + // ERRORS + // The time() function may fail for any of the reasons described in + // gettimeofday(2). + // + // SEE ALSO + // gettimeofday(2), ctime(3) + // + // STANDARDS + // The time function conforms to IEEE Std 1003.1-2001 (``POSIX.1''). + // + // BUGS + // Neither ISO/IEC 9899:1999 (``ISO C99'') nor IEEE Std 1003.1-2001 + // (``POSIX.1'') requires time() to set errno on failure; thus, it is impos- + // sible for an application to distinguish the valid time value -1 (repre- + // senting the last UTC second of 1969) from the error return value. + // + // Systems conforming to earlier versions of the C and POSIX standards + // (including older versions of FreeBSD) did not set *tloc in the error + // case. + // + // HISTORY + // A time() function appeared in Version 6 AT&T UNIX. + // + // BSD July 18, 2003 BSD + // + // + private static void TZif_GenerateAdjustmentRules(out AdjustmentRule[]? rules, TimeSpan baseUtcOffset, DateTime[] dts, byte[] typeOfLocalTime, + TZifType[] transitionType, bool[] StandardTime, bool[] GmtTime, string? futureTransitionsPosixFormat) + { + rules = null; + + if (dts.Length > 0) + { + int index = 0; + List rulesList = new List(); + + while (index <= dts.Length) + { + TZif_GenerateAdjustmentRule(ref index, baseUtcOffset, rulesList, dts, typeOfLocalTime, transitionType, StandardTime, GmtTime, futureTransitionsPosixFormat); + } + + rules = rulesList.ToArray(); + if (rules != null && rules.Length == 0) + { + rules = null; + } + } + } + + private static void TZif_GenerateAdjustmentRule(ref int index, TimeSpan timeZoneBaseUtcOffset, List rulesList, DateTime[] dts, + byte[] typeOfLocalTime, TZifType[] transitionTypes, bool[] StandardTime, bool[] GmtTime, string? futureTransitionsPosixFormat) + { + // To generate AdjustmentRules, use the following approach: + // The first AdjustmentRule will go from DateTime.MinValue to the first transition time greater than DateTime.MinValue. + // Each middle AdjustmentRule wil go from dts[index-1] to dts[index]. + // The last AdjustmentRule will go from dts[dts.Length-1] to Datetime.MaxValue. + + // 0. Skip any DateTime.MinValue transition times. In newer versions of the tzfile, there + // is a "big bang" transition time, which is before the year 0001. Since any times before year 0001 + // cannot be represented by DateTime, there is no reason to make AdjustmentRules for these unrepresentable time periods. + // 1. If there are no DateTime.MinValue times, the first AdjustmentRule goes from DateTime.MinValue + // to the first transition and uses the first standard transitionType (or the first transitionType if none of them are standard) + // 2. Create an AdjustmentRule for each transition, i.e. from dts[index - 1] to dts[index]. + // This rule uses the transitionType[index - 1] and the whole AdjustmentRule only describes a single offset - either + // all daylight savings, or all standard time. + // 3. After all the transitions are filled out, the last AdjustmentRule is created from either: + // a. a POSIX-style timezone description ("futureTransitionsPosixFormat"), if there is one or + // b. continue the last transition offset until DateTime.Max + + while (index < dts.Length && dts[index] == DateTime.MinValue) + { + index++; + } + + if (rulesList.Count == 0 && index < dts.Length) + { + TZifType transitionType = TZif_GetEarlyDateTransitionType(transitionTypes); + DateTime endTransitionDate = dts[index]; + + TimeSpan transitionOffset = TZif_CalculateTransitionOffsetFromBase(transitionType.UtcOffset, timeZoneBaseUtcOffset); + TimeSpan daylightDelta = transitionType.IsDst ? transitionOffset : TimeSpan.Zero; + TimeSpan baseUtcDelta = transitionType.IsDst ? TimeSpan.Zero : transitionOffset; + + AdjustmentRule r = AdjustmentRule.CreateAdjustmentRule( + DateTime.MinValue, + endTransitionDate.AddTicks(-1), + daylightDelta, + default, + default, + baseUtcDelta, + noDaylightTransitions: true); + + if (!IsValidAdjustmentRuleOffset(timeZoneBaseUtcOffset, r)) + { + NormalizeAdjustmentRuleOffset(timeZoneBaseUtcOffset, ref r); + } + + rulesList.Add(r); + } + else if (index < dts.Length) + { + DateTime startTransitionDate = dts[index - 1]; + TZifType startTransitionType = transitionTypes[typeOfLocalTime[index - 1]]; + + DateTime endTransitionDate = dts[index]; + + TimeSpan transitionOffset = TZif_CalculateTransitionOffsetFromBase(startTransitionType.UtcOffset, timeZoneBaseUtcOffset); + TimeSpan daylightDelta = startTransitionType.IsDst ? transitionOffset : TimeSpan.Zero; + TimeSpan baseUtcDelta = startTransitionType.IsDst ? TimeSpan.Zero : transitionOffset; + + TransitionTime dstStart; + if (startTransitionType.IsDst) + { + // the TransitionTime fields are not used when AdjustmentRule.NoDaylightTransitions == true. + // However, there are some cases in the past where DST = true, and the daylight savings offset + // now equals what the current BaseUtcOffset is. In that case, the AdjustmentRule.DaylightOffset + // is going to be TimeSpan.Zero. But we still need to return 'true' from AdjustmentRule.HasDaylightSaving. + // To ensure we always return true from HasDaylightSaving, make a "special" dstStart that will make the logic + // in HasDaylightSaving return true. + dstStart = s_daylightRuleMarker; + } + else + { + dstStart = default; + } + + AdjustmentRule r = AdjustmentRule.CreateAdjustmentRule( + startTransitionDate, + endTransitionDate.AddTicks(-1), + daylightDelta, + dstStart, + default, + baseUtcDelta, + noDaylightTransitions: true); + + if (!IsValidAdjustmentRuleOffset(timeZoneBaseUtcOffset, r)) + { + NormalizeAdjustmentRuleOffset(timeZoneBaseUtcOffset, ref r); + } + + rulesList.Add(r); + } + else + { + // create the AdjustmentRule that will be used for all DateTimes after the last transition + + // NOTE: index == dts.Length + DateTime startTransitionDate = dts[index - 1]; + + AdjustmentRule? r = !string.IsNullOrEmpty(futureTransitionsPosixFormat) ? + TZif_CreateAdjustmentRuleForPosixFormat(futureTransitionsPosixFormat, startTransitionDate, timeZoneBaseUtcOffset) : + null; + + if (r == null) + { + // just use the last transition as the rule which will be used until the end of time + + TZifType transitionType = transitionTypes[typeOfLocalTime[index - 1]]; + TimeSpan transitionOffset = TZif_CalculateTransitionOffsetFromBase(transitionType.UtcOffset, timeZoneBaseUtcOffset); + TimeSpan daylightDelta = transitionType.IsDst ? transitionOffset : TimeSpan.Zero; + TimeSpan baseUtcDelta = transitionType.IsDst ? TimeSpan.Zero : transitionOffset; + + r = AdjustmentRule.CreateAdjustmentRule( + startTransitionDate, + DateTime.MaxValue, + daylightDelta, + default, + default, + baseUtcDelta, + noDaylightTransitions: true); + } + + if (!IsValidAdjustmentRuleOffset(timeZoneBaseUtcOffset, r)) + { + NormalizeAdjustmentRuleOffset(timeZoneBaseUtcOffset, ref r); + } + + rulesList.Add(r); + } + + index++; + } + + private static TimeSpan TZif_CalculateTransitionOffsetFromBase(TimeSpan transitionOffset, TimeSpan timeZoneBaseUtcOffset) + { + TimeSpan result = transitionOffset - timeZoneBaseUtcOffset; + + // TZif supports seconds-level granularity with offsets but TimeZoneInfo only supports minutes since it aligns + // with DateTimeOffset, SQL Server, and the W3C XML Specification + if (result.Ticks % TimeSpan.TicksPerMinute != 0) + { + result = new TimeSpan(result.Hours, result.Minutes, 0); + } + + return result; + } + + /// + /// Gets the first standard-time transition type, or simply the first transition type + /// if there are no standard transition types. + /// > + /// + /// from 'man tzfile': + /// localtime(3) uses the first standard-time ttinfo structure in the file + /// (or simply the first ttinfo structure in the absence of a standard-time + /// structure) if either tzh_timecnt is zero or the time argument is less + /// than the first transition time recorded in the file. + /// + private static TZifType TZif_GetEarlyDateTransitionType(TZifType[] transitionTypes) + { + foreach (TZifType transitionType in transitionTypes) + { + if (!transitionType.IsDst) + { + return transitionType; + } + } + + if (transitionTypes.Length > 0) + { + return transitionTypes[0]; + } + + throw new InvalidTimeZoneException(SR.InvalidTimeZone_NoTTInfoStructures); + } + + /// + /// Creates an AdjustmentRule given the POSIX TZ environment variable string. + /// + /// + /// See http://man7.org/linux/man-pages/man3/tzset.3.html for the format and semantics of this POSIX string. + /// + private static AdjustmentRule? TZif_CreateAdjustmentRuleForPosixFormat(string posixFormat, DateTime startTransitionDate, TimeSpan timeZoneBaseUtcOffset) + { + if (TZif_ParsePosixFormat(posixFormat, + out ReadOnlySpan standardName, + out ReadOnlySpan standardOffset, + out ReadOnlySpan daylightSavingsName, + out ReadOnlySpan daylightSavingsOffset, + out ReadOnlySpan start, + out ReadOnlySpan startTime, + out ReadOnlySpan end, + out ReadOnlySpan endTime)) + { + // a valid posixFormat has at least standardName and standardOffset + + TimeSpan? parsedBaseOffset = TZif_ParseOffsetString(standardOffset); + if (parsedBaseOffset.HasValue) + { + TimeSpan baseOffset = parsedBaseOffset.GetValueOrDefault().Negate(); // offsets are backwards in POSIX notation + baseOffset = TZif_CalculateTransitionOffsetFromBase(baseOffset, timeZoneBaseUtcOffset); + + // having a daylightSavingsName means there is a DST rule + if (!daylightSavingsName.IsEmpty) + { + TimeSpan? parsedDaylightSavings = TZif_ParseOffsetString(daylightSavingsOffset); + TimeSpan daylightSavingsTimeSpan; + if (!parsedDaylightSavings.HasValue) + { + // default DST to 1 hour if it isn't specified + daylightSavingsTimeSpan = new TimeSpan(1, 0, 0); + } + else + { + daylightSavingsTimeSpan = parsedDaylightSavings.GetValueOrDefault().Negate(); // offsets are backwards in POSIX notation + daylightSavingsTimeSpan = TZif_CalculateTransitionOffsetFromBase(daylightSavingsTimeSpan, timeZoneBaseUtcOffset); + daylightSavingsTimeSpan = TZif_CalculateTransitionOffsetFromBase(daylightSavingsTimeSpan, baseOffset); + } + + TransitionTime? dstStart = TZif_CreateTransitionTimeFromPosixRule(start, startTime); + TransitionTime? dstEnd = TZif_CreateTransitionTimeFromPosixRule(end, endTime); + + if (dstStart == null || dstEnd == null) + { + return null; + } + + return AdjustmentRule.CreateAdjustmentRule( + startTransitionDate, + DateTime.MaxValue, + daylightSavingsTimeSpan, + dstStart.GetValueOrDefault(), + dstEnd.GetValueOrDefault(), + baseOffset, + noDaylightTransitions: false); + } + else + { + // if there is no daylightSavingsName, the whole AdjustmentRule should be with no transitions - just the baseOffset + return AdjustmentRule.CreateAdjustmentRule( + startTransitionDate, + DateTime.MaxValue, + TimeSpan.Zero, + default, + default, + baseOffset, + noDaylightTransitions: true); + } + } + } + + return null; + } + + private static TimeSpan? TZif_ParseOffsetString(ReadOnlySpan offset) + { + TimeSpan? result = null; + + if (offset.Length > 0) + { + bool negative = offset[0] == '-'; + if (negative || offset[0] == '+') + { + offset = offset.Slice(1); + } + + // Try parsing just hours first. + // Note, TimeSpan.TryParseExact "%h" can't be used here because some time zones using values + // like "26" or "144" and TimeSpan parsing would turn that into 26 or 144 *days* instead of hours. + int hours; + if (int.TryParse(offset, out hours)) + { + result = new TimeSpan(hours, 0, 0); + } + else + { + TimeSpan parsedTimeSpan; + if (TimeSpan.TryParseExact(offset, "g", CultureInfo.InvariantCulture, out parsedTimeSpan)) + { + result = parsedTimeSpan; + } + } + + if (result.HasValue && negative) + { + result = result.GetValueOrDefault().Negate(); + } + } + + return result; + } + + private static DateTime ParseTimeOfDay(ReadOnlySpan time) + { + DateTime timeOfDay; + TimeSpan? timeOffset = TZif_ParseOffsetString(time); + if (timeOffset.HasValue) + { + // This logic isn't correct and can't be corrected until https://github.com/dotnet/runtime/issues/14966 is fixed. + // Some time zones use time values like, "26", "144", or "-2". + // This allows the week to sometimes be week 4 and sometimes week 5 in the month. + // For now, strip off any 'days' in the offset, and just get the time of day correct + timeOffset = new TimeSpan(timeOffset.GetValueOrDefault().Hours, timeOffset.GetValueOrDefault().Minutes, timeOffset.GetValueOrDefault().Seconds); + if (timeOffset.GetValueOrDefault() < TimeSpan.Zero) + { + timeOfDay = new DateTime(1, 1, 2, 0, 0, 0); + } + else + { + timeOfDay = new DateTime(1, 1, 1, 0, 0, 0); + } + + timeOfDay += timeOffset.GetValueOrDefault(); + } + else + { + // default to 2AM. + timeOfDay = new DateTime(1, 1, 1, 2, 0, 0); + } + + return timeOfDay; + } + + private static TransitionTime? TZif_CreateTransitionTimeFromPosixRule(ReadOnlySpan date, ReadOnlySpan time) + { + if (date.IsEmpty) + { + return null; + } + + if (date[0] == 'M') + { + // Mm.w.d + // This specifies day d of week w of month m. The day d must be between 0(Sunday) and 6.The week w must be between 1 and 5; + // week 1 is the first week in which day d occurs, and week 5 specifies the last d day in the month. The month m should be between 1 and 12. + + int month; + int week; + DayOfWeek day; + if (!TZif_ParseMDateRule(date, out month, out week, out day)) + { + throw new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_UnparseablePosixMDateString, date.ToString())); + } + + return TransitionTime.CreateFloatingDateRule(ParseTimeOfDay(time), month, week, day); + } + else + { + if (date[0] != 'J') + { + // 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 + // the year is a leap year or not. + // + // In leap year, n would be counted as: + // + // 0 30 31 59 60 90 335 365 + // |-------Jan--------|-------Feb--------|-------Mar--------|....|-------Dec--------| + // + // while in non leap year we'll have + // + // 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. + // + // 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 (int.TryParse(date, out int julianDay) && julianDay < 59) + { + 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; + } + + // Julian day + TZif_ParseJulianDay(date, out int month, out int day); + return TransitionTime.CreateFixedDateRule(ParseTimeOfDay(time), month, day); + } + } + + /// + /// Parses a string like Jn into month and day values. + /// + private static void TZif_ParseJulianDay(ReadOnlySpan date, out int month, out int day) + { + // Jn + // This specifies the Julian day, with n between 1 and 365.February 29 is never counted, even in leap years. + Debug.Assert(!date.IsEmpty); + Debug.Assert(date[0] == 'J'); + month = day = 0; + + int index = 1; + + if (index >= date.Length || ((uint)(date[index] - '0') > '9'-'0')) + { + throw new InvalidTimeZoneException(SR.InvalidTimeZone_InvalidJulianDay); + } + + int julianDay = 0; + + do + { + julianDay = julianDay * 10 + (int) (date[index] - '0'); + index++; + } while (index < date.Length && ((uint)(date[index] - '0') <= '9'-'0')); + + int[] days = GregorianCalendarHelper.DaysToMonth365; + + if (julianDay == 0 || julianDay > days[days.Length - 1]) + { + throw new InvalidTimeZoneException(SR.InvalidTimeZone_InvalidJulianDay); + } + + int i = 1; + while (i < days.Length && julianDay > days[i]) + { + i++; + } + + Debug.Assert(i > 0 && i < days.Length); + + month = i; + day = julianDay - days[i - 1]; + } + + /// + /// Parses a string like Mm.w.d into month, week and DayOfWeek values. + /// + /// + /// true if the parsing succeeded; otherwise, false. + /// + private static bool TZif_ParseMDateRule(ReadOnlySpan dateRule, out int month, out int week, out DayOfWeek dayOfWeek) + { + if (dateRule[0] == 'M') + { + int monthWeekDotIndex = dateRule.IndexOf('.'); + if (monthWeekDotIndex > 0) + { + ReadOnlySpan weekDaySpan = dateRule.Slice(monthWeekDotIndex + 1); + int weekDayDotIndex = weekDaySpan.IndexOf('.'); + if (weekDayDotIndex > 0) + { + if (int.TryParse(dateRule.Slice(1, monthWeekDotIndex - 1), out month) && + int.TryParse(weekDaySpan.Slice(0, weekDayDotIndex), out week) && + int.TryParse(weekDaySpan.Slice(weekDayDotIndex + 1), out int day)) + { + dayOfWeek = (DayOfWeek)day; + return true; + } + } + } + } + + month = 0; + week = 0; + dayOfWeek = default; + return false; + } + + private static bool TZif_ParsePosixFormat( + ReadOnlySpan posixFormat, + out ReadOnlySpan standardName, + out ReadOnlySpan standardOffset, + out ReadOnlySpan daylightSavingsName, + out ReadOnlySpan daylightSavingsOffset, + out ReadOnlySpan start, + out ReadOnlySpan startTime, + out ReadOnlySpan end, + out ReadOnlySpan endTime) + { + standardName = null; + standardOffset = null; + daylightSavingsName = null; + daylightSavingsOffset = null; + start = null; + startTime = null; + end = null; + endTime = null; + + int index = 0; + standardName = TZif_ParsePosixName(posixFormat, ref index); + standardOffset = TZif_ParsePosixOffset(posixFormat, ref index); + + daylightSavingsName = TZif_ParsePosixName(posixFormat, ref index); + if (!daylightSavingsName.IsEmpty) + { + daylightSavingsOffset = TZif_ParsePosixOffset(posixFormat, ref index); + + if (index < posixFormat.Length && posixFormat[index] == ',') + { + index++; + TZif_ParsePosixDateTime(posixFormat, ref index, out start, out startTime); + + if (index < posixFormat.Length && posixFormat[index] == ',') + { + index++; + TZif_ParsePosixDateTime(posixFormat, ref index, out end, out endTime); + } + } + } + + return !standardName.IsEmpty && !standardOffset.IsEmpty; + } + + private static ReadOnlySpan TZif_ParsePosixName(ReadOnlySpan posixFormat, ref int index) + { + bool isBracketEnclosed = index < posixFormat.Length && posixFormat[index] == '<'; + if (isBracketEnclosed) + { + // move past the opening bracket + index++; + + ReadOnlySpan result = TZif_ParsePosixString(posixFormat, ref index, c => c == '>'); + + // move past the closing bracket + if (index < posixFormat.Length && posixFormat[index] == '>') + { + index++; + } + + return result; + } + else + { + return TZif_ParsePosixString( + posixFormat, + ref index, + c => char.IsDigit(c) || c == '+' || c == '-' || c == ','); + } + } + + private static ReadOnlySpan TZif_ParsePosixOffset(ReadOnlySpan posixFormat, ref int index) => + TZif_ParsePosixString(posixFormat, ref index, c => !char.IsDigit(c) && c != '+' && c != '-' && c != ':'); + + private static void TZif_ParsePosixDateTime(ReadOnlySpan posixFormat, ref int index, out ReadOnlySpan date, out ReadOnlySpan time) + { + time = null; + + date = TZif_ParsePosixDate(posixFormat, ref index); + if (index < posixFormat.Length && posixFormat[index] == '/') + { + index++; + time = TZif_ParsePosixTime(posixFormat, ref index); + } + } + + private static ReadOnlySpan TZif_ParsePosixDate(ReadOnlySpan posixFormat, ref int index) => + TZif_ParsePosixString(posixFormat, ref index, c => c == '/' || c == ','); + + private static ReadOnlySpan TZif_ParsePosixTime(ReadOnlySpan posixFormat, ref int index) => + TZif_ParsePosixString(posixFormat, ref index, c => c == ','); + + private static ReadOnlySpan TZif_ParsePosixString(ReadOnlySpan posixFormat, ref int index, Func breakCondition) + { + int startIndex = index; + for (; index < posixFormat.Length; index++) + { + char current = posixFormat[index]; + if (breakCondition(current)) + { + break; + } + } + + return posixFormat.Slice(startIndex, index - startIndex); + } + + // Returns the Substring from zoneAbbreviations starting at index and ending at '\0' + // zoneAbbreviations is expected to be in the form: "PST\0PDT\0PWT\0\PPT" + private static string TZif_GetZoneAbbreviation(string zoneAbbreviations, int index) + { + int lastIndex = zoneAbbreviations.IndexOf('\0', index); + return lastIndex > 0 ? + zoneAbbreviations.Substring(index, lastIndex - index) : + zoneAbbreviations.Substring(index); + } + + // Converts an array of bytes into an int - always using standard byte order (Big Endian) + // per TZif file standard + private static int TZif_ToInt32(byte[] value, int startIndex) + => BinaryPrimitives.ReadInt32BigEndian(value.AsSpan(startIndex)); + + // Converts an array of bytes into a long - always using standard byte order (Big Endian) + // per TZif file standard + private static long TZif_ToInt64(byte[] value, int startIndex) + => BinaryPrimitives.ReadInt64BigEndian(value.AsSpan(startIndex)); + + private static long TZif_ToUnixTime(byte[] value, int startIndex, TZVersion version) => + version != TZVersion.V1 ? + TZif_ToInt64(value, startIndex) : + TZif_ToInt32(value, startIndex); + + private static DateTime TZif_UnixTimeToDateTime(long unixTime) => + unixTime < DateTimeOffset.UnixMinSeconds ? DateTime.MinValue : + unixTime > DateTimeOffset.UnixMaxSeconds ? DateTime.MaxValue : + DateTimeOffset.FromUnixTimeSeconds(unixTime).UtcDateTime; + + private static void TZif_ParseRaw(byte[] data, out TZifHead t, out DateTime[] dts, out byte[] typeOfLocalTime, out TZifType[] transitionType, + out string zoneAbbreviations, out bool[] StandardTime, out bool[] GmtTime, out string? futureTransitionsPosixFormat) + { + // initialize the out parameters in case the TZifHead ctor throws + dts = null!; + typeOfLocalTime = null!; + transitionType = null!; + zoneAbbreviations = string.Empty; + StandardTime = null!; + GmtTime = null!; + futureTransitionsPosixFormat = null; + + // read in the 44-byte TZ header containing the count/length fields + // + int index = 0; + t = new TZifHead(data, index); + index += TZifHead.Length; + + int timeValuesLength = 4; // the first version uses 4-bytes to specify times + if (t.Version != TZVersion.V1) + { + // move index past the V1 information to read the V2 information + index += (int)((timeValuesLength * t.TimeCount) + t.TimeCount + (6 * t.TypeCount) + ((timeValuesLength + 4) * t.LeapCount) + t.IsStdCount + t.IsGmtCount + t.CharCount); + + // read the V2 header + t = new TZifHead(data, index); + index += TZifHead.Length; + timeValuesLength = 8; // the second version uses 8-bytes + } + + // initialize the containers for the rest of the TZ data + dts = new DateTime[t.TimeCount]; + typeOfLocalTime = new byte[t.TimeCount]; + transitionType = new TZifType[t.TypeCount]; + zoneAbbreviations = string.Empty; + StandardTime = new bool[t.TypeCount]; + GmtTime = new bool[t.TypeCount]; + + // read in the UTC transition points and convert them to Windows + // + for (int i = 0; i < t.TimeCount; i++) + { + long unixTime = TZif_ToUnixTime(data, index, t.Version); + dts[i] = TZif_UnixTimeToDateTime(unixTime); + index += timeValuesLength; + } + + // read in the Type Indices; there is a 1:1 mapping of UTC transition points to Type Indices + // these indices directly map to the array index in the transitionType array below + // + for (int i = 0; i < t.TimeCount; i++) + { + typeOfLocalTime[i] = data[index]; + index++; + } + + // read in the Type table. Each 6-byte entry represents + // {UtcOffset, IsDst, AbbreviationIndex} + // + // each AbbreviationIndex is a character index into the zoneAbbreviations string below + // + for (int i = 0; i < t.TypeCount; i++) + { + transitionType[i] = new TZifType(data, index); + index += 6; + } + + // read in the Abbreviation ASCII string. This string will be in the form: + // "PST\0PDT\0PWT\0\PPT" + // + Encoding enc = Encoding.UTF8; + zoneAbbreviations = enc.GetString(data, index, (int)t.CharCount); + index += (int)t.CharCount; + + // skip ahead of the Leap-Seconds Adjustment data. In a future release, consider adding + // support for Leap-Seconds + // + index += (int)(t.LeapCount * (timeValuesLength + 4)); // skip the leap second transition times + + // read in the Standard Time table. There should be a 1:1 mapping between Type-Index and Standard + // Time table entries. + // + // TRUE = transition time is standard time + // FALSE = transition time is wall clock time + // ABSENT = transition time is wall clock time + // + for (int i = 0; i < t.IsStdCount && i < t.TypeCount && index < data.Length; i++) + { + StandardTime[i] = (data[index++] != 0); + } + + // read in the GMT Time table. There should be a 1:1 mapping between Type-Index and GMT Time table + // entries. + // + // TRUE = transition time is UTC + // FALSE = transition time is local time + // ABSENT = transition time is local time + // + for (int i = 0; i < t.IsGmtCount && i < t.TypeCount && index < data.Length; i++) + { + GmtTime[i] = (data[index++] != 0); + } + + if (t.Version != TZVersion.V1) + { + // read the POSIX-style format, which should be wrapped in newlines with the last newline at the end of the file + if (data[index++] == '\n' && data[data.Length - 1] == '\n') + { + futureTransitionsPosixFormat = enc.GetString(data, index, data.Length - index - 1); + } + } + } + + /// + /// Normalize adjustment rule offset so that it is within valid range + /// This method should not be called at all but is here in case something changes in the future + /// or if really old time zones are present on the OS (no combination is known at the moment) + /// + private static void NormalizeAdjustmentRuleOffset(TimeSpan baseUtcOffset, [NotNull] ref AdjustmentRule adjustmentRule) + { + // Certain time zones such as: + // Time Zone start date end date offset + // ----------------------------------------------------- + // America/Yakutat 0001-01-01 1867-10-18 14:41:00 + // America/Yakutat 1867-10-18 1900-08-20 14:41:00 + // America/Sitka 0001-01-01 1867-10-18 14:58:00 + // America/Sitka 1867-10-18 1900-08-20 14:58:00 + // Asia/Manila 0001-01-01 1844-12-31 -15:56:00 + // Pacific/Guam 0001-01-01 1845-01-01 -14:21:00 + // Pacific/Saipan 0001-01-01 1845-01-01 -14:21:00 + // + // have larger offset than currently supported by framework. + // If for whatever reason we find that time zone exceeding max + // offset of 14h this function will truncate it to the max valid offset. + // Updating max offset may cause problems with interacting with SQL server + // which uses SQL DATETIMEOFFSET field type which was originally designed to be + // bit-for-bit compatible with DateTimeOffset. + + TimeSpan utcOffset = GetUtcOffset(baseUtcOffset, adjustmentRule); + + // utc base offset delta increment + TimeSpan adjustment = TimeSpan.Zero; + + if (utcOffset > MaxOffset) + { + adjustment = MaxOffset - utcOffset; + } + else if (utcOffset < MinOffset) + { + adjustment = MinOffset - utcOffset; + } + + if (adjustment != TimeSpan.Zero) + { + adjustmentRule = AdjustmentRule.CreateAdjustmentRule( + adjustmentRule.DateStart, + adjustmentRule.DateEnd, + adjustmentRule.DaylightDelta, + adjustmentRule.DaylightTransitionStart, + adjustmentRule.DaylightTransitionEnd, + adjustmentRule.BaseUtcOffsetDelta + adjustment, + adjustmentRule.NoDaylightTransitions); + } + } + + private struct TZifType + { + public const int Length = 6; + + public readonly TimeSpan UtcOffset; + public readonly bool IsDst; + public readonly byte AbbreviationIndex; + + public TZifType(byte[] data, int index) + { + if (data == null || data.Length < index + Length) + { + throw new ArgumentException(SR.Argument_TimeZoneInfoInvalidTZif, nameof(data)); + } + UtcOffset = new TimeSpan(0, 0, TZif_ToInt32(data, index + 00)); + IsDst = (data[index + 4] != 0); + AbbreviationIndex = data[index + 5]; + } + } + + private struct TZifHead + { + public const int Length = 44; + + public readonly uint Magic; // TZ_MAGIC "TZif" + public readonly TZVersion Version; // 1 byte for a \0 or 2 or 3 + // public byte[15] Reserved; // reserved for future use + public readonly uint IsGmtCount; // number of transition time flags + public readonly uint IsStdCount; // number of transition time flags + public readonly uint LeapCount; // number of leap seconds + public readonly uint TimeCount; // number of transition times + public readonly uint TypeCount; // number of local time types + public readonly uint CharCount; // number of abbreviated characters + + public TZifHead(byte[] data, int index) + { + if (data == null || data.Length < Length) + { + throw new ArgumentException("bad data", nameof(data)); + } + + Magic = (uint)TZif_ToInt32(data, index + 00); + + if (Magic != 0x545A6966) + { + // 0x545A6966 = {0x54, 0x5A, 0x69, 0x66} = "TZif" + throw new ArgumentException(SR.Argument_TimeZoneInfoBadTZif, nameof(data)); + } + + byte version = data[index + 04]; + Version = + version == '2' ? TZVersion.V2 : + version == '3' ? TZVersion.V3 : + TZVersion.V1; // default/fallback to V1 to guard against future, unsupported version numbers + + // skip the 15 byte reserved field + + // don't use the BitConverter class which parses data + // based on the Endianess of the machine architecture. + // this data is expected to always be in "standard byte order", + // regardless of the machine it is being processed on. + + IsGmtCount = (uint)TZif_ToInt32(data, index + 20); + IsStdCount = (uint)TZif_ToInt32(data, index + 24); + LeapCount = (uint)TZif_ToInt32(data, index + 28); + TimeCount = (uint)TZif_ToInt32(data, index + 32); + TypeCount = (uint)TZif_ToInt32(data, index + 36); + CharCount = (uint)TZif_ToInt32(data, index + 40); + } + } + + private enum TZVersion : byte + { + V1 = 0, + V2, + V3, + // when adding more versions, ensure all the logic using TZVersion is still correct + } + + // Helper function for string array search. (LINQ is not available here.) + private static bool StringArrayContains(string value, string[] source, StringComparison comparison) + { + foreach (string s in source) + { + if (string.Equals(s, value, comparison)) + { + return true; + } + } + + return false; + } + } +} 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 ebc626a6221940..18e507a8ef8609 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -1,299 +1,15 @@ // 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.Binary; using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; using System.IO; using System.Text; -using System.Threading; -using System.Security; namespace System { public sealed partial class TimeZoneInfo { - private const string DefaultTimeZoneDirectory = "/usr/share/zoneinfo/"; private const string ZoneTabFileName = "zone.tab"; - private const string TimeZoneEnvironmentVariable = "TZ"; - private const string TimeZoneDirectoryEnvironmentVariable = "TZDIR"; - - // UTC aliases per https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml - // Hard-coded because we need to treat all aliases of UTC the same even when globalization data is not available. - // (This list is not likely to change.) - private static readonly string[] s_UtcAliases = new[] { - "Etc/UTC", - "Etc/UCT", - "Etc/Universal", - "Etc/Zulu", - "UCT", - "UTC", - "Universal", - "Zulu" - }; - - private static readonly TimeZoneInfo s_utcTimeZone = CreateUtcTimeZone(); - - private TimeZoneInfo(byte[] data, string id, bool dstDisabled) - { - _id = id; - - HasIanaId = true; - - // Handle UTC and its aliases - if (StringArrayContains(_id, s_UtcAliases, StringComparison.OrdinalIgnoreCase)) - { - _standardDisplayName = GetUtcStandardDisplayName(); - _daylightDisplayName = _standardDisplayName; - _displayName = GetUtcFullDisplayName(_id, _standardDisplayName); - _baseUtcOffset = TimeSpan.Zero; - _adjustmentRules = Array.Empty(); - return; - } - - TZifHead t; - DateTime[] dts; - byte[] typeOfLocalTime; - TZifType[] transitionType; - string zoneAbbreviations; - bool[] StandardTime; - bool[] GmtTime; - string? futureTransitionsPosixFormat; - string? standardAbbrevName = null; - string? daylightAbbrevName = null; - - // parse the raw TZif bytes; this method can throw ArgumentException when the data is malformed. - TZif_ParseRaw(data, out t, out dts, out typeOfLocalTime, out transitionType, out zoneAbbreviations, out StandardTime, out GmtTime, out futureTransitionsPosixFormat); - - // find the best matching baseUtcOffset and display strings based on the current utcNow value. - // NOTE: read the Standard and Daylight display strings from the tzfile now in case they can't be loaded later - // from the globalization data. - DateTime utcNow = DateTime.UtcNow; - for (int i = 0; i < dts.Length && dts[i] <= utcNow; i++) - { - int type = typeOfLocalTime[i]; - if (!transitionType[type].IsDst) - { - _baseUtcOffset = transitionType[type].UtcOffset; - standardAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[type].AbbreviationIndex); - } - else - { - daylightAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[type].AbbreviationIndex); - } - } - - if (dts.Length == 0) - { - // time zones like Africa/Bujumbura and Etc/GMT* have no transition times but still contain - // TZifType entries that may contain a baseUtcOffset and display strings - for (int i = 0; i < transitionType.Length; i++) - { - if (!transitionType[i].IsDst) - { - _baseUtcOffset = transitionType[i].UtcOffset; - standardAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[i].AbbreviationIndex); - } - else - { - daylightAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[i].AbbreviationIndex); - } - } - } - - // Set fallback values using abbreviations, base offset, and id - // These are expected in environments without time zone globalization data - _standardDisplayName = standardAbbrevName; - _daylightDisplayName = daylightAbbrevName ?? standardAbbrevName; - _displayName = $"(UTC{(_baseUtcOffset >= TimeSpan.Zero ? '+' : '-')}{_baseUtcOffset:hh\\:mm}) {_id}"; - - // Try to populate the display names from the globalization data - TryPopulateTimeZoneDisplayNamesFromGlobalizationData(_id, _baseUtcOffset, ref _standardDisplayName, ref _daylightDisplayName, ref _displayName); - - // TZif supports seconds-level granularity with offsets but TimeZoneInfo only supports minutes since it aligns - // with DateTimeOffset, SQL Server, and the W3C XML Specification - if (_baseUtcOffset.Ticks % TimeSpan.TicksPerMinute != 0) - { - _baseUtcOffset = new TimeSpan(_baseUtcOffset.Hours, _baseUtcOffset.Minutes, 0); - } - - if (!dstDisabled) - { - // only create the adjustment rule if DST is enabled - TZif_GenerateAdjustmentRules(out _adjustmentRules, _baseUtcOffset, dts, typeOfLocalTime, transitionType, StandardTime, GmtTime, futureTransitionsPosixFormat); - } - - ValidateTimeZoneInfo(_id, _baseUtcOffset, _adjustmentRules, out _supportsDaylightSavingTime); - } - - // The TransitionTime fields are not used when AdjustmentRule.NoDaylightTransitions == true. - // However, there are some cases in the past where DST = true, and the daylight savings offset - // now equals what the current BaseUtcOffset is. In that case, the AdjustmentRule.DaylightOffset - // is going to be TimeSpan.Zero. But we still need to return 'true' from AdjustmentRule.HasDaylightSaving. - // To ensure we always return true from HasDaylightSaving, make a "special" dstStart that will make the logic - // in HasDaylightSaving return true. - private static readonly TransitionTime s_daylightRuleMarker = TransitionTime.CreateFixedDateRule(DateTime.MinValue.AddMilliseconds(2), 1, 1); - - // Truncate the date and the time to Milliseconds precision - private static DateTime GetTimeOnlyInMillisecondsPrecision(DateTime input) => new DateTime((input.TimeOfDay.Ticks / TimeSpan.TicksPerMillisecond) * TimeSpan.TicksPerMillisecond); - - /// - /// Returns a cloned array of AdjustmentRule objects - /// - public AdjustmentRule[] GetAdjustmentRules() - { - if (_adjustmentRules == null) - { - return Array.Empty(); - } - - // The rules we use in Unix care mostly about the start and end dates but don't fill the transition start and end info. - // as the rules now is public, we should fill it properly so the caller doesn't have to know how we use it internally - // and can use it as it is used in Windows - - List rulesList = new List(_adjustmentRules.Length); - - for (int i = 0; i < _adjustmentRules.Length; i++) - { - AdjustmentRule rule = _adjustmentRules[i]; - - if (rule.NoDaylightTransitions && - rule.DaylightTransitionStart != s_daylightRuleMarker && - rule.DaylightDelta == TimeSpan.Zero && rule.BaseUtcOffsetDelta == TimeSpan.Zero) - { - // This rule has no time transition, ignore it. - continue; - } - - DateTime start = rule.DateStart.Kind == DateTimeKind.Utc ? - // At the daylight start we didn't start the daylight saving yet then we convert to Local time - // by adding the _baseUtcOffset to the UTC time - new DateTime(rule.DateStart.Ticks + _baseUtcOffset.Ticks, DateTimeKind.Unspecified) : - rule.DateStart; - DateTime end = rule.DateEnd.Kind == DateTimeKind.Utc ? - // At the daylight saving end, the UTC time is mapped to local time which is already shifted by the daylight delta - // we calculate the local time by adding _baseUtcOffset + DaylightDelta to the UTC time - new DateTime(rule.DateEnd.Ticks + _baseUtcOffset.Ticks + rule.DaylightDelta.Ticks, DateTimeKind.Unspecified) : - rule.DateEnd; - - if (start.Year == end.Year || !rule.NoDaylightTransitions) - { - // If the rule is covering only one year then the start and end transitions would occur in that year, we don't need to split the rule. - // Also, rule.NoDaylightTransitions be false in case the rule was created from a POSIX time zone string and having a DST transition. We can represent this in one rule too - TransitionTime startTransition = rule.NoDaylightTransitions ? TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(start), start.Month, start.Day) : rule.DaylightTransitionStart; - TransitionTime endTransition = rule.NoDaylightTransitions ? TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(end), end.Month, end.Day) : rule.DaylightTransitionEnd; - rulesList.Add(AdjustmentRule.CreateAdjustmentRule(start.Date, end.Date, rule.DaylightDelta, startTransition, endTransition, rule.BaseUtcOffsetDelta)); - } - else - { - // For rules spanning more than one year. The time transition inside this rule would apply for the whole time spanning these years - // and not for partial time of every year. - // AdjustmentRule cannot express such rule using the DaylightTransitionStart and DaylightTransitionEnd because - // the DaylightTransitionStart and DaylightTransitionEnd express the transition for every year. - // We split the rule into more rules. The first rule will start from the start year of the original rule and ends at the end of the same year. - // The second splitted rule would cover the middle range of the original rule and ranging from the year start+1 to - // year end-1. The transition time in this rule would start from Jan 1st to end of December. - // The last splitted rule would start from the Jan 1st of the end year of the original rule and ends at the end transition time of the original rule. - - // Add the first rule. - DateTime endForFirstRule = new DateTime(start.Year + 1, 1, 1).AddMilliseconds(-1); // At the end of the first year - TransitionTime startTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(start), start.Month, start.Day); - TransitionTime endTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(endForFirstRule), endForFirstRule.Month, endForFirstRule.Day); - rulesList.Add(AdjustmentRule.CreateAdjustmentRule(start.Date, endForFirstRule.Date, rule.DaylightDelta, startTransition, endTransition, rule.BaseUtcOffsetDelta)); - - // Check if there is range of years between the start and the end years - if (end.Year - start.Year > 1) - { - // Add the middle rule. - DateTime middleYearStart = new DateTime(start.Year + 1, 1, 1); - DateTime middleYearEnd = new DateTime(end.Year, 1, 1).AddMilliseconds(-1); - startTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(middleYearStart), middleYearStart.Month, middleYearStart.Day); - endTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(middleYearEnd), middleYearEnd.Month, middleYearEnd.Day); - rulesList.Add(AdjustmentRule.CreateAdjustmentRule(middleYearStart.Date, middleYearEnd.Date, rule.DaylightDelta, startTransition, endTransition, rule.BaseUtcOffsetDelta)); - } - - // Add the end rule. - DateTime endYearStart = new DateTime(end.Year, 1, 1); // At the beginning of the last year - startTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(endYearStart), endYearStart.Month, endYearStart.Day); - endTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(end), end.Month, end.Day); - rulesList.Add(AdjustmentRule.CreateAdjustmentRule(endYearStart.Date, end.Date, rule.DaylightDelta, startTransition, endTransition, rule.BaseUtcOffsetDelta)); - } - } - - return rulesList.ToArray(); - } - - private static void PopulateAllSystemTimeZones(CachedData cachedData) - { - Debug.Assert(Monitor.IsEntered(cachedData)); - - string timeZoneDirectory = GetTimeZoneDirectory(); - foreach (string timeZoneId in GetTimeZoneIds(timeZoneDirectory)) - { - TryGetTimeZone(timeZoneId, false, out _, out _, cachedData, alwaysFallbackToLocalMachine: true); // populate the cache - } - } - - /// - /// Helper function for retrieving the local system time zone. - /// May throw COMException, TimeZoneNotFoundException, InvalidTimeZoneException. - /// Assumes cachedData lock is taken. - /// - /// A new TimeZoneInfo instance. - private static TimeZoneInfo GetLocalTimeZone(CachedData cachedData) - { - Debug.Assert(Monitor.IsEntered(cachedData)); - - // Without Registry support, create the TimeZoneInfo from a TZ file - return GetLocalTimeZoneFromTzFile(); - } - - private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out TimeZoneInfo? value, out Exception? e) - { - value = null; - e = null; - - string timeZoneDirectory = GetTimeZoneDirectory(); - string timeZoneFilePath = Path.Combine(timeZoneDirectory, id); - byte[] rawData; - try - { - rawData = File.ReadAllBytes(timeZoneFilePath); - } - catch (UnauthorizedAccessException ex) - { - e = ex; - return TimeZoneInfoResult.SecurityException; - } - catch (FileNotFoundException ex) - { - e = ex; - return TimeZoneInfoResult.TimeZoneNotFoundException; - } - catch (DirectoryNotFoundException ex) - { - e = ex; - return TimeZoneInfoResult.TimeZoneNotFoundException; - } - catch (IOException ex) - { - e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, timeZoneFilePath), ex); - return TimeZoneInfoResult.InvalidTimeZoneException; - } - - value = GetTimeZoneFromTzData(rawData, id); - - if (value == null) - { - e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, timeZoneFilePath)); - return TimeZoneInfoResult.InvalidTimeZoneException; - } - - return TimeZoneInfoResult.Success; - } /// /// Returns a collection of TimeZone Id values from the zone.tab file in the timeZoneDirectory. @@ -350,1461 +66,5 @@ private static List GetTimeZoneIds(string timeZoneDirectory) return timeZoneIds; } - - /// - /// Gets the tzfile raw data for the current 'local' time zone using the following rules. - /// 1. Read the TZ environment variable. If it is set, use it. - /// 2. Look for the data in /etc/localtime. - /// 3. Look for the data in GetTimeZoneDirectory()/localtime. - /// 4. Use UTC if all else fails. - /// - private static bool TryGetLocalTzFile([NotNullWhen(true)] out byte[]? rawData, [NotNullWhen(true)] out string? id) - { - rawData = null; - id = null; - string? tzVariable = GetTzEnvironmentVariable(); - - // If the env var is null, use the localtime file - if (tzVariable == null) - { - return - TryLoadTzFile("/etc/localtime", ref rawData, ref id) || - TryLoadTzFile(Path.Combine(GetTimeZoneDirectory(), "localtime"), ref rawData, ref id); - } - - // If it's empty, use UTC (TryGetLocalTzFile() should return false). - if (tzVariable.Length == 0) - { - return false; - } - - // Otherwise, use the path from the env var. If it's not absolute, make it relative - // to the system timezone directory - string tzFilePath; - if (tzVariable[0] != '/') - { - id = tzVariable; - tzFilePath = Path.Combine(GetTimeZoneDirectory(), tzVariable); - } - else - { - tzFilePath = tzVariable; - } - return TryLoadTzFile(tzFilePath, ref rawData, ref id); - } - - private static string? GetTzEnvironmentVariable() - { - string? result = Environment.GetEnvironmentVariable(TimeZoneEnvironmentVariable); - if (!string.IsNullOrEmpty(result)) - { - if (result[0] == ':') - { - // strip off the ':' prefix - result = result.Substring(1); - } - } - - return result; - } - - private static bool TryLoadTzFile(string tzFilePath, [NotNullWhen(true)] ref byte[]? rawData, [NotNullWhen(true)] ref string? id) - { - if (File.Exists(tzFilePath)) - { - try - { - rawData = File.ReadAllBytes(tzFilePath); - if (string.IsNullOrEmpty(id)) - { - id = FindTimeZoneIdUsingReadLink(tzFilePath); - - if (string.IsNullOrEmpty(id)) - { - id = FindTimeZoneId(rawData); - } - } - return true; - } - catch (IOException) { } - catch (SecurityException) { } - catch (UnauthorizedAccessException) { } - } - return false; - } - - /// - /// Finds the time zone id by using 'readlink' on the path to see if tzFilePath is - /// a symlink to a file. - /// - private static string? FindTimeZoneIdUsingReadLink(string tzFilePath) - { - string? id = null; - - string? symlinkPath = Interop.Sys.ReadLink(tzFilePath); - if (symlinkPath != null) - { - // symlinkPath can be relative path, use Path to get the full absolute path. - symlinkPath = Path.GetFullPath(symlinkPath, Path.GetDirectoryName(tzFilePath)!); - - string timeZoneDirectory = GetTimeZoneDirectory(); - if (symlinkPath.StartsWith(timeZoneDirectory, StringComparison.Ordinal)) - { - id = symlinkPath.Substring(timeZoneDirectory.Length); - } - } - - return id; - } - - private static string? GetDirectoryEntryFullPath(ref Interop.Sys.DirectoryEntry dirent, string currentPath) - { - ReadOnlySpan direntName = dirent.GetName(stackalloc char[Interop.Sys.DirectoryEntry.NameBufferSize]); - - if ((direntName.Length == 1 && direntName[0] == '.') || - (direntName.Length == 2 && direntName[0] == '.' && direntName[1] == '.')) - return null; - - return Path.Join(currentPath.AsSpan(), direntName); - } - - /// - /// Enumerate files - /// - private static unsafe void EnumerateFilesRecursively(string path, Predicate condition) - { - List? toExplore = null; // List used as a stack - - int bufferSize = Interop.Sys.GetReadDirRBufferSize(); - byte[]? dirBuffer = null; - try - { - dirBuffer = ArrayPool.Shared.Rent(bufferSize); - string currentPath = path; - - fixed (byte* dirBufferPtr = dirBuffer) - { - while (true) - { - IntPtr dirHandle = Interop.Sys.OpenDir(currentPath); - if (dirHandle == IntPtr.Zero) - { - throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo(), currentPath, isDirectory: true); - } - - try - { - // Read each entry from the enumerator - Interop.Sys.DirectoryEntry dirent; - while (Interop.Sys.ReadDirR(dirHandle, dirBufferPtr, bufferSize, out dirent) == 0) - { - string? fullPath = GetDirectoryEntryFullPath(ref dirent, currentPath); - if (fullPath == null) - continue; - - // Get from the dir entry whether the entry is a file or directory. - // We classify everything as a file unless we know it to be a directory. - bool isDir; - if (dirent.InodeType == Interop.Sys.NodeType.DT_DIR) - { - // We know it's a directory. - isDir = true; - } - else if (dirent.InodeType == Interop.Sys.NodeType.DT_LNK || dirent.InodeType == Interop.Sys.NodeType.DT_UNKNOWN) - { - // It's a symlink or unknown: stat to it to see if we can resolve it to a directory. - // If we can't (e.g. symlink to a file, broken symlink, etc.), we'll just treat it as a file. - - Interop.Sys.FileStatus fileinfo; - if (Interop.Sys.Stat(fullPath, out fileinfo) >= 0) - { - isDir = (fileinfo.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR; - } - else - { - isDir = false; - } - } - else - { - // Otherwise, treat it as a file. This includes regular files, FIFOs, etc. - isDir = false; - } - - // Yield the result if the user has asked for it. In the case of directories, - // always explore it by pushing it onto the stack, regardless of whether - // we're returning directories. - if (isDir) - { - toExplore ??= new List(); - toExplore.Add(fullPath); - } - else if (condition(fullPath)) - { - return; - } - } - } - finally - { - if (dirHandle != IntPtr.Zero) - Interop.Sys.CloseDir(dirHandle); - } - - if (toExplore == null || toExplore.Count == 0) - break; - - currentPath = toExplore[toExplore.Count - 1]; - toExplore.RemoveAt(toExplore.Count - 1); - } - } - } - finally - { - if (dirBuffer != null) - ArrayPool.Shared.Return(dirBuffer); - } - } - - /// - /// Find the time zone id by searching all the tzfiles for the one that matches rawData - /// and return its file name. - /// - private static string FindTimeZoneId(byte[] rawData) - { - // default to "Local" if we can't find the right tzfile - string id = LocalId; - string timeZoneDirectory = GetTimeZoneDirectory(); - string localtimeFilePath = Path.Combine(timeZoneDirectory, "localtime"); - string posixrulesFilePath = Path.Combine(timeZoneDirectory, "posixrules"); - byte[] buffer = new byte[rawData.Length]; - - try - { - EnumerateFilesRecursively(timeZoneDirectory, (string filePath) => - { - // skip the localtime and posixrules file, since they won't give us the correct id - if (!string.Equals(filePath, localtimeFilePath, StringComparison.OrdinalIgnoreCase) - && !string.Equals(filePath, posixrulesFilePath, StringComparison.OrdinalIgnoreCase)) - { - if (CompareTimeZoneFile(filePath, buffer, rawData)) - { - // if all bytes are the same, this must be the right tz file - id = filePath; - - // strip off the root time zone directory - if (id.StartsWith(timeZoneDirectory, StringComparison.Ordinal)) - { - id = id.Substring(timeZoneDirectory.Length); - } - return true; - } - } - return false; - }); - } - catch (IOException) { } - catch (SecurityException) { } - catch (UnauthorizedAccessException) { } - - return id; - } - - private static bool CompareTimeZoneFile(string filePath, byte[] buffer, byte[] rawData) - { - try - { - // bufferSize == 1 used to avoid unnecessary buffer in FileStream - using (FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1)) - { - if (stream.Length == rawData.Length) - { - int index = 0; - int count = rawData.Length; - - while (count > 0) - { - int n = stream.Read(buffer, index, count); - if (n == 0) - ThrowHelper.ThrowEndOfFileException(); - - int end = index + n; - for (; index < end; index++) - { - if (buffer[index] != rawData[index]) - { - return false; - } - } - - count -= n; - } - - return true; - } - } - } - catch (IOException) { } - catch (SecurityException) { } - catch (UnauthorizedAccessException) { } - - return false; - } - - /// - /// Helper function used by 'GetLocalTimeZone()' - this function wraps the call - /// for loading time zone data from computers without Registry support. - /// - /// The TryGetLocalTzFile() call returns a Byte[] containing the compiled tzfile. - /// - private static TimeZoneInfo GetLocalTimeZoneFromTzFile() - { - byte[]? rawData; - string? id; - if (TryGetLocalTzFile(out rawData, out id)) - { - TimeZoneInfo? result = GetTimeZoneFromTzData(rawData, id); - if (result != null) - { - return result; - } - } - - // if we can't find a local time zone, return UTC - return Utc; - } - - private static TimeZoneInfo? GetTimeZoneFromTzData(byte[]? rawData, string id) - { - if (rawData != null) - { - try - { - return new TimeZoneInfo(rawData, id, dstDisabled: false); // create a TimeZoneInfo instance from the TZif data w/ DST support - } - catch (ArgumentException) { } - catch (InvalidTimeZoneException) { } - - try - { - return new TimeZoneInfo(rawData, id, dstDisabled: true); // create a TimeZoneInfo instance from the TZif data w/o DST support - } - catch (ArgumentException) { } - catch (InvalidTimeZoneException) { } - } - return null; - } - - private static string GetTimeZoneDirectory() - { - string? tzDirectory = Environment.GetEnvironmentVariable(TimeZoneDirectoryEnvironmentVariable); - - if (tzDirectory == null) - { - tzDirectory = DefaultTimeZoneDirectory; - } - else if (!tzDirectory.EndsWith(Path.DirectorySeparatorChar)) - { - tzDirectory += PathInternal.DirectorySeparatorCharAsString; - } - - return tzDirectory; - } - - /// - /// Helper function for retrieving a TimeZoneInfo object by time_zone_name. - /// This function wraps the logic necessary to keep the private - /// SystemTimeZones cache in working order - /// - /// This function will either return a valid TimeZoneInfo instance or - /// it will throw 'InvalidTimeZoneException' / 'TimeZoneNotFoundException'. - /// - public static TimeZoneInfo FindSystemTimeZoneById(string id) - { - // Special case for Utc as it will not exist in the dictionary with the rest - // of the system time zones. There is no need to do this check for Local.Id - // since Local is a real time zone that exists in the dictionary cache - if (string.Equals(id, UtcId, StringComparison.OrdinalIgnoreCase)) - { - return Utc; - } - - if (id == null) - { - throw new ArgumentNullException(nameof(id)); - } - else if (id.Length == 0 || id.Contains('\0')) - { - throw new TimeZoneNotFoundException(SR.Format(SR.TimeZoneNotFound_MissingData, id)); - } - - TimeZoneInfo? value; - Exception? e; - - TimeZoneInfoResult result; - - CachedData cachedData = s_cachedData; - - lock (cachedData) - { - result = TryGetTimeZone(id, false, out value, out e, cachedData, alwaysFallbackToLocalMachine: true); - } - - if (result == TimeZoneInfoResult.Success) - { - return value!; - } - else if (result == TimeZoneInfoResult.InvalidTimeZoneException) - { - Debug.Assert(e is InvalidTimeZoneException, - "TryGetTimeZone must create an InvalidTimeZoneException when it returns TimeZoneInfoResult.InvalidTimeZoneException"); - throw e; - } - else if (result == TimeZoneInfoResult.SecurityException) - { - throw new SecurityException(SR.Format(SR.Security_CannotReadFileData, id), e); - } - else - { - throw new TimeZoneNotFoundException(SR.Format(SR.TimeZoneNotFound_MissingData, id), e); - } - } - - // DateTime.Now fast path that avoids allocating an historically accurate TimeZoneInfo.Local and just creates a 1-year (current year) accurate time zone - internal static TimeSpan GetDateTimeNowUtcOffsetFromUtc(DateTime time, out bool isAmbiguousLocalDst) - { - bool isDaylightSavings; - // Use the standard code path for Unix since there isn't a faster way of handling current-year-only time zones - return GetUtcOffsetFromUtc(time, Local, out isDaylightSavings, out isAmbiguousLocalDst); - } - - // TZFILE(5) BSD File Formats Manual TZFILE(5) - // - // NAME - // tzfile -- timezone information - // - // SYNOPSIS - // #include "/usr/src/lib/libc/stdtime/tzfile.h" - // - // DESCRIPTION - // The time zone information files used by tzset(3) begin with the magic - // characters ``TZif'' to identify them as time zone information files, fol- - // lowed by sixteen bytes reserved for future use, followed by four four- - // byte values written in a ``standard'' byte order (the high-order byte of - // the value is written first). These values are, in order: - // - // tzh_ttisgmtcnt The number of UTC/local indicators stored in the file. - // tzh_ttisstdcnt The number of standard/wall indicators stored in the - // file. - // tzh_leapcnt The number of leap seconds for which data is stored in - // the file. - // tzh_timecnt The number of ``transition times'' for which data is - // stored in the file. - // tzh_typecnt The number of ``local time types'' for which data is - // stored in the file (must not be zero). - // tzh_charcnt The number of characters of ``time zone abbreviation - // strings'' stored in the file. - // - // The above header is followed by tzh_timecnt four-byte values of type - // long, sorted in ascending order. These values are written in ``stan- - // dard'' byte order. Each is used as a transition time (as returned by - // time(3)) at which the rules for computing local time change. Next come - // tzh_timecnt one-byte values of type unsigned char; each one tells which - // of the different types of ``local time'' types described in the file is - // associated with the same-indexed transition time. These values serve as - // indices into an array of ttinfo structures that appears next in the file; - // these structures are defined as follows: - // - // struct ttinfo { - // long tt_gmtoff; - // int tt_isdst; - // unsigned int tt_abbrind; - // }; - // - // Each structure is written as a four-byte value for tt_gmtoff of type - // long, in a standard byte order, followed by a one-byte value for tt_isdst - // and a one-byte value for tt_abbrind. In each structure, tt_gmtoff gives - // the number of seconds to be added to UTC, tt_isdst tells whether tm_isdst - // should be set by localtime(3) and tt_abbrind serves as an index into the - // array of time zone abbreviation characters that follow the ttinfo struc- - // ture(s) in the file. - // - // Then there are tzh_leapcnt pairs of four-byte values, written in standard - // byte order; the first value of each pair gives the time (as returned by - // time(3)) at which a leap second occurs; the second gives the total number - // of leap seconds to be applied after the given time. The pairs of values - // are sorted in ascending order by time.b - // - // Then there are tzh_ttisstdcnt standard/wall indicators, each stored as a - // one-byte value; they tell whether the transition times associated with - // local time types were specified as standard time or wall clock time, and - // are used when a time zone file is used in handling POSIX-style time zone - // environment variables. - // - // Finally there are tzh_ttisgmtcnt UTC/local indicators, each stored as a - // one-byte value; they tell whether the transition times associated with - // local time types were specified as UTC or local time, and are used when a - // time zone file is used in handling POSIX-style time zone environment - // variables. - // - // localtime uses the first standard-time ttinfo structure in the file (or - // simply the first ttinfo structure in the absence of a standard-time - // structure) if either tzh_timecnt is zero or the time argument is less - // than the first transition time recorded in the file. - // - // SEE ALSO - // ctime(3), time2posix(3), zic(8) - // - // BSD September 13, 1994 BSD - // - // - // - // TIME(3) BSD Library Functions Manual TIME(3) - // - // NAME - // time -- get time of day - // - // LIBRARY - // Standard C Library (libc, -lc) - // - // SYNOPSIS - // #include - // - // time_t - // time(time_t *tloc); - // - // DESCRIPTION - // The time() function returns the value of time in seconds since 0 hours, 0 - // minutes, 0 seconds, January 1, 1970, Coordinated Universal Time, without - // including leap seconds. If an error occurs, time() returns the value - // (time_t)-1. - // - // The return value is also stored in *tloc, provided that tloc is non-null. - // - // ERRORS - // The time() function may fail for any of the reasons described in - // gettimeofday(2). - // - // SEE ALSO - // gettimeofday(2), ctime(3) - // - // STANDARDS - // The time function conforms to IEEE Std 1003.1-2001 (``POSIX.1''). - // - // BUGS - // Neither ISO/IEC 9899:1999 (``ISO C99'') nor IEEE Std 1003.1-2001 - // (``POSIX.1'') requires time() to set errno on failure; thus, it is impos- - // sible for an application to distinguish the valid time value -1 (repre- - // senting the last UTC second of 1969) from the error return value. - // - // Systems conforming to earlier versions of the C and POSIX standards - // (including older versions of FreeBSD) did not set *tloc in the error - // case. - // - // HISTORY - // A time() function appeared in Version 6 AT&T UNIX. - // - // BSD July 18, 2003 BSD - // - // - private static void TZif_GenerateAdjustmentRules(out AdjustmentRule[]? rules, TimeSpan baseUtcOffset, DateTime[] dts, byte[] typeOfLocalTime, - TZifType[] transitionType, bool[] StandardTime, bool[] GmtTime, string? futureTransitionsPosixFormat) - { - rules = null; - - if (dts.Length > 0) - { - int index = 0; - List rulesList = new List(); - - while (index <= dts.Length) - { - TZif_GenerateAdjustmentRule(ref index, baseUtcOffset, rulesList, dts, typeOfLocalTime, transitionType, StandardTime, GmtTime, futureTransitionsPosixFormat); - } - - rules = rulesList.ToArray(); - if (rules != null && rules.Length == 0) - { - rules = null; - } - } - } - - private static void TZif_GenerateAdjustmentRule(ref int index, TimeSpan timeZoneBaseUtcOffset, List rulesList, DateTime[] dts, - byte[] typeOfLocalTime, TZifType[] transitionTypes, bool[] StandardTime, bool[] GmtTime, string? futureTransitionsPosixFormat) - { - // To generate AdjustmentRules, use the following approach: - // The first AdjustmentRule will go from DateTime.MinValue to the first transition time greater than DateTime.MinValue. - // Each middle AdjustmentRule wil go from dts[index-1] to dts[index]. - // The last AdjustmentRule will go from dts[dts.Length-1] to Datetime.MaxValue. - - // 0. Skip any DateTime.MinValue transition times. In newer versions of the tzfile, there - // is a "big bang" transition time, which is before the year 0001. Since any times before year 0001 - // cannot be represented by DateTime, there is no reason to make AdjustmentRules for these unrepresentable time periods. - // 1. If there are no DateTime.MinValue times, the first AdjustmentRule goes from DateTime.MinValue - // to the first transition and uses the first standard transitionType (or the first transitionType if none of them are standard) - // 2. Create an AdjustmentRule for each transition, i.e. from dts[index - 1] to dts[index]. - // This rule uses the transitionType[index - 1] and the whole AdjustmentRule only describes a single offset - either - // all daylight savings, or all standard time. - // 3. After all the transitions are filled out, the last AdjustmentRule is created from either: - // a. a POSIX-style timezone description ("futureTransitionsPosixFormat"), if there is one or - // b. continue the last transition offset until DateTime.Max - - while (index < dts.Length && dts[index] == DateTime.MinValue) - { - index++; - } - - if (rulesList.Count == 0 && index < dts.Length) - { - TZifType transitionType = TZif_GetEarlyDateTransitionType(transitionTypes); - DateTime endTransitionDate = dts[index]; - - TimeSpan transitionOffset = TZif_CalculateTransitionOffsetFromBase(transitionType.UtcOffset, timeZoneBaseUtcOffset); - TimeSpan daylightDelta = transitionType.IsDst ? transitionOffset : TimeSpan.Zero; - TimeSpan baseUtcDelta = transitionType.IsDst ? TimeSpan.Zero : transitionOffset; - - AdjustmentRule r = AdjustmentRule.CreateAdjustmentRule( - DateTime.MinValue, - endTransitionDate.AddTicks(-1), - daylightDelta, - default, - default, - baseUtcDelta, - noDaylightTransitions: true); - - if (!IsValidAdjustmentRuleOffset(timeZoneBaseUtcOffset, r)) - { - NormalizeAdjustmentRuleOffset(timeZoneBaseUtcOffset, ref r); - } - - rulesList.Add(r); - } - else if (index < dts.Length) - { - DateTime startTransitionDate = dts[index - 1]; - TZifType startTransitionType = transitionTypes[typeOfLocalTime[index - 1]]; - - DateTime endTransitionDate = dts[index]; - - TimeSpan transitionOffset = TZif_CalculateTransitionOffsetFromBase(startTransitionType.UtcOffset, timeZoneBaseUtcOffset); - TimeSpan daylightDelta = startTransitionType.IsDst ? transitionOffset : TimeSpan.Zero; - TimeSpan baseUtcDelta = startTransitionType.IsDst ? TimeSpan.Zero : transitionOffset; - - TransitionTime dstStart; - if (startTransitionType.IsDst) - { - // the TransitionTime fields are not used when AdjustmentRule.NoDaylightTransitions == true. - // However, there are some cases in the past where DST = true, and the daylight savings offset - // now equals what the current BaseUtcOffset is. In that case, the AdjustmentRule.DaylightOffset - // is going to be TimeSpan.Zero. But we still need to return 'true' from AdjustmentRule.HasDaylightSaving. - // To ensure we always return true from HasDaylightSaving, make a "special" dstStart that will make the logic - // in HasDaylightSaving return true. - dstStart = s_daylightRuleMarker; - } - else - { - dstStart = default; - } - - AdjustmentRule r = AdjustmentRule.CreateAdjustmentRule( - startTransitionDate, - endTransitionDate.AddTicks(-1), - daylightDelta, - dstStart, - default, - baseUtcDelta, - noDaylightTransitions: true); - - if (!IsValidAdjustmentRuleOffset(timeZoneBaseUtcOffset, r)) - { - NormalizeAdjustmentRuleOffset(timeZoneBaseUtcOffset, ref r); - } - - rulesList.Add(r); - } - else - { - // create the AdjustmentRule that will be used for all DateTimes after the last transition - - // NOTE: index == dts.Length - DateTime startTransitionDate = dts[index - 1]; - - AdjustmentRule? r = !string.IsNullOrEmpty(futureTransitionsPosixFormat) ? - TZif_CreateAdjustmentRuleForPosixFormat(futureTransitionsPosixFormat, startTransitionDate, timeZoneBaseUtcOffset) : - null; - - if (r == null) - { - // just use the last transition as the rule which will be used until the end of time - - TZifType transitionType = transitionTypes[typeOfLocalTime[index - 1]]; - TimeSpan transitionOffset = TZif_CalculateTransitionOffsetFromBase(transitionType.UtcOffset, timeZoneBaseUtcOffset); - TimeSpan daylightDelta = transitionType.IsDst ? transitionOffset : TimeSpan.Zero; - TimeSpan baseUtcDelta = transitionType.IsDst ? TimeSpan.Zero : transitionOffset; - - r = AdjustmentRule.CreateAdjustmentRule( - startTransitionDate, - DateTime.MaxValue, - daylightDelta, - default, - default, - baseUtcDelta, - noDaylightTransitions: true); - } - - if (!IsValidAdjustmentRuleOffset(timeZoneBaseUtcOffset, r)) - { - NormalizeAdjustmentRuleOffset(timeZoneBaseUtcOffset, ref r); - } - - rulesList.Add(r); - } - - index++; - } - - private static TimeSpan TZif_CalculateTransitionOffsetFromBase(TimeSpan transitionOffset, TimeSpan timeZoneBaseUtcOffset) - { - TimeSpan result = transitionOffset - timeZoneBaseUtcOffset; - - // TZif supports seconds-level granularity with offsets but TimeZoneInfo only supports minutes since it aligns - // with DateTimeOffset, SQL Server, and the W3C XML Specification - if (result.Ticks % TimeSpan.TicksPerMinute != 0) - { - result = new TimeSpan(result.Hours, result.Minutes, 0); - } - - return result; - } - - /// - /// Gets the first standard-time transition type, or simply the first transition type - /// if there are no standard transition types. - /// > - /// - /// from 'man tzfile': - /// localtime(3) uses the first standard-time ttinfo structure in the file - /// (or simply the first ttinfo structure in the absence of a standard-time - /// structure) if either tzh_timecnt is zero or the time argument is less - /// than the first transition time recorded in the file. - /// - private static TZifType TZif_GetEarlyDateTransitionType(TZifType[] transitionTypes) - { - foreach (TZifType transitionType in transitionTypes) - { - if (!transitionType.IsDst) - { - return transitionType; - } - } - - if (transitionTypes.Length > 0) - { - return transitionTypes[0]; - } - - throw new InvalidTimeZoneException(SR.InvalidTimeZone_NoTTInfoStructures); - } - - /// - /// Creates an AdjustmentRule given the POSIX TZ environment variable string. - /// - /// - /// See http://man7.org/linux/man-pages/man3/tzset.3.html for the format and semantics of this POSIX string. - /// - private static AdjustmentRule? TZif_CreateAdjustmentRuleForPosixFormat(string posixFormat, DateTime startTransitionDate, TimeSpan timeZoneBaseUtcOffset) - { - if (TZif_ParsePosixFormat(posixFormat, - out ReadOnlySpan standardName, - out ReadOnlySpan standardOffset, - out ReadOnlySpan daylightSavingsName, - out ReadOnlySpan daylightSavingsOffset, - out ReadOnlySpan start, - out ReadOnlySpan startTime, - out ReadOnlySpan end, - out ReadOnlySpan endTime)) - { - // a valid posixFormat has at least standardName and standardOffset - - TimeSpan? parsedBaseOffset = TZif_ParseOffsetString(standardOffset); - if (parsedBaseOffset.HasValue) - { - TimeSpan baseOffset = parsedBaseOffset.GetValueOrDefault().Negate(); // offsets are backwards in POSIX notation - baseOffset = TZif_CalculateTransitionOffsetFromBase(baseOffset, timeZoneBaseUtcOffset); - - // having a daylightSavingsName means there is a DST rule - if (!daylightSavingsName.IsEmpty) - { - TimeSpan? parsedDaylightSavings = TZif_ParseOffsetString(daylightSavingsOffset); - TimeSpan daylightSavingsTimeSpan; - if (!parsedDaylightSavings.HasValue) - { - // default DST to 1 hour if it isn't specified - daylightSavingsTimeSpan = new TimeSpan(1, 0, 0); - } - else - { - daylightSavingsTimeSpan = parsedDaylightSavings.GetValueOrDefault().Negate(); // offsets are backwards in POSIX notation - daylightSavingsTimeSpan = TZif_CalculateTransitionOffsetFromBase(daylightSavingsTimeSpan, timeZoneBaseUtcOffset); - daylightSavingsTimeSpan = TZif_CalculateTransitionOffsetFromBase(daylightSavingsTimeSpan, baseOffset); - } - - TransitionTime? dstStart = TZif_CreateTransitionTimeFromPosixRule(start, startTime); - TransitionTime? dstEnd = TZif_CreateTransitionTimeFromPosixRule(end, endTime); - - if (dstStart == null || dstEnd == null) - { - return null; - } - - return AdjustmentRule.CreateAdjustmentRule( - startTransitionDate, - DateTime.MaxValue, - daylightSavingsTimeSpan, - dstStart.GetValueOrDefault(), - dstEnd.GetValueOrDefault(), - baseOffset, - noDaylightTransitions: false); - } - else - { - // if there is no daylightSavingsName, the whole AdjustmentRule should be with no transitions - just the baseOffset - return AdjustmentRule.CreateAdjustmentRule( - startTransitionDate, - DateTime.MaxValue, - TimeSpan.Zero, - default, - default, - baseOffset, - noDaylightTransitions: true); - } - } - } - - return null; - } - - private static TimeSpan? TZif_ParseOffsetString(ReadOnlySpan offset) - { - TimeSpan? result = null; - - if (offset.Length > 0) - { - bool negative = offset[0] == '-'; - if (negative || offset[0] == '+') - { - offset = offset.Slice(1); - } - - // Try parsing just hours first. - // Note, TimeSpan.TryParseExact "%h" can't be used here because some time zones using values - // like "26" or "144" and TimeSpan parsing would turn that into 26 or 144 *days* instead of hours. - int hours; - if (int.TryParse(offset, out hours)) - { - result = new TimeSpan(hours, 0, 0); - } - else - { - TimeSpan parsedTimeSpan; - if (TimeSpan.TryParseExact(offset, "g", CultureInfo.InvariantCulture, out parsedTimeSpan)) - { - result = parsedTimeSpan; - } - } - - if (result.HasValue && negative) - { - result = result.GetValueOrDefault().Negate(); - } - } - - return result; - } - - private static DateTime ParseTimeOfDay(ReadOnlySpan time) - { - DateTime timeOfDay; - TimeSpan? timeOffset = TZif_ParseOffsetString(time); - if (timeOffset.HasValue) - { - // This logic isn't correct and can't be corrected until https://github.com/dotnet/runtime/issues/14966 is fixed. - // Some time zones use time values like, "26", "144", or "-2". - // This allows the week to sometimes be week 4 and sometimes week 5 in the month. - // For now, strip off any 'days' in the offset, and just get the time of day correct - timeOffset = new TimeSpan(timeOffset.GetValueOrDefault().Hours, timeOffset.GetValueOrDefault().Minutes, timeOffset.GetValueOrDefault().Seconds); - if (timeOffset.GetValueOrDefault() < TimeSpan.Zero) - { - timeOfDay = new DateTime(1, 1, 2, 0, 0, 0); - } - else - { - timeOfDay = new DateTime(1, 1, 1, 0, 0, 0); - } - - timeOfDay += timeOffset.GetValueOrDefault(); - } - else - { - // default to 2AM. - timeOfDay = new DateTime(1, 1, 1, 2, 0, 0); - } - - return timeOfDay; - } - - private static TransitionTime? TZif_CreateTransitionTimeFromPosixRule(ReadOnlySpan date, ReadOnlySpan time) - { - if (date.IsEmpty) - { - return null; - } - - if (date[0] == 'M') - { - // Mm.w.d - // This specifies day d of week w of month m. The day d must be between 0(Sunday) and 6.The week w must be between 1 and 5; - // week 1 is the first week in which day d occurs, and week 5 specifies the last d day in the month. The month m should be between 1 and 12. - - int month; - int week; - DayOfWeek day; - if (!TZif_ParseMDateRule(date, out month, out week, out day)) - { - throw new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_UnparseablePosixMDateString, date.ToString())); - } - - return TransitionTime.CreateFloatingDateRule(ParseTimeOfDay(time), month, week, day); - } - else - { - if (date[0] != 'J') - { - // 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 - // the year is a leap year or not. - // - // In leap year, n would be counted as: - // - // 0 30 31 59 60 90 335 365 - // |-------Jan--------|-------Feb--------|-------Mar--------|....|-------Dec--------| - // - // while in non leap year we'll have - // - // 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. - // - // 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 (int.TryParse(date, out int julianDay) && julianDay < 59) - { - 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; - } - - // Julian day - TZif_ParseJulianDay(date, out int month, out int day); - return TransitionTime.CreateFixedDateRule(ParseTimeOfDay(time), month, day); - } - } - - /// - /// Parses a string like Jn into month and day values. - /// - private static void TZif_ParseJulianDay(ReadOnlySpan date, out int month, out int day) - { - // Jn - // This specifies the Julian day, with n between 1 and 365.February 29 is never counted, even in leap years. - Debug.Assert(!date.IsEmpty); - Debug.Assert(date[0] == 'J'); - month = day = 0; - - int index = 1; - - if (index >= date.Length || ((uint)(date[index] - '0') > '9'-'0')) - { - throw new InvalidTimeZoneException(SR.InvalidTimeZone_InvalidJulianDay); - } - - int julianDay = 0; - - do - { - julianDay = julianDay * 10 + (int) (date[index] - '0'); - index++; - } while (index < date.Length && ((uint)(date[index] - '0') <= '9'-'0')); - - int[] days = GregorianCalendarHelper.DaysToMonth365; - - if (julianDay == 0 || julianDay > days[days.Length - 1]) - { - throw new InvalidTimeZoneException(SR.InvalidTimeZone_InvalidJulianDay); - } - - int i = 1; - while (i < days.Length && julianDay > days[i]) - { - i++; - } - - Debug.Assert(i > 0 && i < days.Length); - - month = i; - day = julianDay - days[i - 1]; - } - - /// - /// Parses a string like Mm.w.d into month, week and DayOfWeek values. - /// - /// - /// true if the parsing succeeded; otherwise, false. - /// - private static bool TZif_ParseMDateRule(ReadOnlySpan dateRule, out int month, out int week, out DayOfWeek dayOfWeek) - { - if (dateRule[0] == 'M') - { - int monthWeekDotIndex = dateRule.IndexOf('.'); - if (monthWeekDotIndex > 0) - { - ReadOnlySpan weekDaySpan = dateRule.Slice(monthWeekDotIndex + 1); - int weekDayDotIndex = weekDaySpan.IndexOf('.'); - if (weekDayDotIndex > 0) - { - if (int.TryParse(dateRule.Slice(1, monthWeekDotIndex - 1), out month) && - int.TryParse(weekDaySpan.Slice(0, weekDayDotIndex), out week) && - int.TryParse(weekDaySpan.Slice(weekDayDotIndex + 1), out int day)) - { - dayOfWeek = (DayOfWeek)day; - return true; - } - } - } - } - - month = 0; - week = 0; - dayOfWeek = default; - return false; - } - - private static bool TZif_ParsePosixFormat( - ReadOnlySpan posixFormat, - out ReadOnlySpan standardName, - out ReadOnlySpan standardOffset, - out ReadOnlySpan daylightSavingsName, - out ReadOnlySpan daylightSavingsOffset, - out ReadOnlySpan start, - out ReadOnlySpan startTime, - out ReadOnlySpan end, - out ReadOnlySpan endTime) - { - standardName = null; - standardOffset = null; - daylightSavingsName = null; - daylightSavingsOffset = null; - start = null; - startTime = null; - end = null; - endTime = null; - - int index = 0; - standardName = TZif_ParsePosixName(posixFormat, ref index); - standardOffset = TZif_ParsePosixOffset(posixFormat, ref index); - - daylightSavingsName = TZif_ParsePosixName(posixFormat, ref index); - if (!daylightSavingsName.IsEmpty) - { - daylightSavingsOffset = TZif_ParsePosixOffset(posixFormat, ref index); - - if (index < posixFormat.Length && posixFormat[index] == ',') - { - index++; - TZif_ParsePosixDateTime(posixFormat, ref index, out start, out startTime); - - if (index < posixFormat.Length && posixFormat[index] == ',') - { - index++; - TZif_ParsePosixDateTime(posixFormat, ref index, out end, out endTime); - } - } - } - - return !standardName.IsEmpty && !standardOffset.IsEmpty; - } - - private static ReadOnlySpan TZif_ParsePosixName(ReadOnlySpan posixFormat, ref int index) - { - bool isBracketEnclosed = index < posixFormat.Length && posixFormat[index] == '<'; - if (isBracketEnclosed) - { - // move past the opening bracket - index++; - - ReadOnlySpan result = TZif_ParsePosixString(posixFormat, ref index, c => c == '>'); - - // move past the closing bracket - if (index < posixFormat.Length && posixFormat[index] == '>') - { - index++; - } - - return result; - } - else - { - return TZif_ParsePosixString( - posixFormat, - ref index, - c => char.IsDigit(c) || c == '+' || c == '-' || c == ','); - } - } - - private static ReadOnlySpan TZif_ParsePosixOffset(ReadOnlySpan posixFormat, ref int index) => - TZif_ParsePosixString(posixFormat, ref index, c => !char.IsDigit(c) && c != '+' && c != '-' && c != ':'); - - private static void TZif_ParsePosixDateTime(ReadOnlySpan posixFormat, ref int index, out ReadOnlySpan date, out ReadOnlySpan time) - { - time = null; - - date = TZif_ParsePosixDate(posixFormat, ref index); - if (index < posixFormat.Length && posixFormat[index] == '/') - { - index++; - time = TZif_ParsePosixTime(posixFormat, ref index); - } - } - - private static ReadOnlySpan TZif_ParsePosixDate(ReadOnlySpan posixFormat, ref int index) => - TZif_ParsePosixString(posixFormat, ref index, c => c == '/' || c == ','); - - private static ReadOnlySpan TZif_ParsePosixTime(ReadOnlySpan posixFormat, ref int index) => - TZif_ParsePosixString(posixFormat, ref index, c => c == ','); - - private static ReadOnlySpan TZif_ParsePosixString(ReadOnlySpan posixFormat, ref int index, Func breakCondition) - { - int startIndex = index; - for (; index < posixFormat.Length; index++) - { - char current = posixFormat[index]; - if (breakCondition(current)) - { - break; - } - } - - return posixFormat.Slice(startIndex, index - startIndex); - } - - // Returns the Substring from zoneAbbreviations starting at index and ending at '\0' - // zoneAbbreviations is expected to be in the form: "PST\0PDT\0PWT\0\PPT" - private static string TZif_GetZoneAbbreviation(string zoneAbbreviations, int index) - { - int lastIndex = zoneAbbreviations.IndexOf('\0', index); - return lastIndex > 0 ? - zoneAbbreviations.Substring(index, lastIndex - index) : - zoneAbbreviations.Substring(index); - } - - // Converts an array of bytes into an int - always using standard byte order (Big Endian) - // per TZif file standard - private static int TZif_ToInt32(byte[] value, int startIndex) - => BinaryPrimitives.ReadInt32BigEndian(value.AsSpan(startIndex)); - - // Converts an array of bytes into a long - always using standard byte order (Big Endian) - // per TZif file standard - private static long TZif_ToInt64(byte[] value, int startIndex) - => BinaryPrimitives.ReadInt64BigEndian(value.AsSpan(startIndex)); - - private static long TZif_ToUnixTime(byte[] value, int startIndex, TZVersion version) => - version != TZVersion.V1 ? - TZif_ToInt64(value, startIndex) : - TZif_ToInt32(value, startIndex); - - private static DateTime TZif_UnixTimeToDateTime(long unixTime) => - unixTime < DateTimeOffset.UnixMinSeconds ? DateTime.MinValue : - unixTime > DateTimeOffset.UnixMaxSeconds ? DateTime.MaxValue : - DateTimeOffset.FromUnixTimeSeconds(unixTime).UtcDateTime; - - private static void TZif_ParseRaw(byte[] data, out TZifHead t, out DateTime[] dts, out byte[] typeOfLocalTime, out TZifType[] transitionType, - out string zoneAbbreviations, out bool[] StandardTime, out bool[] GmtTime, out string? futureTransitionsPosixFormat) - { - // initialize the out parameters in case the TZifHead ctor throws - dts = null!; - typeOfLocalTime = null!; - transitionType = null!; - zoneAbbreviations = string.Empty; - StandardTime = null!; - GmtTime = null!; - futureTransitionsPosixFormat = null; - - // read in the 44-byte TZ header containing the count/length fields - // - int index = 0; - t = new TZifHead(data, index); - index += TZifHead.Length; - - int timeValuesLength = 4; // the first version uses 4-bytes to specify times - if (t.Version != TZVersion.V1) - { - // move index past the V1 information to read the V2 information - index += (int)((timeValuesLength * t.TimeCount) + t.TimeCount + (6 * t.TypeCount) + ((timeValuesLength + 4) * t.LeapCount) + t.IsStdCount + t.IsGmtCount + t.CharCount); - - // read the V2 header - t = new TZifHead(data, index); - index += TZifHead.Length; - timeValuesLength = 8; // the second version uses 8-bytes - } - - // initialize the containers for the rest of the TZ data - dts = new DateTime[t.TimeCount]; - typeOfLocalTime = new byte[t.TimeCount]; - transitionType = new TZifType[t.TypeCount]; - zoneAbbreviations = string.Empty; - StandardTime = new bool[t.TypeCount]; - GmtTime = new bool[t.TypeCount]; - - // read in the UTC transition points and convert them to Windows - // - for (int i = 0; i < t.TimeCount; i++) - { - long unixTime = TZif_ToUnixTime(data, index, t.Version); - dts[i] = TZif_UnixTimeToDateTime(unixTime); - index += timeValuesLength; - } - - // read in the Type Indices; there is a 1:1 mapping of UTC transition points to Type Indices - // these indices directly map to the array index in the transitionType array below - // - for (int i = 0; i < t.TimeCount; i++) - { - typeOfLocalTime[i] = data[index]; - index++; - } - - // read in the Type table. Each 6-byte entry represents - // {UtcOffset, IsDst, AbbreviationIndex} - // - // each AbbreviationIndex is a character index into the zoneAbbreviations string below - // - for (int i = 0; i < t.TypeCount; i++) - { - transitionType[i] = new TZifType(data, index); - index += 6; - } - - // read in the Abbreviation ASCII string. This string will be in the form: - // "PST\0PDT\0PWT\0\PPT" - // - Encoding enc = Encoding.UTF8; - zoneAbbreviations = enc.GetString(data, index, (int)t.CharCount); - index += (int)t.CharCount; - - // skip ahead of the Leap-Seconds Adjustment data. In a future release, consider adding - // support for Leap-Seconds - // - index += (int)(t.LeapCount * (timeValuesLength + 4)); // skip the leap second transition times - - // read in the Standard Time table. There should be a 1:1 mapping between Type-Index and Standard - // Time table entries. - // - // TRUE = transition time is standard time - // FALSE = transition time is wall clock time - // ABSENT = transition time is wall clock time - // - for (int i = 0; i < t.IsStdCount && i < t.TypeCount && index < data.Length; i++) - { - StandardTime[i] = (data[index++] != 0); - } - - // read in the GMT Time table. There should be a 1:1 mapping between Type-Index and GMT Time table - // entries. - // - // TRUE = transition time is UTC - // FALSE = transition time is local time - // ABSENT = transition time is local time - // - for (int i = 0; i < t.IsGmtCount && i < t.TypeCount && index < data.Length; i++) - { - GmtTime[i] = (data[index++] != 0); - } - - if (t.Version != TZVersion.V1) - { - // read the POSIX-style format, which should be wrapped in newlines with the last newline at the end of the file - if (data[index++] == '\n' && data[data.Length - 1] == '\n') - { - futureTransitionsPosixFormat = enc.GetString(data, index, data.Length - index - 1); - } - } - } - - /// - /// Normalize adjustment rule offset so that it is within valid range - /// This method should not be called at all but is here in case something changes in the future - /// or if really old time zones are present on the OS (no combination is known at the moment) - /// - private static void NormalizeAdjustmentRuleOffset(TimeSpan baseUtcOffset, [NotNull] ref AdjustmentRule adjustmentRule) - { - // Certain time zones such as: - // Time Zone start date end date offset - // ----------------------------------------------------- - // America/Yakutat 0001-01-01 1867-10-18 14:41:00 - // America/Yakutat 1867-10-18 1900-08-20 14:41:00 - // America/Sitka 0001-01-01 1867-10-18 14:58:00 - // America/Sitka 1867-10-18 1900-08-20 14:58:00 - // Asia/Manila 0001-01-01 1844-12-31 -15:56:00 - // Pacific/Guam 0001-01-01 1845-01-01 -14:21:00 - // Pacific/Saipan 0001-01-01 1845-01-01 -14:21:00 - // - // have larger offset than currently supported by framework. - // If for whatever reason we find that time zone exceeding max - // offset of 14h this function will truncate it to the max valid offset. - // Updating max offset may cause problems with interacting with SQL server - // which uses SQL DATETIMEOFFSET field type which was originally designed to be - // bit-for-bit compatible with DateTimeOffset. - - TimeSpan utcOffset = GetUtcOffset(baseUtcOffset, adjustmentRule); - - // utc base offset delta increment - TimeSpan adjustment = TimeSpan.Zero; - - if (utcOffset > MaxOffset) - { - adjustment = MaxOffset - utcOffset; - } - else if (utcOffset < MinOffset) - { - adjustment = MinOffset - utcOffset; - } - - if (adjustment != TimeSpan.Zero) - { - adjustmentRule = AdjustmentRule.CreateAdjustmentRule( - adjustmentRule.DateStart, - adjustmentRule.DateEnd, - adjustmentRule.DaylightDelta, - adjustmentRule.DaylightTransitionStart, - adjustmentRule.DaylightTransitionEnd, - adjustmentRule.BaseUtcOffsetDelta + adjustment, - adjustmentRule.NoDaylightTransitions); - } - } - - private struct TZifType - { - public const int Length = 6; - - public readonly TimeSpan UtcOffset; - public readonly bool IsDst; - public readonly byte AbbreviationIndex; - - public TZifType(byte[] data, int index) - { - if (data == null || data.Length < index + Length) - { - throw new ArgumentException(SR.Argument_TimeZoneInfoInvalidTZif, nameof(data)); - } - UtcOffset = new TimeSpan(0, 0, TZif_ToInt32(data, index + 00)); - IsDst = (data[index + 4] != 0); - AbbreviationIndex = data[index + 5]; - } - } - - private struct TZifHead - { - public const int Length = 44; - - public readonly uint Magic; // TZ_MAGIC "TZif" - public readonly TZVersion Version; // 1 byte for a \0 or 2 or 3 - // public byte[15] Reserved; // reserved for future use - public readonly uint IsGmtCount; // number of transition time flags - public readonly uint IsStdCount; // number of transition time flags - public readonly uint LeapCount; // number of leap seconds - public readonly uint TimeCount; // number of transition times - public readonly uint TypeCount; // number of local time types - public readonly uint CharCount; // number of abbreviated characters - - public TZifHead(byte[] data, int index) - { - if (data == null || data.Length < Length) - { - throw new ArgumentException("bad data", nameof(data)); - } - - Magic = (uint)TZif_ToInt32(data, index + 00); - - if (Magic != 0x545A6966) - { - // 0x545A6966 = {0x54, 0x5A, 0x69, 0x66} = "TZif" - throw new ArgumentException(SR.Argument_TimeZoneInfoBadTZif, nameof(data)); - } - - byte version = data[index + 04]; - Version = - version == '2' ? TZVersion.V2 : - version == '3' ? TZVersion.V3 : - TZVersion.V1; // default/fallback to V1 to guard against future, unsupported version numbers - - // skip the 15 byte reserved field - - // don't use the BitConverter class which parses data - // based on the Endianess of the machine architecture. - // this data is expected to always be in "standard byte order", - // regardless of the machine it is being processed on. - - IsGmtCount = (uint)TZif_ToInt32(data, index + 20); - IsStdCount = (uint)TZif_ToInt32(data, index + 24); - LeapCount = (uint)TZif_ToInt32(data, index + 28); - TimeCount = (uint)TZif_ToInt32(data, index + 32); - TypeCount = (uint)TZif_ToInt32(data, index + 36); - CharCount = (uint)TZif_ToInt32(data, index + 40); - } - } - - private enum TZVersion : byte - { - V1 = 0, - V2, - V3, - // when adding more versions, ensure all the logic using TZVersion is still correct - } - - // Helper function for string array search. (LINQ is not available here.) - private static bool StringArrayContains(string value, string[] source, StringComparison comparison) - { - foreach (string s in source) - { - if (string.Equals(s, value, comparison)) - { - return true; - } - } - - return false; - } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs index 866cff7fa21594..89844b5c11c1b1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs @@ -51,6 +51,7 @@ private enum TimeZoneInfoResult // constants for TimeZoneInfo.Local and TimeZoneInfo.Utc private const string UtcId = "UTC"; + private const string LocalId = "Local"; private static CachedData s_cachedData = new CachedData(); From cfa0b33d8b144168d562137dcf4b516c823bd584 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 25 Jun 2021 12:12:51 -0400 Subject: [PATCH 03/81] Differentiate Unix and Android TimeZoneInfo by PopulateAllSystemTimeZones --- .../src/System/TimeZoneInfo.Android.cs | 8 +------- .../src/System/TimeZoneInfo.AnyUnix.cs | 11 ----------- .../src/System/TimeZoneInfo.Unix.cs | 11 +++++++++++ 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 9df2a6de1706f3..8e3a4e2798e501 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -7,12 +7,6 @@ namespace System { public sealed partial class TimeZoneInfo { - // Mitchell - Why isn't this just instantiated in TimeZoneInfo.cs? - // private static readonly TimeZoneInfo s_utcTimeZone = CreateUtcTimeZone(); - - private static List GetTimeZoneIds(string timeZoneDirectory) - { - return new List(); - } + private static void PopulateAllSystemTimeZones(CachedData cachedData) {} } } \ No newline at end of file diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs index 9d3388d3b4db1d..986cc1d6ba4bb9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs @@ -225,17 +225,6 @@ public AdjustmentRule[] GetAdjustmentRules() return rulesList.ToArray(); } - private static void PopulateAllSystemTimeZones(CachedData cachedData) - { - Debug.Assert(Monitor.IsEntered(cachedData)); - - string timeZoneDirectory = GetTimeZoneDirectory(); - foreach (string timeZoneId in GetTimeZoneIds(timeZoneDirectory)) - { - TryGetTimeZone(timeZoneId, false, out _, out _, cachedData, alwaysFallbackToLocalMachine: true); // populate the cache - } - } - /// /// Helper function for retrieving the local system time zone. /// May throw COMException, TimeZoneNotFoundException, InvalidTimeZoneException. 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 18e507a8ef8609..d29fc64ae9cc7f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -11,6 +11,17 @@ public sealed partial class TimeZoneInfo { private const string ZoneTabFileName = "zone.tab"; + private static void PopulateAllSystemTimeZones(CachedData cachedData) + { + Debug.Assert(Monitor.IsEntered(cachedData)); + + string timeZoneDirectory = GetTimeZoneDirectory(); + foreach (string timeZoneId in GetTimeZoneIds(timeZoneDirectory)) + { + TryGetTimeZone(timeZoneId, false, out _, out _, cachedData, alwaysFallbackToLocalMachine: true); // populate the cache + } + } + /// /// Returns a collection of TimeZone Id values from the zone.tab file in the timeZoneDirectory. /// From caa39a66e02fd59c98d6b8ee948b142a65666a25 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Mon, 28 Jun 2021 13:03:36 -0400 Subject: [PATCH 04/81] Utilize Android paths for timezone data --- .../src/System/TimeZoneInfo.Android.cs | 43 +++++++++- .../src/System/TimeZoneInfo.AnyUnix.cs | 83 +++++++++++++++---- .../src/System/TimeZoneInfo.Unix.cs | 68 ++------------- 3 files changed, 118 insertions(+), 76 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 8e3a4e2798e501..84cfb1a79ab375 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -1,12 +1,53 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; +using System.IO; namespace System { public sealed partial class TimeZoneInfo { - private static void PopulateAllSystemTimeZones(CachedData cachedData) {} + private const string TimeZoneFileName = "tzdata"; + + private static string GetApexTimeDataRoot () + { + var ret = Environment.GetEnvironmentVariable ("ANDROID_TZDATA_ROOT"); + if (!string.IsNullOrEmpty(ret)) { + return ret; + } + + return "/apex/com.android.tzdata"; + } + + private static string GetApexRuntimeRoot () + { + var ret = Environment.GetEnvironmentVariable ("ANDROID_RUNTIME_ROOT"); + if (!string.IsNullOrEmpty (ret)) { + return ret; + } + + return "/apex/com.android.runtime"; + } + + internal static readonly string[] Paths = new string[]{ + GetApexTimeDataRoot () + "/etc/tz/", // Android 10+, TimeData module where the updates land + GetApexRuntimeRoot () + "/etc/tz/", // Android 10+, Fallback location if the above isn't found or corrupted + Environment.GetEnvironmentVariable ("ANDROID_DATA") + "/misc/zoneinfo/", + }; + + private static string GetTimeZoneDirectory() + { + foreach (var filePath in Paths) + { + if (File.Exists(Path.Combine(filePath, "TimeZoneFileName"))) + { + return filePath; + } + } + + return Environment.GetEnvironmentVariable ("ANDROID_ROOT") + DefaultTimeZoneDirectory; + } } } \ No newline at end of file diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs index 986cc1d6ba4bb9..ffe22f05c02566 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs @@ -18,7 +18,6 @@ public sealed partial class TimeZoneInfo { private const string DefaultTimeZoneDirectory = "/usr/share/zoneinfo/"; private const string TimeZoneEnvironmentVariable = "TZ"; - private const string TimeZoneDirectoryEnvironmentVariable = "TZDIR"; // UTC aliases per https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml // Hard-coded because we need to treat all aliases of UTC the same even when globalization data is not available. @@ -225,6 +224,17 @@ public AdjustmentRule[] GetAdjustmentRules() return rulesList.ToArray(); } + private static void PopulateAllSystemTimeZones(CachedData cachedData) + { + Debug.Assert(Monitor.IsEntered(cachedData)); + + string timeZoneDirectory = GetTimeZoneDirectory(); + foreach (string timeZoneId in GetTimeZoneIds(timeZoneDirectory)) + { + TryGetTimeZone(timeZoneId, false, out _, out _, cachedData, alwaysFallbackToLocalMachine: true); // populate the cache + } + } + /// /// Helper function for retrieving the local system time zone. /// May throw COMException, TimeZoneNotFoundException, InvalidTimeZoneException. @@ -283,6 +293,62 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out return TimeZoneInfoResult.Success; } + /// + /// Returns a collection of TimeZone Id values from the time zone file in the timeZoneDirectory. + /// + /// + /// Lines that start with # are comments and are skipped. + /// + private static List GetTimeZoneIds(string timeZoneDirectory) + { + List timeZoneIds = new List(); + + try + { + using (StreamReader sr = new StreamReader(Path.Combine(timeZoneDirectory, TimeZoneFileName), Encoding.UTF8)) + { + string? zoneTabFileLine; + while ((zoneTabFileLine = sr.ReadLine()) != null) + { + if (!string.IsNullOrEmpty(zoneTabFileLine) && zoneTabFileLine[0] != '#') + { + // the format of the line is "country-code \t coordinates \t TimeZone Id \t comments" + + int firstTabIndex = zoneTabFileLine.IndexOf('\t'); + if (firstTabIndex != -1) + { + int secondTabIndex = zoneTabFileLine.IndexOf('\t', firstTabIndex + 1); + if (secondTabIndex != -1) + { + string timeZoneId; + int startIndex = secondTabIndex + 1; + int thirdTabIndex = zoneTabFileLine.IndexOf('\t', startIndex); + if (thirdTabIndex != -1) + { + int length = thirdTabIndex - startIndex; + timeZoneId = zoneTabFileLine.Substring(startIndex, length); + } + else + { + timeZoneId = zoneTabFileLine.Substring(startIndex); + } + + if (!string.IsNullOrEmpty(timeZoneId)) + { + timeZoneIds.Add(timeZoneId); + } + } + } + } + } + } + } + catch (IOException) { } + catch (UnauthorizedAccessException) { } + + return timeZoneIds; + } + /// /// Gets the tzfile raw data for the current 'local' time zone using the following rules. /// 1. Read the TZ environment variable. If it is set, use it. @@ -627,21 +693,6 @@ private static TimeZoneInfo GetLocalTimeZoneFromTzFile() return null; } - private static string GetTimeZoneDirectory() - { - string? tzDirectory = Environment.GetEnvironmentVariable(TimeZoneDirectoryEnvironmentVariable); - - if (tzDirectory == null) - { - tzDirectory = DefaultTimeZoneDirectory; - } - else if (!tzDirectory.EndsWith(Path.DirectorySeparatorChar)) - { - tzDirectory += PathInternal.DirectorySeparatorCharAsString; - } - - return tzDirectory; - } /// /// Helper function for retrieving a TimeZoneInfo object by time_zone_name. 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 d29fc64ae9cc7f..595143b0384afc 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -9,73 +9,23 @@ namespace System { public sealed partial class TimeZoneInfo { - private const string ZoneTabFileName = "zone.tab"; + private const string TimeZoneFileName = "zone.tab"; + private const string TimeZoneDirectoryEnvironmentVariable = "TZDIR"; - private static void PopulateAllSystemTimeZones(CachedData cachedData) + private static string GetTimeZoneDirectory() { - Debug.Assert(Monitor.IsEntered(cachedData)); + string? tzDirectory = Environment.GetEnvironmentVariable(TimeZoneDirectoryEnvironmentVariable); - string timeZoneDirectory = GetTimeZoneDirectory(); - foreach (string timeZoneId in GetTimeZoneIds(timeZoneDirectory)) + if (tzDirectory == null) { - TryGetTimeZone(timeZoneId, false, out _, out _, cachedData, alwaysFallbackToLocalMachine: true); // populate the cache + tzDirectory = DefaultTimeZoneDirectory; } - } - - /// - /// Returns a collection of TimeZone Id values from the zone.tab file in the timeZoneDirectory. - /// - /// - /// Lines that start with # are comments and are skipped. - /// - private static List GetTimeZoneIds(string timeZoneDirectory) - { - List timeZoneIds = new List(); - - try + else if (!tzDirectory.EndsWith(Path.DirectorySeparatorChar)) { - using (StreamReader sr = new StreamReader(Path.Combine(timeZoneDirectory, ZoneTabFileName), Encoding.UTF8)) - { - string? zoneTabFileLine; - while ((zoneTabFileLine = sr.ReadLine()) != null) - { - if (!string.IsNullOrEmpty(zoneTabFileLine) && zoneTabFileLine[0] != '#') - { - // the format of the line is "country-code \t coordinates \t TimeZone Id \t comments" - - int firstTabIndex = zoneTabFileLine.IndexOf('\t'); - if (firstTabIndex != -1) - { - int secondTabIndex = zoneTabFileLine.IndexOf('\t', firstTabIndex + 1); - if (secondTabIndex != -1) - { - string timeZoneId; - int startIndex = secondTabIndex + 1; - int thirdTabIndex = zoneTabFileLine.IndexOf('\t', startIndex); - if (thirdTabIndex != -1) - { - int length = thirdTabIndex - startIndex; - timeZoneId = zoneTabFileLine.Substring(startIndex, length); - } - else - { - timeZoneId = zoneTabFileLine.Substring(startIndex); - } - - if (!string.IsNullOrEmpty(timeZoneId)) - { - timeZoneIds.Add(timeZoneId); - } - } - } - } - } - } + tzDirectory += PathInternal.DirectorySeparatorCharAsString; } - catch (IOException) { } - catch (UnauthorizedAccessException) { } - return timeZoneIds; + return tzDirectory; } } } From ee51c67cb7d063d0b93a2e22880421b4cd5d26ce Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Mon, 28 Jun 2021 14:15:38 -0400 Subject: [PATCH 05/81] Clean up mono style to match runtime style --- .../src/System/TimeZoneInfo.Android.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 84cfb1a79ab375..ead25709aeff32 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -11,31 +11,31 @@ public sealed partial class TimeZoneInfo { private const string TimeZoneFileName = "tzdata"; - private static string GetApexTimeDataRoot () + private static string GetApexTimeDataRoot() { - var ret = Environment.GetEnvironmentVariable ("ANDROID_TZDATA_ROOT"); - if (!string.IsNullOrEmpty(ret)) { + var ret = Environment.GetEnvironmentVariable("ANDROID_TZDATA_ROOT"); + if (!string.IsNullOrEmpty(ret)) + { return ret; } return "/apex/com.android.tzdata"; } - private static string GetApexRuntimeRoot () + private static string GetApexRuntimeRoot() { - var ret = Environment.GetEnvironmentVariable ("ANDROID_RUNTIME_ROOT"); - if (!string.IsNullOrEmpty (ret)) { + var ret = Environment.GetEnvironmentVariable("ANDROID_RUNTIME_ROOT"); + if (!string.IsNullOrEmpty(ret)) + { return ret; } return "/apex/com.android.runtime"; } - internal static readonly string[] Paths = new string[]{ - GetApexTimeDataRoot () + "/etc/tz/", // Android 10+, TimeData module where the updates land - GetApexRuntimeRoot () + "/etc/tz/", // Android 10+, Fallback location if the above isn't found or corrupted - Environment.GetEnvironmentVariable ("ANDROID_DATA") + "/misc/zoneinfo/", - }; + internal static readonly string[] Paths = new string[] { GetApexTimeDataRoot() + "/etc/tz/", // Android 10+, TimeData module where the updates land + GetApexRuntimeRoot() + "/etc/tz/", // Android 10+, Fallback location if the above isn't found or corrupted + Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/",}; private static string GetTimeZoneDirectory() { @@ -47,7 +47,7 @@ private static string GetTimeZoneDirectory() } } - return Environment.GetEnvironmentVariable ("ANDROID_ROOT") + DefaultTimeZoneDirectory; + return Environment.GetEnvironmentVariable("ANDROID_ROOT") + DefaultTimeZoneDirectory; } } } \ No newline at end of file From ce916115eac250ceea63ef073ff51cc6f17aa4e3 Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Tue, 29 Jun 2021 10:43:46 -0400 Subject: [PATCH 06/81] Pulled in parsing implementation from mono and broke out GetLocalTimeZone into separate Unix and Android functions. --- .../src/System/TimeZoneInfo.Android.cs | 658 +++++++++++++++++- .../src/System/TimeZoneInfo.AnyUnix.cs | 26 +- .../src/System/TimeZoneInfo.Unix.cs | 29 + 3 files changed, 687 insertions(+), 26 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index ead25709aeff32..11c394cd91ccfb 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -9,8 +9,17 @@ namespace System { public sealed partial class TimeZoneInfo { - private const string TimeZoneFileName = "tzdata"; + // TODO: Consider restructuring underlying AndroidTimeZones class. + // Although it may be easier to work with this way. + private static TimeZone GetLocalTimeZoneCore() + { + return AndroidTimeZones.Local; + } + // TODO: Validate you still need these functions / fields. We should try to isolate the android implementation + // as much as possible. + // In other words, mirroring how mono/mono did it is a good first step and then we can walk back what's + // common with TimeZoneInfo.cs and TimeZoneInfo.AnyUnix.cs private static string GetApexTimeDataRoot() { var ret = Environment.GetEnvironmentVariable("ANDROID_TZDATA_ROOT"); @@ -49,5 +58,652 @@ private static string GetTimeZoneDirectory() return Environment.GetEnvironmentVariable("ANDROID_ROOT") + DefaultTimeZoneDirectory; } + + static class AndroidTimeZones + { + private static IAndroidTimeZoneDB db; + + static AndroidTimeZones() + { + db = GetDefaultTimeZoneDB(); + } + + private static IAndroidTimeZoneDB GetDefaultTimeZoneDB() + { + foreach (var p in AndroidTzData.Paths) + { + if (File.Exists (p)) + { + return new AndroidTzData(AndroidTzData.Paths); + } + } + if (Directory.Exists (ZoneInfoDB.ZoneDirectoryName)) + { + return new ZoneInfoDB(); + } + return null; + } + + internal static IEnumerable GetAvailableIds() + { + return db == null + ? new string[0] + : db.GetAvailableIds(); + } + + private static TimeZoneInfo _GetTimeZone(string id, string name) + { + if (db == null) + return null; + byte[] buffer = db.GetTimeZoneData(name); + if (buffer == null) + return null; + + // TODO: See if we needd to port this function or can use something in TZ + return TimeZoneInfo.ParseTZBuffer(id, buffer, buffer.Length); + } + + internal static TimeZoneInfo GetTimeZone (string id, string name) + { + if (name != null) + { + if (name == "GMT" || name == "UTC") + { + return new TimeZoneInfo(id, TimeSpan.FromSeconds(0), id, name, name, null, disableDaylightSavingTime:true); + } + if (name.StartsWith ("GMT")) + { + return new TimeZoneInfo (id, + TimeSpan.FromSeconds(ParseNumericZone(name)), + id, name, name, null, disableDaylightSavingTime:true); + } + } + + try + { + return _GetTimeZone(id, name); + } catch (Exception) + { + return null; + } + } + + static int ParseNumericZone (string name) + { + if (name == null || !name.StartsWith ("GMT") || name.Length <= 3) + return 0; + + int sign; + if (name [3] == '+') + sign = 1; + else if (name [3] == '-') + sign = -1; + else + return 0; + + int where; + int hour = 0; + bool colon = false; + for (where = 4; where < name.Length; where++) + { + char c = name [where]; + + if (c == ':') + { + where++; + colon = true; + break; + } + + if (c >= '0' && c <= '9') + hour = hour * 10 + c - '0'; + else + return 0; + } + + int min = 0; + for (; where < name.Length; where++) + { + char c = name [where]; + + if (c >= '0' && c <= '9') + min = min * 10 + c - '0'; + else + return 0; + } + + if (colon) + return sign * (hour * 60 + min) * 60; + else if (hour >= 100) + return sign * ((hour / 100) * 60 + (hour % 100)) * 60; + else + return sign * (hour * 60) * 60; + } + + internal static TimeZoneInfo Local + { + get + { + var id = GetDefaultTimeZoneName(); + return GetTimeZone(id, id); + } + } + + // TODO: We probably don't need this. However, if we do, move to Interop + // + //[DllImport ("__Internal")] + //static extern int monodroid_get_system_property (string name, ref IntPtr value); + + // TODO: Move this into Interop + // + //[DllImport ("__Internal")] + //static extern void monodroid_free (IntPtr ptr); + + static string GetDefaultTimeZoneName() + { + IntPtr value = IntPtr.Zero; + int n = 0; + string defaultTimeZone = Environment.GetEnvironmentVariable("__XA_OVERRIDE_TIMEZONE_ID__"); + + if (!string.IsNullOrEmpty(defaultTimeZone)) + return defaultTimeZone; + + // TODO: See how we can test this without hacks + // Used by the tests + //if (Environment.GetEnvironmentVariable ("__XA_USE_JAVA_DEFAULT_TIMEZONE_ID__") == null) + // n = monodroid_get_system_property ("persist.sys.timezone", ref value); + + if (n > 0 && value != IntPtr.Zero) + { + defaultTimeZone = (Marshal.PtrToStringAnsi(value) ?? String.Empty).Trim(); + monodroid_free(value); + if (!String.IsNullOrEmpty(defaultTimeZone)) + return defaultTimeZone; + } + + // TODO: AndroidPlatform does not exist in runtime. We need to add an interop call + defaultTimeZone = (AndroidPlatform.GetDefaultTimeZone() ?? String.Empty).Trim(); + if (!String.IsNullOrEmpty(defaultTimeZone)) + return defaultTimeZone; + + return null; + } + } + } + + interface IAndroidTimeZoneDB + { + IEnumerable GetAvailableIds(); + byte[] GetTimeZoneData(string id); + } + + [StructLayout(LayoutKind.Sequential, Pack=1)] + unsafe struct AndroidTzDataHeader + { + public fixed byte signature [12]; + public int indexOffset; + public int dataOffset; + public int zoneTabOffset; + } + + [StructLayout(LayoutKind.Sequential, Pack=1)] + unsafe struct AndroidTzDataEntry + { + public fixed byte id [40]; + public int byteOffset; + public int length; + public int rawUtcOffset; + } + + /* + * Android v4.3 Timezone support infrastructure. + * + * This is a C# port of libcore.util.ZoneInfoDB: + * + * https://android.googlesource.com/platform/libcore/+/master/luni/src/main/java/libcore/util/ZoneInfoDB.java + * + * This is needed in order to read Android v4.3 tzdata files. + * + * Android 10+ moved the up-to-date tzdata location to a module updatable via the Google Play Store and the + * database location changed (https://source.android.com/devices/architecture/modular-system/runtime#time-zone-data-interactions) + * The older locations still exist (at least the `/system/usr/share/zoneinfo` one) but they won't be updated. + */ + sealed class AndroidTzData : IAndroidTimeZoneDB + { + + internal static readonly string[] Paths = new string[] { + GetApexTimeDataRoot() + "/etc/tz/tzdata", // Android 10+, TimeData module where the updates land + GetApexRuntimeRoot() + "/etc/tz/tzdata", // Android 10+, Fallback location if the above isn't found or corrupted + Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/tzdata", + Environment.GetEnvironmentVariable("ANDROID_ROOT") + "/usr/share/zoneinfo/tzdata", + }; + + private string zdataPath; + private Stream data; + private string version; + private string zoneTab; + + private string[] ids; + private int[] byteOffsets; + private int[] lengths; + + public AndroidTzData (params string[] paths) + { + foreach(var path in paths) + { + if (LoadData(path)) + { + tzdataPath = path; + return; + } + } + + tzdataPath = "/"; + version = "missing"; + zoneTab = "# Emergency fallback data.\n"; + ids = new[]{ "GMT" }; + } + + public string Version => version; + + public string ZoneTab => zoneTab; + + static string GetApexTimeDataRoot () + { + string ret = Environment.GetEnvironmentVariable ("ANDROID_TZDATA_ROOT"); + if (!String.IsNullOrEmpty (ret)) { + return ret; + } + + return "/apex/com.android.tzdata"; + } + + static string GetApexRuntimeRoot() + { + string ret = Environment.GetEnvironmentVariable ("ANDROID_RUNTIME_ROOT"); + if (!String.IsNullOrEmpty (ret)) + { + return ret; + } + + return "/apex/com.android.runtime"; + } + + bool LoadData(string path) + { + if (!File.Exists(path)) + return false; + + try + { + data = File.OpenRead(path); + } + catch (IOException) + { + return false; + } + catch (UnauthorizedAccessException) + { + return false; + } + + try + { + ReadHeader(); + return true; + } + catch (Exception e) + { + // log something here instead of the console. + //Console.Error.WriteLine ("tzdata file \"{0}\" was present but invalid: {1}", path, e); + } + return false; + } + + unsafe void ReadHeader() + { + int size = Math.Max(Marshal.SizeOf(typeof(AndroidTzDataHeader)), Marshal.SizeOf(typeof(AndroidTzDataEntry))); + var buffer = new byte[size]; + var header = ReadAt(0, buffer); + + header.indexOffset = NetworkToHostOrder(header.indexOffset); + header.dataOffset = NetworkToHostOrder(header.dataOffset); + header.zoneTabOffset = NetworkToHostOrder(header.zoneTabOffset); + + sbyte* s = (sbyte*)header.signature; + string magic = new string(s, 0, 6, Encoding.ASCII); + + if (magic != "tzdata" || header.signature[11] != 0) + { + var b = new StringBuilder (); + b.Append ("bad tzdata magic:"); + for (int i = 0; i < 12; ++i) { + b.Append(" ").Append(((byte)s[i]).ToString ("x2")); + } + + //TODO: Put strings in resource file + throw new InvalidOperationException ("bad tzdata magic: " + b.ToString ()); + } + + version = new string(s, 6, 5, Encoding.ASCII); + + ReadIndex(header.indexOffset, header.dataOffset, buffer); + ReadZoneTab(header.zoneTabOffset, checked((int)data.Length) - header.zoneTabOffset); + } + + unsafe T ReadAt (long position, byte[] buffer) + where T : struct + { + int size = Marshal.SizeOf(typeof(T)); + if (buffer.Length < size) + { + //TODO: Put strings in resource file + throw new InvalidOperationException ("Internal error: buffer too small"); + } + + data.Position = position; + int r; + if ((r = data.Read(buffer, 0, size)) < size) + { + //TODO: Put strings in resource file + throw new InvalidOperationException ( + string.Format ("Error reading '{0}': read {1} bytes, expected {2}", tzdataPath, r, size)); + } + + fixed (byte* b = buffer) + { + return (T)Marshal.PtrToStructure((IntPtr)b, typeof(T)); + } + } + + static int NetworkToHostOrder(int value) + { + if (!BitConverter.IsLittleEndian) + return value; + + return + (((value >> 24) & 0xFF) | + ((value >> 08) & 0xFF00) | + ((value << 08) & 0xFF0000) | + ((value << 24))); + } + + unsafe void ReadIndex(int indexOffset, int dataOffset, byte[] buffer) + { + int indexSize = dataOffset - indexOffset; + int entryCount = indexSize / Marshal.SizeOf(typeof (AndroidTzDataEntry)); + int entrySize = Marshal.SizeOf(typeof (AndroidTzDataEntry)); + + byteOffsets = new int [entryCount]; + ids = new string [entryCount]; + lengths = new int [entryCount]; + + for (int i = 0; i < entryCount; ++i) + { + var entry = ReadAt(indexOffset + (entrySize*i), buffer); + var p = (sbyte*)entry.id; + + byteOffsets[i] = NetworkToHostOrder(entry.byteOffset) + dataOffset; + ids[i] = new string(p, 0, GetStringLength(p, 40), Encoding.ASCII); + lengths[i] = NetworkToHostOrder(entry.length); + + if (lengths[i] < Marshal.SizeOf(typeof(AndroidTzDataHeader))) + { + //TODO: Put strings in resource file + throw new InvalidOperationException("Length in index file < sizeof(tzhead)"); + } + } + } + + static unsafe int GetStringLength(sbyte* s, int maxLength) + { + int len; + for (len = 0; len < maxLength; len++, s++) + { + if (*s == 0) + break; + } + return len; + } + + unsafe void ReadZoneTab(int zoneTabOffset, int zoneTabSize) + { + byte[] ztab = new byte [zoneTabSize]; + + data.Position = zoneTabOffset; + + int r; + if ((r = data.Read(ztab, 0, ztab.Length)) < ztab.Length) + { + //TODO: Put strings in resource file + throw new InvalidOperationException( + string.Format ("Error reading zonetab: read {0} bytes, expected {1}", r, zoneTabSize)); + } + + zoneTab = Encoding.ASCII.GetString(ztab, 0, ztab.Length); + } + + public IEnumerable GetAvailableIds() + { + return ids; + } + + public byte[] GetTimeZoneData(string id) + { + int i = Array.BinarySearch(ids, id, StringComparer.Ordinal); + if (i < 0) + return null; + + int offset = byteOffsets[i]; + int length = lengths[i]; + var buffer = new byte[length]; + + lock (data) + { + data.Position = offset; + int r; + if ((r = data.Read(buffer, 0, buffer.Length)) < buffer.Length) + { + //TODO: Put strings in resource file + throw new InvalidOperationException( + string.Format ("Unable to fully read from file '{0}' at offset {1} length {2}; read {3} bytes expected {4}.", + tzdataPath, offset, length, r, buffer.Length)); + } + } + + return buffer; + } + } + + /* + * Android < v4.3 Timezone support infrastructure. + * + * This is a C# port of org.apache.harmony.luni.internal.util.ZoneInfoDB: + * + * http://android.git.kernel.org/?p=platform/libcore.git;a=blob;f=luni/src/main/java/org/apache/harmony/luni/internal/util/ZoneInfoDB.java;h=3e7bdc3a952b24da535806d434a3a27690feae26;hb=HEAD + * + * From the ZoneInfoDB source: + * + * However, to conserve disk space the data for all time zones are + * concatenated into a single file, and a second file is used to indicate + * the starting position of each time zone record. A third file indicates + * the version of the zoneinfo databse used to generate the data. + * + * which succinctly describes why we can't just use the LIBC implementation in + * TimeZoneInfo.cs -- the "standard Unixy" directory structure is NOT used. + */ + sealed class ZoneInfoDB : IAndroidTimeZoneDB + { + private const int TimeZoneNameLength = 40; + private const int TimeZoneIntSize = 4; + + internal static readonly string ZoneDirectoryName = Environment.GetEnvironmentVariable ("ANDROID_ROOT") + "/usr/share/zoneinfo/"; + + private const string ZoneFileName = "zoneinfo.dat"; + private const string IndexFileName = "zoneinfo.idx"; + private const string DefaultVersion = "2007h"; + private const string VersionFileName = "zoneinfo.version"; + + private readonly string zoneRoot; + private readonly string version; + private readonly string[] names; + private readonly int[] starts; + private readonly int[] lengths; + private readonly int[] offsets; + + public ZoneInfoDB(string zoneInfoDB = null) + { + zoneRoot = zoneInfoDB ?? ZoneDirectoryName; + try + { + version = ReadVersion(Path.Combine(zoneRoot, VersionFileName)); + } + catch + { + version = DefaultVersion; + } + + try + { + ReadDatabase(Path.Combine(zoneRoot, IndexFileName), out names, out starts, out lengths, out offsets); + } + catch + { + names = new string [0]; + starts = new int [0]; + lengths = new int [0]; + offsets = new int [0]; + } + } + + static string ReadVersion(string path) + { + using (var file = new StreamReader(path, Encoding.GetEncoding("iso-8859-1"))) + { + return file.ReadToEnd().Trim(); + } + } + + void ReadDatabase(string path, out string[] names, out int[] starts, out int[] lengths, out int[] offsets) + { + using (var file = File.OpenRead (path)) + { + var nbuf = new byte[TimeZoneNameLength]; + + int numEntries = (int)(file.Length / (TimeZoneNameLength + 3*TimeZoneIntSize)); + + char[] namebuf = new char[TimeZoneNameLength]; + + names = new string[numEntries]; + starts = new int[numEntries]; + lengths = new int[numEntries]; + offsets = new int[numEntries]; + + for (int i = 0; i < numEntries; ++i) + { + Fill(file, nbuf, nbuf.Length); + int namelen; + for (namelen = 0; namelen < nbuf.Length; ++namelen) + { + if (nbuf[namelen] == '\0') + break; + namebuf [namelen] = (char)(nbuf[namelen] & 0xFF); + } + + names[i] = new string(namebuf, 0, namelen); + starts[i] = ReadInt32(file, nbuf); + lengths[i] = ReadInt32(file, nbuf); + offsets[i] = ReadInt32(file, nbuf); + } + } + } + + static void Fill (Stream stream, byte[] nbuf, int required) + { + int read = 0, offset = 0; + while (offset < required && (read = stream.Read(nbuf, offset, required - offset)) > 0) + offset += read; + + if (read != required) + { + //TODO: Put strings in resource file + throw new EndOfStreamException("Needed to read " + required + " bytes; read " + read + " bytes"); + } + } + + // From java.io.RandomAccessFioe.readInt(), as we need to use the same + // byte ordering as Java uses. + static int ReadInt32(Stream stream, byte[] nbuf) + { + Fill(stream, nbuf, 4); + return ((nbuf[0] & 0xff) << 24) + ((nbuf[1] & 0xff) << 16) + + ((nbuf[2] & 0xff) << 8) + (nbuf[3] & 0xff); + } + + internal string Version => version; + + public IEnumerable GetAvailableIds() + { + return GetAvailableIds(0, false); + } + + IEnumerable GetAvailableIds(int rawOffset) + { + return GetAvailableIds(rawOffset, true); + } + + IEnumerable GetAvailableIds(int rawOffset, bool checkOffset) + { + for (int i = 0; i < offsets.Length; ++i) + { + if (!checkOffset || offsets[i] == rawOffset) + yield return names [i]; + } + } + + public byte[] GetTimeZoneData(string id) + { + int start, length; + using (var stream = GetTimeZoneData(id, out start, out length)) { + if (stream == null) + return null; + byte[] buf = new byte[length]; + Fill(stream, buf, buf.Length); + return buf; + } + } + + FileStream GetTimeZoneData(string name, out int start, out int length) + { + // Just in case, to avoid NREX as in xambug #4902 + if (name == null) + { + start = 0; + length = 0; + return null; + } + + var f = new FileInfo(Path.Combine(zoneRoot, name)); + if (f.Exists) + { + start = 0; + length = (int)f.Length; + return f.OpenRead(); + } + + start = length = 0; + + int i = Array.BinarySearch(names, name, StringComparer.Ordinal); + if (i < 0) + return null; + + start = starts[i]; + length = lengths[i]; + + var stream = File.OpenRead(Path.Combine(zoneRoot, ZoneFileName)); + stream.Seek(start, SeekOrigin.Begin); + + return stream; + } } } \ No newline at end of file diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs index ffe22f05c02566..992adf1c4e8f9b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs @@ -245,8 +245,7 @@ private static TimeZoneInfo GetLocalTimeZone(CachedData cachedData) { Debug.Assert(Monitor.IsEntered(cachedData)); - // Without Registry support, create the TimeZoneInfo from a TZ file - return GetLocalTimeZoneFromTzFile(); + return GetLocalTimeZoneCore(cachedData); } private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out TimeZoneInfo? value, out Exception? e) @@ -649,29 +648,6 @@ private static bool CompareTimeZoneFile(string filePath, byte[] buffer, byte[] r return false; } - /// - /// Helper function used by 'GetLocalTimeZone()' - this function wraps the call - /// for loading time zone data from computers without Registry support. - /// - /// The TryGetLocalTzFile() call returns a Byte[] containing the compiled tzfile. - /// - private static TimeZoneInfo GetLocalTimeZoneFromTzFile() - { - byte[]? rawData; - string? id; - if (TryGetLocalTzFile(out rawData, out id)) - { - TimeZoneInfo? result = GetTimeZoneFromTzData(rawData, id); - if (result != null) - { - return result; - } - } - - // if we can't find a local time zone, return UTC - return Utc; - } - private static TimeZoneInfo? GetTimeZoneFromTzData(byte[]? rawData, string id) { if (rawData != null) 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 595143b0384afc..9a6553254ff982 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -12,6 +12,35 @@ public sealed partial class TimeZoneInfo private const string TimeZoneFileName = "zone.tab"; private const string TimeZoneDirectoryEnvironmentVariable = "TZDIR"; + private static TimeZone GetLocalTimeZoneCore() + { + // Without Registry support, create the TimeZoneInfo from a TZ file + return GetLocalTimeZoneFromTzFile(); + } + + /// + /// Helper function used by 'GetLocalTimeZone()' - this function wraps the call + /// for loading time zone data from computers without Registry support. + /// + /// The TryGetLocalTzFile() call returns a Byte[] containing the compiled tzfile. + /// + private static TimeZoneInfo GetLocalTimeZoneFromTzFile() + { + byte[]? rawData; + string? id; + if (TryGetLocalTzFile(out rawData, out id)) + { + TimeZoneInfo? result = GetTimeZoneFromTzData(rawData, id); + if (result != null) + { + return result; + } + } + + // if we can't find a local time zone, return UTC + return Utc; + } + private static string GetTimeZoneDirectory() { string? tzDirectory = Environment.GetEnvironmentVariable(TimeZoneDirectoryEnvironmentVariable); From 4ed861603051eed0791dd8ba4bec01c84495286b Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Tue, 29 Jun 2021 16:32:06 -0400 Subject: [PATCH 07/81] TimeZoneInfo.Android builds properly now. Found PopulateAllSystemTimeZones to map to a method in the mono port --- .../src/System/TimeZoneInfo.Android.cs | 403 +++++------------- .../src/System/TimeZoneInfo.AnyUnix.cs | 64 +-- .../src/System/TimeZoneInfo.Unix.cs | 65 +++ 3 files changed, 179 insertions(+), 353 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 11c394cd91ccfb..856e0ccaaf4459 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -3,22 +3,37 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Runtime.InteropServices; +using System.Text; namespace System { public sealed partial class TimeZoneInfo { - // TODO: Consider restructuring underlying AndroidTimeZones class. + // TODO: Consider restructuring underlying AndroidTimeZones class©. // Although it may be easier to work with this way. - private static TimeZone GetLocalTimeZoneCore() + private static TimeZoneInfo GetLocalTimeZoneCore() { - return AndroidTimeZones.Local; + return AndroidTimeZones.Local!; } - // TODO: Validate you still need these functions / fields. We should try to isolate the android implementation + //TODO: PopulateAllSystemTimeZones maps to GetSystemTimeZonesCore in mono/mono implementation + private static void PopulateAllSystemTimeZonesCore() + { + + } + + //TODO: Figure out if this maps to something in the other TimeZoneInfo files + private static TimeZoneInfo? ParseTZBuffer(string? id, byte[] buffer, int length) + { + throw new NotImplementedException("ParseTZBuffer has not been implemented yet"); + } + + // TODO: Validate you still need these functions / fields. We should try to isolate the android implementation // as much as possible. - // In other words, mirroring how mono/mono did it is a good first step and then we can walk back what's + // In other words, mirroring how mono/mono did it is a good first step and then we can walk back what's // common with TimeZoneInfo.cs and TimeZoneInfo.AnyUnix.cs private static string GetApexTimeDataRoot() { @@ -44,7 +59,7 @@ private static string GetApexRuntimeRoot() internal static readonly string[] Paths = new string[] { GetApexTimeDataRoot() + "/etc/tz/", // Android 10+, TimeData module where the updates land GetApexRuntimeRoot() + "/etc/tz/", // Android 10+, Fallback location if the above isn't found or corrupted - Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/",}; + Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/", }; private static string GetTimeZoneDirectory() { @@ -59,16 +74,11 @@ private static string GetTimeZoneDirectory() return Environment.GetEnvironmentVariable("ANDROID_ROOT") + DefaultTimeZoneDirectory; } - static class AndroidTimeZones + private static class AndroidTimeZones { - private static IAndroidTimeZoneDB db; + private static IAndroidTimeZoneDB? db = GetDefaultTimeZoneDB(); - static AndroidTimeZones() - { - db = GetDefaultTimeZoneDB(); - } - - private static IAndroidTimeZoneDB GetDefaultTimeZoneDB() + private static IAndroidTimeZoneDB? GetDefaultTimeZoneDB() { foreach (var p in AndroidTzData.Paths) { @@ -77,21 +87,18 @@ private static IAndroidTimeZoneDB GetDefaultTimeZoneDB() return new AndroidTzData(AndroidTzData.Paths); } } - if (Directory.Exists (ZoneInfoDB.ZoneDirectoryName)) - { - return new ZoneInfoDB(); - } + //TODO: What should we throw here? return null; } internal static IEnumerable GetAvailableIds() { return db == null - ? new string[0] + ? Array.Empty() : db.GetAvailableIds(); } - private static TimeZoneInfo _GetTimeZone(string id, string name) + private static TimeZoneInfo? _GetTimeZone(string? id, string? name) { if (db == null) return null; @@ -103,19 +110,19 @@ private static TimeZoneInfo _GetTimeZone(string id, string name) return TimeZoneInfo.ParseTZBuffer(id, buffer, buffer.Length); } - internal static TimeZoneInfo GetTimeZone (string id, string name) + internal static TimeZoneInfo? GetTimeZone(string? id, string? name) { if (name != null) { if (name == "GMT" || name == "UTC") { - return new TimeZoneInfo(id, TimeSpan.FromSeconds(0), id, name, name, null, disableDaylightSavingTime:true); + return new TimeZoneInfo(id!, TimeSpan.FromSeconds(0), id!, name!, name!, null, disableDaylightSavingTime:true); } if (name.StartsWith ("GMT")) { - return new TimeZoneInfo (id, - TimeSpan.FromSeconds(ParseNumericZone(name)), - id, name, name, null, disableDaylightSavingTime:true); + return new TimeZoneInfo (id!, + TimeSpan.FromSeconds(ParseNumericZone(name!)), + id!, name!, name!, null, disableDaylightSavingTime:true); } } @@ -128,7 +135,7 @@ internal static TimeZoneInfo GetTimeZone (string id, string name) } } - static int ParseNumericZone (string name) + private static int ParseNumericZone (string? name) { if (name == null || !name.StartsWith ("GMT") || name.Length <= 3) return 0; @@ -180,7 +187,7 @@ static int ParseNumericZone (string name) return sign * (hour * 60) * 60; } - internal static TimeZoneInfo Local + internal static TimeZoneInfo? Local { get { @@ -198,12 +205,12 @@ internal static TimeZoneInfo Local // //[DllImport ("__Internal")] //static extern void monodroid_free (IntPtr ptr); - - static string GetDefaultTimeZoneName() + + private static string? GetDefaultTimeZoneName() { IntPtr value = IntPtr.Zero; - int n = 0; - string defaultTimeZone = Environment.GetEnvironmentVariable("__XA_OVERRIDE_TIMEZONE_ID__"); + //int n = 0; + string? defaultTimeZone = Environment.GetEnvironmentVariable("__XA_OVERRIDE_TIMEZONE_ID__"); if (!string.IsNullOrEmpty(defaultTimeZone)) return defaultTimeZone; @@ -212,18 +219,19 @@ static string GetDefaultTimeZoneName() // Used by the tests //if (Environment.GetEnvironmentVariable ("__XA_USE_JAVA_DEFAULT_TIMEZONE_ID__") == null) // n = monodroid_get_system_property ("persist.sys.timezone", ref value); - - if (n > 0 && value != IntPtr.Zero) - { - defaultTimeZone = (Marshal.PtrToStringAnsi(value) ?? String.Empty).Trim(); - monodroid_free(value); - if (!String.IsNullOrEmpty(defaultTimeZone)) - return defaultTimeZone; - } - + +// if (n > 0 && value != IntPtr.Zero) +// { +// defaultTimeZone = (Marshal.PtrToStringAnsi(value) ?? String.Empty).Trim(); +// monodroid_free(value); +// if (!String.IsNullOrEmpty(defaultTimeZone)) +// return defaultTimeZone; +// } + // TODO: AndroidPlatform does not exist in runtime. We need to add an interop call - defaultTimeZone = (AndroidPlatform.GetDefaultTimeZone() ?? String.Empty).Trim(); - if (!String.IsNullOrEmpty(defaultTimeZone)) + //defaultTimeZone = (AndroidPlatform.GetDefaultTimeZone() ?? String.Empty).Trim(); + defaultTimeZone = string.Empty; + if (!string.IsNullOrEmpty(defaultTimeZone)) return defaultTimeZone; return null; @@ -231,14 +239,14 @@ static string GetDefaultTimeZoneName() } } - interface IAndroidTimeZoneDB + internal interface IAndroidTimeZoneDB { IEnumerable GetAvailableIds(); - byte[] GetTimeZoneData(string id); + byte[] GetTimeZoneData(string? id); } [StructLayout(LayoutKind.Sequential, Pack=1)] - unsafe struct AndroidTzDataHeader + internal unsafe struct AndroidTzDataHeader { public fixed byte signature [12]; public int indexOffset; @@ -247,7 +255,7 @@ unsafe struct AndroidTzDataHeader } [StructLayout(LayoutKind.Sequential, Pack=1)] - unsafe struct AndroidTzDataEntry + internal unsafe struct AndroidTzDataEntry { public fixed byte id [40]; public int byteOffset; @@ -268,7 +276,7 @@ unsafe struct AndroidTzDataEntry * database location changed (https://source.android.com/devices/architecture/modular-system/runtime#time-zone-data-interactions) * The older locations still exist (at least the `/system/usr/share/zoneinfo` one) but they won't be updated. */ - sealed class AndroidTzData : IAndroidTimeZoneDB + internal sealed class AndroidTzData : IAndroidTimeZoneDB { internal static readonly string[] Paths = new string[] { @@ -278,18 +286,18 @@ sealed class AndroidTzData : IAndroidTimeZoneDB Environment.GetEnvironmentVariable("ANDROID_ROOT") + "/usr/share/zoneinfo/tzdata", }; - private string zdataPath; - private Stream data; - private string version; - private string zoneTab; + private string tzdataPath; + private Stream? data; + private string version = ""; + private string zoneTab = ""; - private string[] ids; - private int[] byteOffsets; - private int[] lengths; + private string[]? ids; + private int[]? byteOffsets; + private int[]? lengths; public AndroidTzData (params string[] paths) { - foreach(var path in paths) + foreach (var path in paths) { if (LoadData(path)) { @@ -308,41 +316,41 @@ public AndroidTzData (params string[] paths) public string ZoneTab => zoneTab; - static string GetApexTimeDataRoot () + private static string GetApexTimeDataRoot() { - string ret = Environment.GetEnvironmentVariable ("ANDROID_TZDATA_ROOT"); - if (!String.IsNullOrEmpty (ret)) { - return ret; + string? ret = Environment.GetEnvironmentVariable("ANDROID_TZDATA_ROOT"); + if (!string.IsNullOrEmpty (ret!)) { + return ret!; } return "/apex/com.android.tzdata"; } - static string GetApexRuntimeRoot() + private static string GetApexRuntimeRoot() { - string ret = Environment.GetEnvironmentVariable ("ANDROID_RUNTIME_ROOT"); - if (!String.IsNullOrEmpty (ret)) + string? ret = Environment.GetEnvironmentVariable("ANDROID_RUNTIME_ROOT"); + if (!string.IsNullOrEmpty (ret!)) { - return ret; + return ret!; } return "/apex/com.android.runtime"; } - bool LoadData(string path) + private bool LoadData(string path) { if (!File.Exists(path)) return false; - + try { data = File.OpenRead(path); } - catch (IOException) + catch (IOException) { return false; } - catch (UnauthorizedAccessException) + catch (UnauthorizedAccessException) { return false; } @@ -352,7 +360,7 @@ bool LoadData(string path) ReadHeader(); return true; } - catch (Exception e) + catch { // log something here instead of the console. //Console.Error.WriteLine ("tzdata file \"{0}\" was present but invalid: {1}", path, e); @@ -360,7 +368,7 @@ bool LoadData(string path) return false; } - unsafe void ReadHeader() + private unsafe void ReadHeader() { int size = Math.Max(Marshal.SizeOf(typeof(AndroidTzDataHeader)), Marshal.SizeOf(typeof(AndroidTzDataEntry))); var buffer = new byte[size]; @@ -378,7 +386,7 @@ unsafe void ReadHeader() var b = new StringBuilder (); b.Append ("bad tzdata magic:"); for (int i = 0; i < 12; ++i) { - b.Append(" ").Append(((byte)s[i]).ToString ("x2")); + b.Append(' ').Append(((byte)s[i]).ToString ("x2")); } //TODO: Put strings in resource file @@ -388,10 +396,12 @@ unsafe void ReadHeader() version = new string(s, 6, 5, Encoding.ASCII); ReadIndex(header.indexOffset, header.dataOffset, buffer); - ReadZoneTab(header.zoneTabOffset, checked((int)data.Length) - header.zoneTabOffset); + ReadZoneTab(header.zoneTabOffset, checked((int)data!.Length) - header.zoneTabOffset); } - unsafe T ReadAt (long position, byte[] buffer) + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2087:UnrecognizedReflectionPattern", + Justification = "Implementation detail of Android TimeZone")] + private unsafe T ReadAt (long position, byte[] buffer) where T : struct { int size = Marshal.SizeOf(typeof(T)); @@ -401,9 +411,9 @@ unsafe T ReadAt (long position, byte[] buffer) throw new InvalidOperationException ("Internal error: buffer too small"); } - data.Position = position; + data!.Position = position; int r; - if ((r = data.Read(buffer, 0, size)) < size) + if ((r = data!.Read(buffer, 0, size)) < size) { //TODO: Put strings in resource file throw new InvalidOperationException ( @@ -412,11 +422,11 @@ unsafe T ReadAt (long position, byte[] buffer) fixed (byte* b = buffer) { - return (T)Marshal.PtrToStructure((IntPtr)b, typeof(T)); + return (T)Marshal.PtrToStructure((IntPtr)b, typeof(T))!; } } - static int NetworkToHostOrder(int value) + private static int NetworkToHostOrder(int value) { if (!BitConverter.IsLittleEndian) return value; @@ -428,26 +438,26 @@ static int NetworkToHostOrder(int value) ((value << 24))); } - unsafe void ReadIndex(int indexOffset, int dataOffset, byte[] buffer) + private unsafe void ReadIndex(int indexOffset, int dataOffset, byte[] buffer) { int indexSize = dataOffset - indexOffset; - int entryCount = indexSize / Marshal.SizeOf(typeof (AndroidTzDataEntry)); - int entrySize = Marshal.SizeOf(typeof (AndroidTzDataEntry)); + int entryCount = indexSize / Marshal.SizeOf(typeof(AndroidTzDataEntry)); + int entrySize = Marshal.SizeOf(typeof(AndroidTzDataEntry)); - byteOffsets = new int [entryCount]; - ids = new string [entryCount]; - lengths = new int [entryCount]; + byteOffsets = new int[entryCount]; + ids = new string[entryCount]; + lengths = new int[entryCount]; for (int i = 0; i < entryCount; ++i) { var entry = ReadAt(indexOffset + (entrySize*i), buffer); var p = (sbyte*)entry.id; - byteOffsets[i] = NetworkToHostOrder(entry.byteOffset) + dataOffset; - ids[i] = new string(p, 0, GetStringLength(p, 40), Encoding.ASCII); - lengths[i] = NetworkToHostOrder(entry.length); + byteOffsets![i] = NetworkToHostOrder(entry.byteOffset) + dataOffset; + ids![i] = new string(p, 0, GetStringLength(p, 40), Encoding.ASCII); + lengths![i] = NetworkToHostOrder(entry.length); - if (lengths[i] < Marshal.SizeOf(typeof(AndroidTzDataHeader))) + if (lengths![i] < Marshal.SizeOf(typeof(AndroidTzDataHeader))) { //TODO: Put strings in resource file throw new InvalidOperationException("Length in index file < sizeof(tzhead)"); @@ -455,7 +465,7 @@ unsafe void ReadIndex(int indexOffset, int dataOffset, byte[] buffer) } } - static unsafe int GetStringLength(sbyte* s, int maxLength) + private static unsafe int GetStringLength(sbyte* s, int maxLength) { int len; for (len = 0; len < maxLength; len++, s++) @@ -466,14 +476,14 @@ static unsafe int GetStringLength(sbyte* s, int maxLength) return len; } - unsafe void ReadZoneTab(int zoneTabOffset, int zoneTabSize) + private unsafe void ReadZoneTab(int zoneTabOffset, int zoneTabSize) { byte[] ztab = new byte [zoneTabSize]; - data.Position = zoneTabOffset; + data!.Position = zoneTabOffset; int r; - if ((r = data.Read(ztab, 0, ztab.Length)) < ztab.Length) + if ((r = data!.Read(ztab, 0, ztab.Length)) < ztab.Length) { //TODO: Put strings in resource file throw new InvalidOperationException( @@ -485,24 +495,27 @@ unsafe void ReadZoneTab(int zoneTabOffset, int zoneTabSize) public IEnumerable GetAvailableIds() { - return ids; + return ids!; } - public byte[] GetTimeZoneData(string id) + public byte[] GetTimeZoneData(string? id) { - int i = Array.BinarySearch(ids, id, StringComparer.Ordinal); + int i = Array.BinarySearch(ids!, id!, StringComparer.Ordinal); if (i < 0) - return null; + { + //TODO: Put strings in resource file + throw new InvalidOperationException("Error finding the timezone id"); + } - int offset = byteOffsets[i]; - int length = lengths[i]; + int offset = byteOffsets![i]; + int length = lengths![i]; var buffer = new byte[length]; - lock (data) + lock (data!) { - data.Position = offset; + data!.Position = offset; int r; - if ((r = data.Read(buffer, 0, buffer.Length)) < buffer.Length) + if ((r = data!.Read(buffer, 0, buffer.Length)) < buffer.Length) { //TODO: Put strings in resource file throw new InvalidOperationException( @@ -514,196 +527,4 @@ public byte[] GetTimeZoneData(string id) return buffer; } } - - /* - * Android < v4.3 Timezone support infrastructure. - * - * This is a C# port of org.apache.harmony.luni.internal.util.ZoneInfoDB: - * - * http://android.git.kernel.org/?p=platform/libcore.git;a=blob;f=luni/src/main/java/org/apache/harmony/luni/internal/util/ZoneInfoDB.java;h=3e7bdc3a952b24da535806d434a3a27690feae26;hb=HEAD - * - * From the ZoneInfoDB source: - * - * However, to conserve disk space the data for all time zones are - * concatenated into a single file, and a second file is used to indicate - * the starting position of each time zone record. A third file indicates - * the version of the zoneinfo databse used to generate the data. - * - * which succinctly describes why we can't just use the LIBC implementation in - * TimeZoneInfo.cs -- the "standard Unixy" directory structure is NOT used. - */ - sealed class ZoneInfoDB : IAndroidTimeZoneDB - { - private const int TimeZoneNameLength = 40; - private const int TimeZoneIntSize = 4; - - internal static readonly string ZoneDirectoryName = Environment.GetEnvironmentVariable ("ANDROID_ROOT") + "/usr/share/zoneinfo/"; - - private const string ZoneFileName = "zoneinfo.dat"; - private const string IndexFileName = "zoneinfo.idx"; - private const string DefaultVersion = "2007h"; - private const string VersionFileName = "zoneinfo.version"; - - private readonly string zoneRoot; - private readonly string version; - private readonly string[] names; - private readonly int[] starts; - private readonly int[] lengths; - private readonly int[] offsets; - - public ZoneInfoDB(string zoneInfoDB = null) - { - zoneRoot = zoneInfoDB ?? ZoneDirectoryName; - try - { - version = ReadVersion(Path.Combine(zoneRoot, VersionFileName)); - } - catch - { - version = DefaultVersion; - } - - try - { - ReadDatabase(Path.Combine(zoneRoot, IndexFileName), out names, out starts, out lengths, out offsets); - } - catch - { - names = new string [0]; - starts = new int [0]; - lengths = new int [0]; - offsets = new int [0]; - } - } - - static string ReadVersion(string path) - { - using (var file = new StreamReader(path, Encoding.GetEncoding("iso-8859-1"))) - { - return file.ReadToEnd().Trim(); - } - } - - void ReadDatabase(string path, out string[] names, out int[] starts, out int[] lengths, out int[] offsets) - { - using (var file = File.OpenRead (path)) - { - var nbuf = new byte[TimeZoneNameLength]; - - int numEntries = (int)(file.Length / (TimeZoneNameLength + 3*TimeZoneIntSize)); - - char[] namebuf = new char[TimeZoneNameLength]; - - names = new string[numEntries]; - starts = new int[numEntries]; - lengths = new int[numEntries]; - offsets = new int[numEntries]; - - for (int i = 0; i < numEntries; ++i) - { - Fill(file, nbuf, nbuf.Length); - int namelen; - for (namelen = 0; namelen < nbuf.Length; ++namelen) - { - if (nbuf[namelen] == '\0') - break; - namebuf [namelen] = (char)(nbuf[namelen] & 0xFF); - } - - names[i] = new string(namebuf, 0, namelen); - starts[i] = ReadInt32(file, nbuf); - lengths[i] = ReadInt32(file, nbuf); - offsets[i] = ReadInt32(file, nbuf); - } - } - } - - static void Fill (Stream stream, byte[] nbuf, int required) - { - int read = 0, offset = 0; - while (offset < required && (read = stream.Read(nbuf, offset, required - offset)) > 0) - offset += read; - - if (read != required) - { - //TODO: Put strings in resource file - throw new EndOfStreamException("Needed to read " + required + " bytes; read " + read + " bytes"); - } - } - - // From java.io.RandomAccessFioe.readInt(), as we need to use the same - // byte ordering as Java uses. - static int ReadInt32(Stream stream, byte[] nbuf) - { - Fill(stream, nbuf, 4); - return ((nbuf[0] & 0xff) << 24) + ((nbuf[1] & 0xff) << 16) + - ((nbuf[2] & 0xff) << 8) + (nbuf[3] & 0xff); - } - - internal string Version => version; - - public IEnumerable GetAvailableIds() - { - return GetAvailableIds(0, false); - } - - IEnumerable GetAvailableIds(int rawOffset) - { - return GetAvailableIds(rawOffset, true); - } - - IEnumerable GetAvailableIds(int rawOffset, bool checkOffset) - { - for (int i = 0; i < offsets.Length; ++i) - { - if (!checkOffset || offsets[i] == rawOffset) - yield return names [i]; - } - } - - public byte[] GetTimeZoneData(string id) - { - int start, length; - using (var stream = GetTimeZoneData(id, out start, out length)) { - if (stream == null) - return null; - byte[] buf = new byte[length]; - Fill(stream, buf, buf.Length); - return buf; - } - } - - FileStream GetTimeZoneData(string name, out int start, out int length) - { - // Just in case, to avoid NREX as in xambug #4902 - if (name == null) - { - start = 0; - length = 0; - return null; - } - - var f = new FileInfo(Path.Combine(zoneRoot, name)); - if (f.Exists) - { - start = 0; - length = (int)f.Length; - return f.OpenRead(); - } - - start = length = 0; - - int i = Array.BinarySearch(names, name, StringComparer.Ordinal); - if (i < 0) - return null; - - start = starts[i]; - length = lengths[i]; - - var stream = File.OpenRead(Path.Combine(zoneRoot, ZoneFileName)); - stream.Seek(start, SeekOrigin.Begin); - - return stream; - } - } } \ No newline at end of file diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs index 992adf1c4e8f9b..7021f470c84abd 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs @@ -228,11 +228,7 @@ private static void PopulateAllSystemTimeZones(CachedData cachedData) { Debug.Assert(Monitor.IsEntered(cachedData)); - string timeZoneDirectory = GetTimeZoneDirectory(); - foreach (string timeZoneId in GetTimeZoneIds(timeZoneDirectory)) - { - TryGetTimeZone(timeZoneId, false, out _, out _, cachedData, alwaysFallbackToLocalMachine: true); // populate the cache - } + PopulateAllSystemTimeZonesCore(); } /// @@ -245,7 +241,7 @@ private static TimeZoneInfo GetLocalTimeZone(CachedData cachedData) { Debug.Assert(Monitor.IsEntered(cachedData)); - return GetLocalTimeZoneCore(cachedData); + return GetLocalTimeZoneCore(); } private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out TimeZoneInfo? value, out Exception? e) @@ -292,62 +288,6 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out return TimeZoneInfoResult.Success; } - /// - /// Returns a collection of TimeZone Id values from the time zone file in the timeZoneDirectory. - /// - /// - /// Lines that start with # are comments and are skipped. - /// - private static List GetTimeZoneIds(string timeZoneDirectory) - { - List timeZoneIds = new List(); - - try - { - using (StreamReader sr = new StreamReader(Path.Combine(timeZoneDirectory, TimeZoneFileName), Encoding.UTF8)) - { - string? zoneTabFileLine; - while ((zoneTabFileLine = sr.ReadLine()) != null) - { - if (!string.IsNullOrEmpty(zoneTabFileLine) && zoneTabFileLine[0] != '#') - { - // the format of the line is "country-code \t coordinates \t TimeZone Id \t comments" - - int firstTabIndex = zoneTabFileLine.IndexOf('\t'); - if (firstTabIndex != -1) - { - int secondTabIndex = zoneTabFileLine.IndexOf('\t', firstTabIndex + 1); - if (secondTabIndex != -1) - { - string timeZoneId; - int startIndex = secondTabIndex + 1; - int thirdTabIndex = zoneTabFileLine.IndexOf('\t', startIndex); - if (thirdTabIndex != -1) - { - int length = thirdTabIndex - startIndex; - timeZoneId = zoneTabFileLine.Substring(startIndex, length); - } - else - { - timeZoneId = zoneTabFileLine.Substring(startIndex); - } - - if (!string.IsNullOrEmpty(timeZoneId)) - { - timeZoneIds.Add(timeZoneId); - } - } - } - } - } - } - } - catch (IOException) { } - catch (UnauthorizedAccessException) { } - - return timeZoneIds; - } - /// /// Gets the tzfile raw data for the current 'local' time zone using the following rules. /// 1. Read the TZ environment variable. If it is set, use it. 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 9a6553254ff982..573937fe37e7e8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -18,6 +18,71 @@ private static TimeZone GetLocalTimeZoneCore() return GetLocalTimeZoneFromTzFile(); } + private static void PopulateAllSystemTimeZonesCore() + { + string timeZoneDirectory = GetTimeZoneDirectory(); + foreach (string timeZoneId in GetTimeZoneIds(timeZoneDirectory)) + { + TryGetTimeZone(timeZoneId, false, out _, out _, cachedData, alwaysFallbackToLocalMachine: true); // populate the cache + } + } + + /// + /// Returns a collection of TimeZone Id values from the time zone file in the timeZoneDirectory. + /// + /// + /// Lines that start with # are comments and are skipped. + /// + private static List GetTimeZoneIds(string timeZoneDirectory) + { + List timeZoneIds = new List(); + + try + { + using (StreamReader sr = new StreamReader(Path.Combine(timeZoneDirectory, TimeZoneFileName), Encoding.UTF8)) + { + string? zoneTabFileLine; + while ((zoneTabFileLine = sr.ReadLine()) != null) + { + if (!string.IsNullOrEmpty(zoneTabFileLine) && zoneTabFileLine[0] != '#') + { + // the format of the line is "country-code \t coordinates \t TimeZone Id \t comments" + + int firstTabIndex = zoneTabFileLine.IndexOf('\t'); + if (firstTabIndex != -1) + { + int secondTabIndex = zoneTabFileLine.IndexOf('\t', firstTabIndex + 1); + if (secondTabIndex != -1) + { + string timeZoneId; + int startIndex = secondTabIndex + 1; + int thirdTabIndex = zoneTabFileLine.IndexOf('\t', startIndex); + if (thirdTabIndex != -1) + { + int length = thirdTabIndex - startIndex; + timeZoneId = zoneTabFileLine.Substring(startIndex, length); + } + else + { + timeZoneId = zoneTabFileLine.Substring(startIndex); + } + + if (!string.IsNullOrEmpty(timeZoneId)) + { + timeZoneIds.Add(timeZoneId); + } + } + } + } + } + } + } + catch (IOException) { } + catch (UnauthorizedAccessException) { } + + return timeZoneIds; + } + /// /// Helper function used by 'GetLocalTimeZone()' - this function wraps the call /// for loading time zone data from computers without Registry support. From 555d0b5adfe2a19fe00c6929876b083bd86227db Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Tue, 29 Jun 2021 16:56:43 -0400 Subject: [PATCH 08/81] Found TryGetTimeZoneFromLocalMachine to map to FindSystemTimeZoneByIdCore in the mono port, so created TryGetTimeZoneFromLocalMachineCore for both TimeZoneInfo.Unix.cs and Android.cs --- .../src/System/TimeZoneInfo.Android.cs | 6 +++ .../src/System/TimeZoneInfo.AnyUnix.cs | 41 +---------------- .../src/System/TimeZoneInfo.Unix.cs | 44 +++++++++++++++++++ 3 files changed, 51 insertions(+), 40 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 856e0ccaaf4459..d89b837f5b2fb8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -25,6 +25,12 @@ private static void PopulateAllSystemTimeZonesCore() } + //TODO: TryGetTimeZoneFromLocalMachine maps to FindSystemTimeZoneByIdCore in mono/mono implementation + private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, out TimeZoneInfo? value, out Exception? e) + { + throw new NotImplementedException("TryGetTimeZoneFromLocalMachineCore is not implemented for Android"); + } + //TODO: Figure out if this maps to something in the other TimeZoneInfo files private static TimeZoneInfo? ParseTZBuffer(string? id, byte[] buffer, int length) { diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs index 7021f470c84abd..50c8b070a11555 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs @@ -246,46 +246,7 @@ private static TimeZoneInfo GetLocalTimeZone(CachedData cachedData) private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out TimeZoneInfo? value, out Exception? e) { - value = null; - e = null; - - string timeZoneDirectory = GetTimeZoneDirectory(); - string timeZoneFilePath = Path.Combine(timeZoneDirectory, id); - byte[] rawData; - try - { - rawData = File.ReadAllBytes(timeZoneFilePath); - } - catch (UnauthorizedAccessException ex) - { - e = ex; - return TimeZoneInfoResult.SecurityException; - } - catch (FileNotFoundException ex) - { - e = ex; - return TimeZoneInfoResult.TimeZoneNotFoundException; - } - catch (DirectoryNotFoundException ex) - { - e = ex; - return TimeZoneInfoResult.TimeZoneNotFoundException; - } - catch (IOException ex) - { - e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, timeZoneFilePath), ex); - return TimeZoneInfoResult.InvalidTimeZoneException; - } - - value = GetTimeZoneFromTzData(rawData, id); - - if (value == null) - { - e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, timeZoneFilePath)); - return TimeZoneInfoResult.InvalidTimeZoneException; - } - - return TimeZoneInfoResult.Success; + return TryGetTimeZoneFromLocalMachineCore(id, out value, out e); } /// 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 573937fe37e7e8..23fd6ae55e53b3 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -27,6 +27,50 @@ private static void PopulateAllSystemTimeZonesCore() } } + private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, out TimeZoneInfo? value, out Exception? e) + { + value = null; + e = null; + + string timeZoneDirectory = GetTimeZoneDirectory(); + string timeZoneFilePath = Path.Combine(timeZoneDirectory, id); + byte[] rawData; + try + { + rawData = File.ReadAllBytes(timeZoneFilePath); + } + catch (UnauthorizedAccessException ex) + { + e = ex; + return TimeZoneInfoResult.SecurityException; + } + catch (FileNotFoundException ex) + { + e = ex; + return TimeZoneInfoResult.TimeZoneNotFoundException; + } + catch (DirectoryNotFoundException ex) + { + e = ex; + return TimeZoneInfoResult.TimeZoneNotFoundException; + } + catch (IOException ex) + { + e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, timeZoneFilePath), ex); + return TimeZoneInfoResult.InvalidTimeZoneException; + } + + value = GetTimeZoneFromTzData(rawData, id); + + if (value == null) + { + e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, timeZoneFilePath)); + return TimeZoneInfoResult.InvalidTimeZoneException; + } + + return TimeZoneInfoResult.Success; + } + /// /// Returns a collection of TimeZone Id values from the time zone file in the timeZoneDirectory. /// From d59fb1ae93acb079b5222996be57b8484fbd24da Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Tue, 29 Jun 2021 19:20:38 -0400 Subject: [PATCH 09/81] Fixed typo on type --- .../System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 23fd6ae55e53b3..c4bdd85acd6f69 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -12,7 +12,7 @@ public sealed partial class TimeZoneInfo private const string TimeZoneFileName = "zone.tab"; private const string TimeZoneDirectoryEnvironmentVariable = "TZDIR"; - private static TimeZone GetLocalTimeZoneCore() + private static TimeZoneInfo GetLocalTimeZoneCore() { // Without Registry support, create the TimeZoneInfo from a TZ file return GetLocalTimeZoneFromTzFile(); From 84dcbb7765b28b255a0d59f4a22cd31a2bc518df Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Wed, 30 Jun 2021 08:34:34 -0400 Subject: [PATCH 10/81] Added cacheddata param --- .../System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs | 2 +- .../System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs | 2 +- .../System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index d89b837f5b2fb8..615eda35fd2566 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -20,7 +20,7 @@ private static TimeZoneInfo GetLocalTimeZoneCore() } //TODO: PopulateAllSystemTimeZones maps to GetSystemTimeZonesCore in mono/mono implementation - private static void PopulateAllSystemTimeZonesCore() + private static void PopulateAllSystemTimeZonesCore(CachedData cachedData) { } diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs index 50c8b070a11555..c6f683680bccfe 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs @@ -241,7 +241,7 @@ private static TimeZoneInfo GetLocalTimeZone(CachedData cachedData) { Debug.Assert(Monitor.IsEntered(cachedData)); - return GetLocalTimeZoneCore(); + return GetLocalTimeZoneCore(cachedData); } private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out TimeZoneInfo? value, out Exception? e) 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 c4bdd85acd6f69..3e9aa4c3be5b3b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -18,7 +18,7 @@ private static TimeZoneInfo GetLocalTimeZoneCore() return GetLocalTimeZoneFromTzFile(); } - private static void PopulateAllSystemTimeZonesCore() + private static void PopulateAllSystemTimeZonesCore(CachedData cachedData) { string timeZoneDirectory = GetTimeZoneDirectory(); foreach (string timeZoneId in GetTimeZoneIds(timeZoneDirectory)) From cb9efd4f590a17972a7cb1770b64b7e1737535dd Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 30 Jun 2021 09:52:55 -0400 Subject: [PATCH 11/81] Initial implementation of PopulateAllSystemTimeZonesCore and TryGetTimeZoneFromLocalMachineCore --- .../src/System/TimeZoneInfo.Android.cs | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 615eda35fd2566..3ebb522dce54fe 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -22,13 +22,58 @@ private static TimeZoneInfo GetLocalTimeZoneCore() //TODO: PopulateAllSystemTimeZones maps to GetSystemTimeZonesCore in mono/mono implementation private static void PopulateAllSystemTimeZonesCore(CachedData cachedData) { - + // I Think we can utilize the majority of PopulateAllSystemTimeZonesCore in the Unix implementation + // We would just need to properly handle + // + // string timeZoneDirectory = GetTimeZoneDirectory(); + // foreach (string timeZoneId in GetTimeZoneIds(timeZoneDirectory)) + // + // That seems to be covered by mono/mono `AndroidTzData.GetAvailableIds` and works as long as ReadIndex is called + // db is first called -> GetDefaultTimeZoneDB -> AndroidTzData(paths) -> LoadData -> ReadHeader -> ReadIndex + foreach (string timeZoneId in AndroidTzData.GetAvailableIds()) + { + TryGetTimeZone(timeZoneId, false, out _, out _, cachedData, alwaysFallbackToLocalMachine: true); // populate the cache + } } //TODO: TryGetTimeZoneFromLocalMachine maps to FindSystemTimeZoneByIdCore in mono/mono implementation private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, out TimeZoneInfo? value, out Exception? e) { - throw new NotImplementedException("TryGetTimeZoneFromLocalMachineCore is not implemented for Android"); + value = null; + e = null; + + try + { + value = GetTimeZone(id, id); + } + catch (UnauthorizedAccessException ex) + { + e = ex; + return TimeZoneInfoResult.SecurityException; + } + catch (FileNotFoundException ex) + { + e = ex; + return TimeZoneInfoResult.TimeZoneNotFoundException; + } + catch (DirectoryNotFoundException ex) + { + e = ex; + return TimeZoneInfoResult.TimeZoneNotFoundException; + } + catch (IOException ex) + { + e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, timeZoneFilePath), ex); + return TimeZoneInfoResult.InvalidTimeZoneException; + } + + if (value == null) + { + e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, timeZoneFilePath)); + return TimeZoneInfoResult.TimeZoneNotFoundException; // Mono/mono throws TimeZoneNotFoundException, runtime throws InvalidTimeZoneException + } + + return TimeZoneInfoResult.Success; } //TODO: Figure out if this maps to something in the other TimeZoneInfo files From b68e2909b4e1dfba753a053775635f765ac08743 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 30 Jun 2021 10:27:21 -0400 Subject: [PATCH 12/81] Clean up missing variables and wrong function calls --- .../src/System/TimeZoneInfo.Android.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 3ebb522dce54fe..5a89acffd34e85 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -12,6 +12,8 @@ namespace System { public sealed partial class TimeZoneInfo { + private const string TimeZoneFileName = "tzdata"; + // TODO: Consider restructuring underlying AndroidTimeZones class©. // Although it may be easier to work with this way. private static TimeZoneInfo GetLocalTimeZoneCore() @@ -30,8 +32,10 @@ private static void PopulateAllSystemTimeZonesCore(CachedData cachedData) // // That seems to be covered by mono/mono `AndroidTzData.GetAvailableIds` and works as long as ReadIndex is called // db is first called -> GetDefaultTimeZoneDB -> AndroidTzData(paths) -> LoadData -> ReadHeader -> ReadIndex - foreach (string timeZoneId in AndroidTzData.GetAvailableIds()) + foreach (string timeZoneId in AndroidTimeZones.GetAvailableIds()) { + // cachedData is not in this current context, I think we can push PopulateAllSystemTimeZonesCore back to AllUnix + // and instead implement how the time zone IDs are obtained in Unix/Android TryGetTimeZone(timeZoneId, false, out _, out _, cachedData, alwaysFallbackToLocalMachine: true); // populate the cache } } @@ -44,7 +48,7 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, try { - value = GetTimeZone(id, id); + value = AndroidTimeZones.GetTimeZone(id, id); } catch (UnauthorizedAccessException ex) { @@ -63,13 +67,13 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, } catch (IOException ex) { - e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, timeZoneFilePath), ex); + e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, GetTimeZoneDirectory() + TimeZoneFileName), ex); return TimeZoneInfoResult.InvalidTimeZoneException; } if (value == null) { - e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, timeZoneFilePath)); + e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, GetTimeZoneDirectory() + TimeZoneFileName)); return TimeZoneInfoResult.TimeZoneNotFoundException; // Mono/mono throws TimeZoneNotFoundException, runtime throws InvalidTimeZoneException } @@ -116,7 +120,7 @@ private static string GetTimeZoneDirectory() { foreach (var filePath in Paths) { - if (File.Exists(Path.Combine(filePath, "TimeZoneFileName"))) + if (File.Exists(Path.Combine(filePath, TimeZoneFileName))) { return filePath; } From 65615ca7a9c36a9336473cbd12dce52f3f3086c5 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 30 Jun 2021 10:35:42 -0400 Subject: [PATCH 13/81] Fix cachedData parameter overload --- .../System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs index c6f683680bccfe..4e5c8f52fb8873 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs @@ -228,7 +228,7 @@ private static void PopulateAllSystemTimeZones(CachedData cachedData) { Debug.Assert(Monitor.IsEntered(cachedData)); - PopulateAllSystemTimeZonesCore(); + PopulateAllSystemTimeZonesCore(cachedData); } /// @@ -241,7 +241,7 @@ private static TimeZoneInfo GetLocalTimeZone(CachedData cachedData) { Debug.Assert(Monitor.IsEntered(cachedData)); - return GetLocalTimeZoneCore(cachedData); + return GetLocalTimeZoneCore(); } private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out TimeZoneInfo? value, out Exception? e) From 514fa4447794dc7555179252c0deb43eeb4553f2 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 30 Jun 2021 16:36:42 -0400 Subject: [PATCH 14/81] Add JNI GetDefaultTimeZone --- .../Native/Unix/System.Native/pal_datetime.c | 26 +++++++++++++++++++ .../Native/Unix/System.Native/pal_datetime.h | 3 +++ .../src/System/TimeZoneInfo.Android.cs | 11 ++++++-- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/libraries/Native/Unix/System.Native/pal_datetime.c b/src/libraries/Native/Unix/System.Native/pal_datetime.c index 30f93ad05a1d3c..cdc9383fc859d1 100644 --- a/src/libraries/Native/Unix/System.Native/pal_datetime.c +++ b/src/libraries/Native/Unix/System.Native/pal_datetime.c @@ -5,9 +5,20 @@ #include #include +#include #include #include +#include "pal_runtimeinformation.h" +#include "pal_types.h" +#include +#include +#include +#if defined(TARGET_ANDROID) +#include +#endif + + #include "pal_datetime.h" static const int64_t TICKS_PER_SECOND = 10000000; /* 10^7 */ @@ -39,3 +50,18 @@ int64_t SystemNative_GetSystemTimeAsTicks() // in failure we return 00:00 01 January 1970 UTC (Unix epoch) return 0; } + +char* SystemNative_GetDefaultTimeZone() +{ +#if defined(TARGET_ANDROID) + char timezonemitch[PROP_VALUE_MAX]; + if (__system_property_get("persist.sys.timezone", timezonemitch)) + { + return strdup(timezonemitch); + } + else + { + return NULL; + } +#endif +} diff --git a/src/libraries/Native/Unix/System.Native/pal_datetime.h b/src/libraries/Native/Unix/System.Native/pal_datetime.h index 564a69a4857ebc..f9d508e9388e3f 100644 --- a/src/libraries/Native/Unix/System.Native/pal_datetime.h +++ b/src/libraries/Native/Unix/System.Native/pal_datetime.h @@ -4,5 +4,8 @@ #pragma once #include "pal_compiler.h" +#include "pal_types.h" PALEXPORT int64_t SystemNative_GetSystemTimeAsTicks(void); + +PALEXPORT char* SystemNative_GetDefaultTimeZone(void); diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 5a89acffd34e85..f203594d7f7e87 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -83,7 +83,11 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, //TODO: Figure out if this maps to something in the other TimeZoneInfo files private static TimeZoneInfo? ParseTZBuffer(string? id, byte[] buffer, int length) { - throw new NotImplementedException("ParseTZBuffer has not been implemented yet"); + if (string.IsNullOrEmpty(id)) + { + return null; + } + return GetTimeZoneFromTzData(buffer, id); } // TODO: Validate you still need these functions / fields. We should try to isolate the android implementation @@ -261,6 +265,9 @@ internal static TimeZoneInfo? Local //[DllImport ("__Internal")] //static extern void monodroid_free (IntPtr ptr); + [DllImport(Interop.Libraries.SystemNative, EntryPoint = "SystemNative_GetDefaultTimeZone")] + internal static extern string GetDefaultTimeZone(); + private static string? GetDefaultTimeZoneName() { IntPtr value = IntPtr.Zero; @@ -285,7 +292,7 @@ internal static TimeZoneInfo? Local // TODO: AndroidPlatform does not exist in runtime. We need to add an interop call //defaultTimeZone = (AndroidPlatform.GetDefaultTimeZone() ?? String.Empty).Trim(); - defaultTimeZone = string.Empty; + defaultTimeZone = GetDefaultTimeZone(); if (!string.IsNullOrEmpty(defaultTimeZone)) return defaultTimeZone; From 6c3e567035f0117741ebf73969f626c6671d1d82 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 30 Jun 2021 17:06:45 -0400 Subject: [PATCH 15/81] Remove excess of imports in pal_datetime --- src/libraries/Native/Unix/System.Native/pal_datetime.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/libraries/Native/Unix/System.Native/pal_datetime.c b/src/libraries/Native/Unix/System.Native/pal_datetime.c index cdc9383fc859d1..6e9f9abc0f4341 100644 --- a/src/libraries/Native/Unix/System.Native/pal_datetime.c +++ b/src/libraries/Native/Unix/System.Native/pal_datetime.c @@ -9,11 +9,6 @@ #include #include -#include "pal_runtimeinformation.h" -#include "pal_types.h" -#include -#include -#include #if defined(TARGET_ANDROID) #include #endif From 378708617fcdfbabe0831f298f66c248f9bb602b Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 30 Jun 2021 17:07:08 -0400 Subject: [PATCH 16/81] Clean up PopulateAllSystemTimeZonesCore comments --- .../src/System/TimeZoneInfo.Android.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index f203594d7f7e87..65e7aaf6964a2f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -21,17 +21,8 @@ private static TimeZoneInfo GetLocalTimeZoneCore() return AndroidTimeZones.Local!; } - //TODO: PopulateAllSystemTimeZones maps to GetSystemTimeZonesCore in mono/mono implementation private static void PopulateAllSystemTimeZonesCore(CachedData cachedData) { - // I Think we can utilize the majority of PopulateAllSystemTimeZonesCore in the Unix implementation - // We would just need to properly handle - // - // string timeZoneDirectory = GetTimeZoneDirectory(); - // foreach (string timeZoneId in GetTimeZoneIds(timeZoneDirectory)) - // - // That seems to be covered by mono/mono `AndroidTzData.GetAvailableIds` and works as long as ReadIndex is called - // db is first called -> GetDefaultTimeZoneDB -> AndroidTzData(paths) -> LoadData -> ReadHeader -> ReadIndex foreach (string timeZoneId in AndroidTimeZones.GetAvailableIds()) { // cachedData is not in this current context, I think we can push PopulateAllSystemTimeZonesCore back to AllUnix From 912ab4de3fd73a86859e372f117e71614c931318 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 30 Jun 2021 17:07:32 -0400 Subject: [PATCH 17/81] Remove ParseTZBuffer middleman --- .../src/System/TimeZoneInfo.Android.cs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 65e7aaf6964a2f..89f52edd8fe687 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -71,16 +71,6 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, return TimeZoneInfoResult.Success; } - //TODO: Figure out if this maps to something in the other TimeZoneInfo files - private static TimeZoneInfo? ParseTZBuffer(string? id, byte[] buffer, int length) - { - if (string.IsNullOrEmpty(id)) - { - return null; - } - return GetTimeZoneFromTzData(buffer, id); - } - // TODO: Validate you still need these functions / fields. We should try to isolate the android implementation // as much as possible. // In other words, mirroring how mono/mono did it is a good first step and then we can walk back what's @@ -156,8 +146,7 @@ internal static IEnumerable GetAvailableIds() if (buffer == null) return null; - // TODO: See if we needd to port this function or can use something in TZ - return TimeZoneInfo.ParseTZBuffer(id, buffer, buffer.Length); + return GetTimeZoneFromTzData(buffer, id); } internal static TimeZoneInfo? GetTimeZone(string? id, string? name) From d67920e421f86746547dc176d138e1e92301b379 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 30 Jun 2021 17:07:55 -0400 Subject: [PATCH 18/81] Add id check and runtime style to _GetTimeZone --- .../src/System/TimeZoneInfo.Android.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 89f52edd8fe687..bf6006e8a7c998 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -141,10 +141,18 @@ internal static IEnumerable GetAvailableIds() private static TimeZoneInfo? _GetTimeZone(string? id, string? name) { if (db == null) + { return null; + } byte[] buffer = db.GetTimeZoneData(name); if (buffer == null) + { return null; + } + if (string.IsNullOrEmpty(id)) + { + return null; + } return GetTimeZoneFromTzData(buffer, id); } From b602bf78d7ef2ab6d43379feb954ba8d8e990721 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 30 Jun 2021 17:10:13 -0400 Subject: [PATCH 19/81] cleanup pal_datetime imports --- src/libraries/Native/Unix/System.Native/pal_datetime.c | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/libraries/Native/Unix/System.Native/pal_datetime.c b/src/libraries/Native/Unix/System.Native/pal_datetime.c index 6e9f9abc0f4341..93167511565aaa 100644 --- a/src/libraries/Native/Unix/System.Native/pal_datetime.c +++ b/src/libraries/Native/Unix/System.Native/pal_datetime.c @@ -2,19 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. #include "pal_config.h" - -#include +#include "pal_datetime.h" #include +#include #include -#include #include - #if defined(TARGET_ANDROID) #include #endif - - -#include "pal_datetime.h" +#include static const int64_t TICKS_PER_SECOND = 10000000; /* 10^7 */ #if HAVE_CLOCK_REALTIME From caa9229084a539f68ab0659f11c6bde4b0cbfba5 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 30 Jun 2021 17:23:48 -0400 Subject: [PATCH 20/81] Remove ZoneTab related code --- .../src/System/TimeZoneInfo.Android.cs | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index bf6006e8a7c998..3bce2ea00d6da6 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -339,7 +339,6 @@ internal sealed class AndroidTzData : IAndroidTimeZoneDB private string tzdataPath; private Stream? data; private string version = ""; - private string zoneTab = ""; private string[]? ids; private int[]? byteOffsets; @@ -358,14 +357,11 @@ public AndroidTzData (params string[] paths) tzdataPath = "/"; version = "missing"; - zoneTab = "# Emergency fallback data.\n"; ids = new[]{ "GMT" }; } public string Version => version; - public string ZoneTab => zoneTab; - private static string GetApexTimeDataRoot() { string? ret = Environment.GetEnvironmentVariable("ANDROID_TZDATA_ROOT"); @@ -446,7 +442,6 @@ private unsafe void ReadHeader() version = new string(s, 6, 5, Encoding.ASCII); ReadIndex(header.indexOffset, header.dataOffset, buffer); - ReadZoneTab(header.zoneTabOffset, checked((int)data!.Length) - header.zoneTabOffset); } [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2087:UnrecognizedReflectionPattern", @@ -526,23 +521,6 @@ private static unsafe int GetStringLength(sbyte* s, int maxLength) return len; } - private unsafe void ReadZoneTab(int zoneTabOffset, int zoneTabSize) - { - byte[] ztab = new byte [zoneTabSize]; - - data!.Position = zoneTabOffset; - - int r; - if ((r = data!.Read(ztab, 0, ztab.Length)) < ztab.Length) - { - //TODO: Put strings in resource file - throw new InvalidOperationException( - string.Format ("Error reading zonetab: read {0} bytes, expected {1}", r, zoneTabSize)); - } - - zoneTab = Encoding.ASCII.GetString(ztab, 0, ztab.Length); - } - public IEnumerable GetAvailableIds() { return ids!; From a7aee33bc8189f0c3d8cd9cb51c1645ecb22ef36 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 30 Jun 2021 17:25:38 -0400 Subject: [PATCH 21/81] Remove Version related code --- .../src/System/TimeZoneInfo.Android.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 3bce2ea00d6da6..69b111d10a02d7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -338,7 +338,6 @@ internal sealed class AndroidTzData : IAndroidTimeZoneDB private string tzdataPath; private Stream? data; - private string version = ""; private string[]? ids; private int[]? byteOffsets; @@ -356,12 +355,9 @@ public AndroidTzData (params string[] paths) } tzdataPath = "/"; - version = "missing"; ids = new[]{ "GMT" }; } - public string Version => version; - private static string GetApexTimeDataRoot() { string? ret = Environment.GetEnvironmentVariable("ANDROID_TZDATA_ROOT"); @@ -439,8 +435,6 @@ private unsafe void ReadHeader() throw new InvalidOperationException ("bad tzdata magic: " + b.ToString ()); } - version = new string(s, 6, 5, Encoding.ASCII); - ReadIndex(header.indexOffset, header.dataOffset, buffer); } From fb72a04cb15c7ada75a302c53822fb35e25e89d9 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 30 Jun 2021 17:33:48 -0400 Subject: [PATCH 22/81] Remove PopulateAllSystemTimeZonesCore middleman --- .../src/System/TimeZoneInfo.Android.cs | 9 ++------- .../src/System/TimeZoneInfo.AnyUnix.cs | 5 ++++- .../src/System/TimeZoneInfo.Unix.cs | 13 ++----------- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 69b111d10a02d7..332e19f58f6dbf 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -21,14 +21,9 @@ private static TimeZoneInfo GetLocalTimeZoneCore() return AndroidTimeZones.Local!; } - private static void PopulateAllSystemTimeZonesCore(CachedData cachedData) + private static List GetTimeZoneIds() { - foreach (string timeZoneId in AndroidTimeZones.GetAvailableIds()) - { - // cachedData is not in this current context, I think we can push PopulateAllSystemTimeZonesCore back to AllUnix - // and instead implement how the time zone IDs are obtained in Unix/Android - TryGetTimeZone(timeZoneId, false, out _, out _, cachedData, alwaysFallbackToLocalMachine: true); // populate the cache - } + return AndroidTimeZones.GetAvailableIds(); } //TODO: TryGetTimeZoneFromLocalMachine maps to FindSystemTimeZoneByIdCore in mono/mono implementation diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs index 4e5c8f52fb8873..5f66e01189dd44 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs @@ -228,7 +228,10 @@ private static void PopulateAllSystemTimeZones(CachedData cachedData) { Debug.Assert(Monitor.IsEntered(cachedData)); - PopulateAllSystemTimeZonesCore(cachedData); + foreach (string timeZoneId in GetTimeZoneIds()) + { + TryGetTimeZone(timeZoneId, false, out _, out _, cachedData, alwaysFallbackToLocalMachine: true); // populate the cache + } } /// 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 3e9aa4c3be5b3b..4f4edaf2c44798 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -18,15 +18,6 @@ private static TimeZoneInfo GetLocalTimeZoneCore() return GetLocalTimeZoneFromTzFile(); } - private static void PopulateAllSystemTimeZonesCore(CachedData cachedData) - { - string timeZoneDirectory = GetTimeZoneDirectory(); - foreach (string timeZoneId in GetTimeZoneIds(timeZoneDirectory)) - { - TryGetTimeZone(timeZoneId, false, out _, out _, cachedData, alwaysFallbackToLocalMachine: true); // populate the cache - } - } - private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, out TimeZoneInfo? value, out Exception? e) { value = null; @@ -77,13 +68,13 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, /// /// Lines that start with # are comments and are skipped. /// - private static List GetTimeZoneIds(string timeZoneDirectory) + private static List GetTimeZoneIds() { List timeZoneIds = new List(); try { - using (StreamReader sr = new StreamReader(Path.Combine(timeZoneDirectory, TimeZoneFileName), Encoding.UTF8)) + using (StreamReader sr = new StreamReader(Path.Combine(GetTimeZoneDirectory(), TimeZoneFileName), Encoding.UTF8)) { string? zoneTabFileLine; while ((zoneTabFileLine = sr.ReadLine()) != null) From c3f1e18fe5f4783b00d6a2640df9830af6837f65 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Thu, 1 Jul 2021 10:12:01 -0400 Subject: [PATCH 23/81] Add return for non Android --- src/libraries/Native/Unix/System.Native/pal_datetime.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libraries/Native/Unix/System.Native/pal_datetime.c b/src/libraries/Native/Unix/System.Native/pal_datetime.c index 93167511565aaa..705ab14cd3b73e 100644 --- a/src/libraries/Native/Unix/System.Native/pal_datetime.c +++ b/src/libraries/Native/Unix/System.Native/pal_datetime.c @@ -54,5 +54,7 @@ char* SystemNative_GetDefaultTimeZone() { return NULL; } +#else + return NULL; #endif } From c314363b40185cc263125ee669320fddae5fca96 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Thu, 1 Jul 2021 10:51:16 -0400 Subject: [PATCH 24/81] Avoid using static constructor for paths --- .../src/System/TimeZoneInfo.Android.cs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 332e19f58f6dbf..5346d72b7a1a8e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -92,18 +92,21 @@ private static string GetApexRuntimeRoot() return "/apex/com.android.runtime"; } - internal static readonly string[] Paths = new string[] { GetApexTimeDataRoot() + "/etc/tz/", // Android 10+, TimeData module where the updates land - GetApexRuntimeRoot() + "/etc/tz/", // Android 10+, Fallback location if the above isn't found or corrupted - Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/", }; - private static string GetTimeZoneDirectory() { - foreach (var filePath in Paths) + // Android 10+, TimeData module where the updates land + if (File.Exists(Path.Combine(GetApexTimeDataRoot() + "/etc/tz/", TimeZoneFileName))) { - if (File.Exists(Path.Combine(filePath, TimeZoneFileName))) - { - return filePath; - } + return GetApexTimeDataRoot() + "/etc/tz/"; + } + // Android 10+, Fallback location if the above isn't found or corrupted + if (File.Exists(Path.Combine(GetApexRuntimeRoot() + "/etc/tz/", TimeZoneFileName))) + { + return GetApexRuntimeRoot() + "/etc/tz/"; + } + if (File.Exists(Path.Combine(Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/", TimeZoneFileName))) + { + return Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/"; } return Environment.GetEnvironmentVariable("ANDROID_ROOT") + DefaultTimeZoneDirectory; From 0890a23d2f03e7131d2ea29639cb6603903ae9c9 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Thu, 1 Jul 2021 10:51:30 -0400 Subject: [PATCH 25/81] Remove redundant using --- .../System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 5346d72b7a1a8e..e7d449138729f9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; From 80c28e867a2a8a1201cd16e3dd961371a6867a12 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Thu, 1 Jul 2021 11:19:12 -0400 Subject: [PATCH 26/81] Fix GetTimeZoneIds return --- .../System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index e7d449138729f9..eb53263f93e1c1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -22,7 +22,7 @@ private static TimeZoneInfo GetLocalTimeZoneCore() private static List GetTimeZoneIds() { - return AndroidTimeZones.GetAvailableIds(); + return new List(AndroidTimeZones.GetAvailableIds()); } //TODO: TryGetTimeZoneFromLocalMachine maps to FindSystemTimeZoneByIdCore in mono/mono implementation From 858ef8470e1af714a3541f84b766d3f9962bc7e0 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Thu, 1 Jul 2021 15:20:08 -0400 Subject: [PATCH 27/81] Condition TargetsAndroid around function --- src/libraries/Native/Unix/System.Native/pal_datetime.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/libraries/Native/Unix/System.Native/pal_datetime.c b/src/libraries/Native/Unix/System.Native/pal_datetime.c index 705ab14cd3b73e..188bf01374264e 100644 --- a/src/libraries/Native/Unix/System.Native/pal_datetime.c +++ b/src/libraries/Native/Unix/System.Native/pal_datetime.c @@ -42,9 +42,9 @@ int64_t SystemNative_GetSystemTimeAsTicks() return 0; } +#if defined(TARGET_ANDROID) char* SystemNative_GetDefaultTimeZone() { -#if defined(TARGET_ANDROID) char timezonemitch[PROP_VALUE_MAX]; if (__system_property_get("persist.sys.timezone", timezonemitch)) { @@ -54,7 +54,5 @@ char* SystemNative_GetDefaultTimeZone() { return NULL; } -#else - return NULL; -#endif } +#endif From e0622655321fe627eec8da85e287155d5a751d26 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Thu, 1 Jul 2021 16:19:29 -0400 Subject: [PATCH 28/81] Clean up functions --- .../src/System/TimeZoneInfo.Android.cs | 159 ++++++++---------- 1 file changed, 68 insertions(+), 91 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index eb53263f93e1c1..da876c85413709 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -33,6 +33,7 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, try { + // can id be null or empty here? value = AndroidTimeZones.GetTimeZone(id, id); } catch (UnauthorizedAccessException ex) @@ -135,69 +136,29 @@ internal static IEnumerable GetAvailableIds() : db.GetAvailableIds(); } - private static TimeZoneInfo? _GetTimeZone(string? id, string? name) + // This should be called when name begins with GMT + private static int ParseGMTNumericZone(string name) { - if (db == null) - { - return null; - } - byte[] buffer = db.GetTimeZoneData(name); - if (buffer == null) + int sign; + if (name[3] == '+') { - return null; + sign = 1; } - if (string.IsNullOrEmpty(id)) + else if (name[3] == '-') { - return null; - } - - return GetTimeZoneFromTzData(buffer, id); - } - - internal static TimeZoneInfo? GetTimeZone(string? id, string? name) - { - if (name != null) - { - if (name == "GMT" || name == "UTC") - { - return new TimeZoneInfo(id!, TimeSpan.FromSeconds(0), id!, name!, name!, null, disableDaylightSavingTime:true); - } - if (name.StartsWith ("GMT")) - { - return new TimeZoneInfo (id!, - TimeSpan.FromSeconds(ParseNumericZone(name!)), - id!, name!, name!, null, disableDaylightSavingTime:true); - } - } - - try - { - return _GetTimeZone(id, name); - } catch (Exception) - { - return null; - } - } - - private static int ParseNumericZone (string? name) - { - if (name == null || !name.StartsWith ("GMT") || name.Length <= 3) - return 0; - - int sign; - if (name [3] == '+') - sign = 1; - else if (name [3] == '-') sign = -1; + } else + { return 0; + } int where; int hour = 0; bool colon = false; for (where = 4; where < name.Length; where++) { - char c = name [where]; + char c = name[where]; if (c == ':') { @@ -207,9 +168,13 @@ private static int ParseNumericZone (string? name) } if (c >= '0' && c <= '9') + { hour = hour * 10 + c - '0'; + } else + { return 0; + } } int min = 0; @@ -218,70 +183,82 @@ private static int ParseNumericZone (string? name) char c = name [where]; if (c >= '0' && c <= '9') + { min = min * 10 + c - '0'; + } else + { return 0; + } } if (colon) + { return sign * (hour * 60 + min) * 60; + } else if (hour >= 100) + { return sign * ((hour / 100) * 60 + (hour % 100)) * 60; + } else + { return sign * (hour * 60) * 60; + } } - internal static TimeZoneInfo? Local + private static TimeZoneInfo? _GetTimeZone(string id, string name) { - get + if (db == null) { - var id = GetDefaultTimeZoneName(); - return GetTimeZone(id, id); + return null; } + byte[] buffer = db.GetTimeZoneData(name); + if (buffer == null) + { + return null; + } + + return GetTimeZoneFromTzData(buffer, id); } - // TODO: We probably don't need this. However, if we do, move to Interop - // - //[DllImport ("__Internal")] - //static extern int monodroid_get_system_property (string name, ref IntPtr value); + internal static TimeZoneInfo? GetTimeZone(string id, string name) + { + if (name == "GMT" || name == "UTC") + { + return new TimeZoneInfo(id, TimeSpan.FromSeconds(0), id, name, name, null, disableDaylightSavingTime:true); + } + if (name.StartsWith("GMT")) + { + return new TimeZoneInfo(id, TimeSpan.FromSeconds(ParseGMTNumericZone(name)), id, name, name, null, disableDaylightSavingTime:true); + } - // TODO: Move this into Interop - // - //[DllImport ("__Internal")] - //static extern void monodroid_free (IntPtr ptr); + try + { + return _GetTimeZone(id, name); + } + catch (Exception) + { + // Should we return null? + return null; + } + } [DllImport(Interop.Libraries.SystemNative, EntryPoint = "SystemNative_GetDefaultTimeZone")] internal static extern string GetDefaultTimeZone(); - private static string? GetDefaultTimeZoneName() + internal static TimeZoneInfo? Local { - IntPtr value = IntPtr.Zero; - //int n = 0; - string? defaultTimeZone = Environment.GetEnvironmentVariable("__XA_OVERRIDE_TIMEZONE_ID__"); - - if (!string.IsNullOrEmpty(defaultTimeZone)) - return defaultTimeZone; - - // TODO: See how we can test this without hacks - // Used by the tests - //if (Environment.GetEnvironmentVariable ("__XA_USE_JAVA_DEFAULT_TIMEZONE_ID__") == null) - // n = monodroid_get_system_property ("persist.sys.timezone", ref value); - -// if (n > 0 && value != IntPtr.Zero) -// { -// defaultTimeZone = (Marshal.PtrToStringAnsi(value) ?? String.Empty).Trim(); -// monodroid_free(value); -// if (!String.IsNullOrEmpty(defaultTimeZone)) -// return defaultTimeZone; -// } - - // TODO: AndroidPlatform does not exist in runtime. We need to add an interop call - //defaultTimeZone = (AndroidPlatform.GetDefaultTimeZone() ?? String.Empty).Trim(); - defaultTimeZone = GetDefaultTimeZone(); - if (!string.IsNullOrEmpty(defaultTimeZone)) - return defaultTimeZone; + get + { + var id = GetDefaultTimeZone(); + if (!string.IsNullOrEmpty(id)) + { + return GetTimeZone(id, id); + } - return null; + // If we can't find a default time zone, return UTC + return Utc; + } } } } @@ -517,9 +494,9 @@ public IEnumerable GetAvailableIds() return ids!; } - public byte[] GetTimeZoneData(string? id) + public byte[] GetTimeZoneData(string id) { - int i = Array.BinarySearch(ids!, id!, StringComparer.Ordinal); + int i = Array.BinarySearch(ids!, id, StringComparer.Ordinal); if (i < 0) { //TODO: Put strings in resource file From 3956ff7332b39879d5b3e703f1dad602dc8c51d3 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Thu, 1 Jul 2021 16:22:28 -0400 Subject: [PATCH 29/81] Remove GetTimeZoneDirectory Android implementation --- .../src/System/TimeZoneInfo.Android.cs | 46 ------------------- 1 file changed, 46 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index da876c85413709..00d1e873af7d9b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -66,52 +66,6 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, return TimeZoneInfoResult.Success; } - // TODO: Validate you still need these functions / fields. We should try to isolate the android implementation - // as much as possible. - // In other words, mirroring how mono/mono did it is a good first step and then we can walk back what's - // common with TimeZoneInfo.cs and TimeZoneInfo.AnyUnix.cs - private static string GetApexTimeDataRoot() - { - var ret = Environment.GetEnvironmentVariable("ANDROID_TZDATA_ROOT"); - if (!string.IsNullOrEmpty(ret)) - { - return ret; - } - - return "/apex/com.android.tzdata"; - } - - private static string GetApexRuntimeRoot() - { - var ret = Environment.GetEnvironmentVariable("ANDROID_RUNTIME_ROOT"); - if (!string.IsNullOrEmpty(ret)) - { - return ret; - } - - return "/apex/com.android.runtime"; - } - - private static string GetTimeZoneDirectory() - { - // Android 10+, TimeData module where the updates land - if (File.Exists(Path.Combine(GetApexTimeDataRoot() + "/etc/tz/", TimeZoneFileName))) - { - return GetApexTimeDataRoot() + "/etc/tz/"; - } - // Android 10+, Fallback location if the above isn't found or corrupted - if (File.Exists(Path.Combine(GetApexRuntimeRoot() + "/etc/tz/", TimeZoneFileName))) - { - return GetApexRuntimeRoot() + "/etc/tz/"; - } - if (File.Exists(Path.Combine(Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/", TimeZoneFileName))) - { - return Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/"; - } - - return Environment.GetEnvironmentVariable("ANDROID_ROOT") + DefaultTimeZoneDirectory; - } - private static class AndroidTimeZones { private static IAndroidTimeZoneDB? db = GetDefaultTimeZoneDB(); From 18c49556c2d9ca01fa178f27790fa3b8811bc258 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Thu, 1 Jul 2021 16:30:51 -0400 Subject: [PATCH 30/81] Remove interface --- .../src/System/TimeZoneInfo.Android.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 00d1e873af7d9b..5e2202e07eeb78 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -68,9 +68,9 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, private static class AndroidTimeZones { - private static IAndroidTimeZoneDB? db = GetDefaultTimeZoneDB(); + private static AndroidTzData? db = GetDefaultTimeZoneDB(); - private static IAndroidTimeZoneDB? GetDefaultTimeZoneDB() + private static AndroidTzData? GetDefaultTimeZoneDB() { foreach (var p in AndroidTzData.Paths) { @@ -217,12 +217,6 @@ internal static TimeZoneInfo? Local } } - internal interface IAndroidTimeZoneDB - { - IEnumerable GetAvailableIds(); - byte[] GetTimeZoneData(string? id); - } - [StructLayout(LayoutKind.Sequential, Pack=1)] internal unsafe struct AndroidTzDataHeader { @@ -254,7 +248,7 @@ internal unsafe struct AndroidTzDataEntry * database location changed (https://source.android.com/devices/architecture/modular-system/runtime#time-zone-data-interactions) * The older locations still exist (at least the `/system/usr/share/zoneinfo` one) but they won't be updated. */ - internal sealed class AndroidTzData : IAndroidTimeZoneDB + internal sealed class AndroidTzData { internal static readonly string[] Paths = new string[] { From 37c0a0c9e33f7a0af26c88866826e9d51fe7484f Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Thu, 1 Jul 2021 16:31:05 -0400 Subject: [PATCH 31/81] Cleanup style --- .../src/System/TimeZoneInfo.Android.cs | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 5e2202e07eeb78..129eb390eccbfe 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -35,6 +35,8 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, { // can id be null or empty here? value = AndroidTimeZones.GetTimeZone(id, id); + + // Are the below exceptions correct? Does GetTimeZone hit them at all } catch (UnauthorizedAccessException ex) { @@ -74,7 +76,7 @@ private static class AndroidTimeZones { foreach (var p in AndroidTzData.Paths) { - if (File.Exists (p)) + if (File.Exists(p)) { return new AndroidTzData(AndroidTzData.Paths); } @@ -220,7 +222,7 @@ internal static TimeZoneInfo? Local [StructLayout(LayoutKind.Sequential, Pack=1)] internal unsafe struct AndroidTzDataHeader { - public fixed byte signature [12]; + public fixed byte signature[12]; public int indexOffset; public int dataOffset; public int zoneTabOffset; @@ -229,7 +231,7 @@ internal unsafe struct AndroidTzDataHeader [StructLayout(LayoutKind.Sequential, Pack=1)] internal unsafe struct AndroidTzDataEntry { - public fixed byte id [40]; + public fixed byte id[40]; public int byteOffset; public int length; public int rawUtcOffset; @@ -265,7 +267,7 @@ internal sealed class AndroidTzData private int[]? byteOffsets; private int[]? lengths; - public AndroidTzData (params string[] paths) + public AndroidTzData(params string[] paths) { foreach (var path in paths) { @@ -283,7 +285,7 @@ public AndroidTzData (params string[] paths) private static string GetApexTimeDataRoot() { string? ret = Environment.GetEnvironmentVariable("ANDROID_TZDATA_ROOT"); - if (!string.IsNullOrEmpty (ret!)) { + if (!string.IsNullOrEmpty(ret!)) { return ret!; } @@ -293,7 +295,7 @@ private static string GetApexTimeDataRoot() private static string GetApexRuntimeRoot() { string? ret = Environment.GetEnvironmentVariable("ANDROID_RUNTIME_ROOT"); - if (!string.IsNullOrEmpty (ret!)) + if (!string.IsNullOrEmpty(ret!)) { return ret!; } @@ -347,14 +349,14 @@ private unsafe void ReadHeader() if (magic != "tzdata" || header.signature[11] != 0) { - var b = new StringBuilder (); - b.Append ("bad tzdata magic:"); + var b = new StringBuilder(); + b.Append("bad tzdata magic:"); for (int i = 0; i < 12; ++i) { - b.Append(' ').Append(((byte)s[i]).ToString ("x2")); + b.Append(' ').Append(((byte)s[i]).ToString("x2")); } //TODO: Put strings in resource file - throw new InvalidOperationException ("bad tzdata magic: " + b.ToString ()); + throw new InvalidOperationException("bad tzdata magic: " + b.ToString()); } ReadIndex(header.indexOffset, header.dataOffset, buffer); @@ -362,14 +364,14 @@ private unsafe void ReadHeader() [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2087:UnrecognizedReflectionPattern", Justification = "Implementation detail of Android TimeZone")] - private unsafe T ReadAt (long position, byte[] buffer) + private unsafe T ReadAt(long position, byte[] buffer) where T : struct { int size = Marshal.SizeOf(typeof(T)); if (buffer.Length < size) { //TODO: Put strings in resource file - throw new InvalidOperationException ("Internal error: buffer too small"); + throw new InvalidOperationException("Internal error: buffer too small"); } data!.Position = position; @@ -377,11 +379,11 @@ private unsafe T ReadAt (long position, byte[] buffer) if ((r = data!.Read(buffer, 0, size)) < size) { //TODO: Put strings in resource file - throw new InvalidOperationException ( - string.Format ("Error reading '{0}': read {1} bytes, expected {2}", tzdataPath, r, size)); + throw new InvalidOperationException( + string.Format("Error reading '{0}': read {1} bytes, expected {2}", tzdataPath, r, size)); } - fixed (byte* b = buffer) + fixed(byte* b = buffer) { return (T)Marshal.PtrToStructure((IntPtr)b, typeof(T))!; } @@ -463,7 +465,7 @@ public byte[] GetTimeZoneData(string id) { //TODO: Put strings in resource file throw new InvalidOperationException( - string.Format ("Unable to fully read from file '{0}' at offset {1} length {2}; read {3} bytes expected {4}.", + string.Format("Unable to fully read from file '{0}' at offset {1} length {2}; read {3} bytes expected {4}.", tzdataPath, offset, length, r, buffer.Length)); } } From 98ee19d0af62d3d73c283aaf7890144eddbaa7d1 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Thu, 1 Jul 2021 16:34:50 -0400 Subject: [PATCH 32/81] Remove zoneTabOffset --- .../System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 129eb390eccbfe..476752e01127c0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -225,7 +225,6 @@ internal unsafe struct AndroidTzDataHeader public fixed byte signature[12]; public int indexOffset; public int dataOffset; - public int zoneTabOffset; } [StructLayout(LayoutKind.Sequential, Pack=1)] @@ -252,7 +251,6 @@ internal unsafe struct AndroidTzDataEntry */ internal sealed class AndroidTzData { - internal static readonly string[] Paths = new string[] { GetApexTimeDataRoot() + "/etc/tz/tzdata", // Android 10+, TimeData module where the updates land GetApexRuntimeRoot() + "/etc/tz/tzdata", // Android 10+, Fallback location if the above isn't found or corrupted @@ -342,7 +340,6 @@ private unsafe void ReadHeader() header.indexOffset = NetworkToHostOrder(header.indexOffset); header.dataOffset = NetworkToHostOrder(header.dataOffset); - header.zoneTabOffset = NetworkToHostOrder(header.zoneTabOffset); sbyte* s = (sbyte*)header.signature; string magic = new string(s, 0, 6, Encoding.ASCII); From fc8ce4a26f031c4cc5455e9fb81476e70c472704 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 2 Jul 2021 10:33:03 -0400 Subject: [PATCH 33/81] Temporarily add GetTimeZoneDirectory --- .../src/System/TimeZoneInfo.Android.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 476752e01127c0..c561573a8de675 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -25,6 +25,11 @@ private static List GetTimeZoneIds() return new List(AndroidTimeZones.GetAvailableIds()); } + private static string GetTimeZoneDirectory() + { + return Environment.GetEnvironmentVariable("ANDROID_ROOT") + DefaultTimeZoneDirectory; + } + //TODO: TryGetTimeZoneFromLocalMachine maps to FindSystemTimeZoneByIdCore in mono/mono implementation private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, out TimeZoneInfo? value, out Exception? e) { @@ -380,7 +385,7 @@ private unsafe T ReadAt(long position, byte[] buffer) string.Format("Error reading '{0}': read {1} bytes, expected {2}", tzdataPath, r, size)); } - fixed(byte* b = buffer) + fixed (byte* b = buffer) { return (T)Marshal.PtrToStructure((IntPtr)b, typeof(T))!; } From c4397b723445ccdf419a5b5355533747d9d2f518 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 2 Jul 2021 13:23:28 -0400 Subject: [PATCH 34/81] Refactoring code --- .../src/System/TimeZoneInfo.Android.cs | 306 +++++++----------- 1 file changed, 120 insertions(+), 186 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index c561573a8de675..ee5f0aef7d42a6 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -13,214 +13,148 @@ public sealed partial class TimeZoneInfo { private const string TimeZoneFileName = "tzdata"; - // TODO: Consider restructuring underlying AndroidTimeZones class©. - // Although it may be easier to work with this way. - private static TimeZoneInfo GetLocalTimeZoneCore() - { - return AndroidTimeZones.Local!; - } + private static AndroidTzData tzData = new AndroidTzData(); - private static List GetTimeZoneIds() - { - return new List(AndroidTimeZones.GetAvailableIds()); - } + [DllImport(Interop.Libraries.SystemNative, EntryPoint = "SystemNative_GetDefaultTimeZone")] + internal static extern string GetDefaultTimeZone(); - private static string GetTimeZoneDirectory() + // This should be called when name begins with GMT + private static int ParseGMTNumericZone(string name) { - return Environment.GetEnvironmentVariable("ANDROID_ROOT") + DefaultTimeZoneDirectory; - } - - //TODO: TryGetTimeZoneFromLocalMachine maps to FindSystemTimeZoneByIdCore in mono/mono implementation - private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, out TimeZoneInfo? value, out Exception? e) - { - value = null; - e = null; - - try + int sign; + if (name[3] == '+') { - // can id be null or empty here? - value = AndroidTimeZones.GetTimeZone(id, id); - - // Are the below exceptions correct? Does GetTimeZone hit them at all + sign = 1; } - catch (UnauthorizedAccessException ex) + else if (name[3] == '-') { - e = ex; - return TimeZoneInfoResult.SecurityException; + sign = -1; } - catch (FileNotFoundException ex) + else { - e = ex; - return TimeZoneInfoResult.TimeZoneNotFoundException; - } - catch (DirectoryNotFoundException ex) - { - e = ex; - return TimeZoneInfoResult.TimeZoneNotFoundException; - } - catch (IOException ex) - { - e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, GetTimeZoneDirectory() + TimeZoneFileName), ex); - return TimeZoneInfoResult.InvalidTimeZoneException; + return 0; } - if (value == null) + int where; + int hour = 0; + bool colon = false; + for (where = 4; where < name.Length; where++) { - e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, GetTimeZoneDirectory() + TimeZoneFileName)); - return TimeZoneInfoResult.TimeZoneNotFoundException; // Mono/mono throws TimeZoneNotFoundException, runtime throws InvalidTimeZoneException - } + char c = name[where]; - return TimeZoneInfoResult.Success; - } - - private static class AndroidTimeZones - { - private static AndroidTzData? db = GetDefaultTimeZoneDB(); - - private static AndroidTzData? GetDefaultTimeZoneDB() - { - foreach (var p in AndroidTzData.Paths) + if (c == ':') { - if (File.Exists(p)) - { - return new AndroidTzData(AndroidTzData.Paths); - } + where++; + colon = true; + break; } - //TODO: What should we throw here? - return null; - } - - internal static IEnumerable GetAvailableIds() - { - return db == null - ? Array.Empty() - : db.GetAvailableIds(); - } - // This should be called when name begins with GMT - private static int ParseGMTNumericZone(string name) - { - int sign; - if (name[3] == '+') - { - sign = 1; - } - else if (name[3] == '-') + if (c >= '0' && c <= '9') { - sign = -1; + hour = hour * 10 + c - '0'; } else { return 0; } + } - int where; - int hour = 0; - bool colon = false; - for (where = 4; where < name.Length; where++) - { - char c = name[where]; - - if (c == ':') - { - where++; - colon = true; - break; - } - - if (c >= '0' && c <= '9') - { - hour = hour * 10 + c - '0'; - } - else - { - return 0; - } - } - - int min = 0; - for (; where < name.Length; where++) - { - char c = name [where]; - - if (c >= '0' && c <= '9') - { - min = min * 10 + c - '0'; - } - else - { - return 0; - } - } + int min = 0; + for (; where < name.Length; where++) + { + char c = name [where]; - if (colon) - { - return sign * (hour * 60 + min) * 60; - } - else if (hour >= 100) + if (c >= '0' && c <= '9') { - return sign * ((hour / 100) * 60 + (hour % 100)) * 60; + min = min * 10 + c - '0'; } else { - return sign * (hour * 60) * 60; + return 0; } } - private static TimeZoneInfo? _GetTimeZone(string id, string name) + if (colon) { - if (db == null) - { - return null; - } - byte[] buffer = db.GetTimeZoneData(name); - if (buffer == null) - { - return null; - } + return sign * (hour * 60 + min) * 60; + } + else if (hour >= 100) + { + return sign * ((hour / 100) * 60 + (hour % 100)) * 60; + } + else + { + return sign * (hour * 60) * 60; + } + } + internal static TimeZoneInfo? GetTimeZone(string id, string name) + { + if (name == "GMT" || name == "UTC") + { + return new TimeZoneInfo(id, TimeSpan.FromSeconds(0), id, name, name, null, disableDaylightSavingTime:true); + } + if (name.StartsWith("GMT")) + { + return new TimeZoneInfo(id, TimeSpan.FromSeconds(ParseGMTNumericZone(name)), id, name, name, null, disableDaylightSavingTime:true); + } + + try + { + byte[] buffer = tzData.GetTimeZoneData(name); return GetTimeZoneFromTzData(buffer, id); } + catch + { + // How should we handle + return null; + } + } - internal static TimeZoneInfo? GetTimeZone(string id, string name) + private static TimeZoneInfo GetLocalTimeZoneCore() + { + var id = GetDefaultTimeZone(); + if (!string.IsNullOrEmpty(id)) { - if (name == "GMT" || name == "UTC") - { - return new TimeZoneInfo(id, TimeSpan.FromSeconds(0), id, name, name, null, disableDaylightSavingTime:true); - } - if (name.StartsWith("GMT")) - { - return new TimeZoneInfo(id, TimeSpan.FromSeconds(ParseGMTNumericZone(name)), id, name, name, null, disableDaylightSavingTime:true); - } + var defaultTimeZone = GetTimeZone(id, id); - try - { - return _GetTimeZone(id, name); - } - catch (Exception) + if (defaultTimeZone != null) { - // Should we return null? - return null; + return defaultTimeZone; } } - [DllImport(Interop.Libraries.SystemNative, EntryPoint = "SystemNative_GetDefaultTimeZone")] - internal static extern string GetDefaultTimeZone(); + // If we can't find a default time zone, return UTC + return Utc; + } + + // TODO could check all paths in AndroidTzData.Paths, or we move the functions that call this into `TimeZoneInfo.Unix.cs` + private static string GetTimeZoneDirectory() + { + return Environment.GetEnvironmentVariable("ANDROID_ROOT") + DefaultTimeZoneDirectory; + } + + //TODO: TryGetTimeZoneFromLocalMachine maps to FindSystemTimeZoneByIdCore in mono/mono implementation + private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, out TimeZoneInfo? value, out Exception? e) + { + value = null; + e = null; - internal static TimeZoneInfo? Local + // mono/mono FindSystemTimeZoneById suggests Local scenario + value = id == "Local" ? GetLocalTimeZoneCore() : GetTimeZone(id, id); + + if (value == null) { - get - { - var id = GetDefaultTimeZone(); - if (!string.IsNullOrEmpty(id)) - { - return GetTimeZone(id, id); - } - - // If we can't find a default time zone, return UTC - return Utc; - } + e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, GetTimeZoneDirectory() + TimeZoneFileName)); + return TimeZoneInfoResult.TimeZoneNotFoundException; // Mono/mono throws TimeZoneNotFoundException, runtime throws InvalidTimeZoneException } + + return TimeZoneInfoResult.Success; + } + + internal static List GetTimeZoneIds() + { + return tzData.GetTimeZoneIds(); } } @@ -270,9 +204,9 @@ internal sealed class AndroidTzData private int[]? byteOffsets; private int[]? lengths; - public AndroidTzData(params string[] paths) + public AndroidTzData() { - foreach (var path in paths) + foreach (var path in Paths) { if (LoadData(path)) { @@ -288,8 +222,8 @@ public AndroidTzData(params string[] paths) private static string GetApexTimeDataRoot() { string? ret = Environment.GetEnvironmentVariable("ANDROID_TZDATA_ROOT"); - if (!string.IsNullOrEmpty(ret!)) { - return ret!; + if (!string.IsNullOrEmpty(ret)) { + return ret; } return "/apex/com.android.tzdata"; @@ -298,9 +232,9 @@ private static string GetApexTimeDataRoot() private static string GetApexRuntimeRoot() { string? ret = Environment.GetEnvironmentVariable("ANDROID_RUNTIME_ROOT"); - if (!string.IsNullOrEmpty(ret!)) + if (!string.IsNullOrEmpty(ret)) { - return ret!; + return ret; } return "/apex/com.android.runtime"; @@ -309,7 +243,9 @@ private static string GetApexRuntimeRoot() private bool LoadData(string path) { if (!File.Exists(path)) + { return false; + } try { @@ -331,10 +267,8 @@ private bool LoadData(string path) } catch { - // log something here instead of the console. - //Console.Error.WriteLine ("tzdata file \"{0}\" was present but invalid: {1}", path, e); + return false; } - return false; } private unsafe void ReadHeader() @@ -403,6 +337,17 @@ private static int NetworkToHostOrder(int value) ((value << 24))); } + private static unsafe int GetStringLength(sbyte* s, int maxLength) + { + int len; + for (len = 0; len < maxLength; len++, s++) + { + if (*s == 0) + break; + } + return len; + } + private unsafe void ReadIndex(int indexOffset, int dataOffset, byte[] buffer) { int indexSize = dataOffset - indexOffset; @@ -430,20 +375,9 @@ private unsafe void ReadIndex(int indexOffset, int dataOffset, byte[] buffer) } } - private static unsafe int GetStringLength(sbyte* s, int maxLength) - { - int len; - for (len = 0; len < maxLength; len++, s++) - { - if (*s == 0) - break; - } - return len; - } - - public IEnumerable GetAvailableIds() + public List GetTimeZoneIds() { - return ids!; + return new List(ids!); } public byte[] GetTimeZoneData(string id) From 7188c6b296ac0913ab500f58de22b0d1bd1dd95f Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 2 Jul 2021 15:11:31 -0400 Subject: [PATCH 35/81] Address some feedback --- .../src/System/TimeZoneInfo.Android.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index ee5f0aef7d42a6..6ff1b4f123459a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -16,7 +16,7 @@ public sealed partial class TimeZoneInfo private static AndroidTzData tzData = new AndroidTzData(); [DllImport(Interop.Libraries.SystemNative, EntryPoint = "SystemNative_GetDefaultTimeZone")] - internal static extern string GetDefaultTimeZone(); + private static extern string? GetDefaultTimeZone(); // This should be called when name begins with GMT private static int ParseGMTNumericZone(string name) @@ -88,7 +88,7 @@ private static int ParseGMTNumericZone(string name) } } - internal static TimeZoneInfo? GetTimeZone(string id, string name) + private static TimeZoneInfo? GetTimeZone(string id, string name) { if (name == "GMT" || name == "UTC") { @@ -152,7 +152,7 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, return TimeZoneInfoResult.Success; } - internal static List GetTimeZoneIds() + internal static string[] GetTimeZoneIds() { return tzData.GetTimeZoneIds(); } @@ -351,8 +351,8 @@ private static unsafe int GetStringLength(sbyte* s, int maxLength) private unsafe void ReadIndex(int indexOffset, int dataOffset, byte[] buffer) { int indexSize = dataOffset - indexOffset; - int entryCount = indexSize / Marshal.SizeOf(typeof(AndroidTzDataEntry)); int entrySize = Marshal.SizeOf(typeof(AndroidTzDataEntry)); + int entryCount = indexSize / entrySize; byteOffsets = new int[entryCount]; ids = new string[entryCount]; @@ -375,9 +375,9 @@ private unsafe void ReadIndex(int indexOffset, int dataOffset, byte[] buffer) } } - public List GetTimeZoneIds() + public string[] GetTimeZoneIds() { - return new List(ids!); + return ids!; } public byte[] GetTimeZoneData(string id) From 319810116ed21e87cebc371f65a00c58096bfda5 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 2 Jul 2021 15:44:44 -0400 Subject: [PATCH 36/81] Move interop to its own file --- .../Interop.GetDefaultTimeZone.Android.cs | 14 ++++++++++++++ .../Native/Unix/System.Native/pal_datetime.h | 2 ++ .../src/System.Private.CoreLib.Shared.projitems | 3 +++ .../src/System/TimeZoneInfo.Android.cs | 5 +---- 4 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetDefaultTimeZone.Android.cs diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetDefaultTimeZone.Android.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetDefaultTimeZone.Android.cs new file mode 100644 index 00000000000000..2791a653ae8e5b --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetDefaultTimeZone.Android.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Sys + { + [DllImport(Interop.Libraries.SystemNative, EntryPoint = "SystemNative_GetDefaultTimeZone")] + internal static extern string? GetDefaultTimeZone(); + } +} diff --git a/src/libraries/Native/Unix/System.Native/pal_datetime.h b/src/libraries/Native/Unix/System.Native/pal_datetime.h index f9d508e9388e3f..1b88d472780fd1 100644 --- a/src/libraries/Native/Unix/System.Native/pal_datetime.h +++ b/src/libraries/Native/Unix/System.Native/pal_datetime.h @@ -8,4 +8,6 @@ PALEXPORT int64_t SystemNative_GetSystemTimeAsTicks(void); +#if defined(TARGET_ANDROID) PALEXPORT char* SystemNative_GetDefaultTimeZone(void); +#endif diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 74d10709ee6deb..8e907907ef4947 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -1914,6 +1914,9 @@ Common\Interop\Unix\System.Native\Interop.GetCwd.cs + + Common\Interop\Unix\System.Native\Interop.GetDefaultTimeZone.Android.cs + Common\Interop\Unix\System.Native\Interop.GetHostName.cs diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 6ff1b4f123459a..43c44979e78112 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -15,9 +15,6 @@ public sealed partial class TimeZoneInfo private static AndroidTzData tzData = new AndroidTzData(); - [DllImport(Interop.Libraries.SystemNative, EntryPoint = "SystemNative_GetDefaultTimeZone")] - private static extern string? GetDefaultTimeZone(); - // This should be called when name begins with GMT private static int ParseGMTNumericZone(string name) { @@ -113,7 +110,7 @@ private static int ParseGMTNumericZone(string name) private static TimeZoneInfo GetLocalTimeZoneCore() { - var id = GetDefaultTimeZone(); + var id = Interop.Sys.GetDefaultTimeZone(); if (!string.IsNullOrEmpty(id)) { var defaultTimeZone = GetTimeZone(id, id); From 1a495cc3ff93abc05b038b0a22e6f2a34f484605 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 2 Jul 2021 15:50:40 -0400 Subject: [PATCH 37/81] Rename variable --- src/libraries/Native/Unix/System.Native/pal_datetime.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libraries/Native/Unix/System.Native/pal_datetime.c b/src/libraries/Native/Unix/System.Native/pal_datetime.c index 188bf01374264e..3832114b4eb2e6 100644 --- a/src/libraries/Native/Unix/System.Native/pal_datetime.c +++ b/src/libraries/Native/Unix/System.Native/pal_datetime.c @@ -45,10 +45,10 @@ int64_t SystemNative_GetSystemTimeAsTicks() #if defined(TARGET_ANDROID) char* SystemNative_GetDefaultTimeZone() { - char timezonemitch[PROP_VALUE_MAX]; - if (__system_property_get("persist.sys.timezone", timezonemitch)) + char defaulttimezone[PROP_VALUE_MAX]; + if (__system_property_get("persist.sys.timezone", defaulttimezone)) { - return strdup(timezonemitch); + return strdup(defaulttimezone); } else { From 6e40f6198c1f4750d590a8431bb548a6cb62a76c Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 6 Jul 2021 12:30:13 -0400 Subject: [PATCH 38/81] Refactor to modify access from internal to private --- .../src/System/TimeZoneInfo.Android.cs | 394 +++++++++--------- 1 file changed, 197 insertions(+), 197 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 43c44979e78112..1396cb1643d67b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -149,261 +149,261 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, return TimeZoneInfoResult.Success; } - internal static string[] GetTimeZoneIds() + private static string[] GetTimeZoneIds() { return tzData.GetTimeZoneIds(); } - } - [StructLayout(LayoutKind.Sequential, Pack=1)] - internal unsafe struct AndroidTzDataHeader - { - public fixed byte signature[12]; - public int indexOffset; - public int dataOffset; - } + /* + * Android v4.3 Timezone support infrastructure. + * + * This is a C# port of libcore.util.ZoneInfoDB: + * + * https://android.googlesource.com/platform/libcore/+/master/luni/src/main/java/libcore/util/ZoneInfoDB.java + * + * This is needed in order to read Android v4.3 tzdata files. + * + * Android 10+ moved the up-to-date tzdata location to a module updatable via the Google Play Store and the + * database location changed (https://source.android.com/devices/architecture/modular-system/runtime#time-zone-data-interactions) + * The older locations still exist (at least the `/system/usr/share/zoneinfo` one) but they won't be updated. + */ + private sealed class AndroidTzData + { + [StructLayout(LayoutKind.Sequential, Pack=1)] + private unsafe struct AndroidTzDataHeader + { + public fixed byte signature[12]; + public int indexOffset; + public int dataOffset; + } - [StructLayout(LayoutKind.Sequential, Pack=1)] - internal unsafe struct AndroidTzDataEntry - { - public fixed byte id[40]; - public int byteOffset; - public int length; - public int rawUtcOffset; - } + [StructLayout(LayoutKind.Sequential, Pack=1)] + private unsafe struct AndroidTzDataEntry + { + public fixed byte id[40]; + public int byteOffset; + public int length; + public int rawUtcOffset; + } - /* - * Android v4.3 Timezone support infrastructure. - * - * This is a C# port of libcore.util.ZoneInfoDB: - * - * https://android.googlesource.com/platform/libcore/+/master/luni/src/main/java/libcore/util/ZoneInfoDB.java - * - * This is needed in order to read Android v4.3 tzdata files. - * - * Android 10+ moved the up-to-date tzdata location to a module updatable via the Google Play Store and the - * database location changed (https://source.android.com/devices/architecture/modular-system/runtime#time-zone-data-interactions) - * The older locations still exist (at least the `/system/usr/share/zoneinfo` one) but they won't be updated. - */ - internal sealed class AndroidTzData - { - internal static readonly string[] Paths = new string[] { - GetApexTimeDataRoot() + "/etc/tz/tzdata", // Android 10+, TimeData module where the updates land - GetApexRuntimeRoot() + "/etc/tz/tzdata", // Android 10+, Fallback location if the above isn't found or corrupted - Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/tzdata", - Environment.GetEnvironmentVariable("ANDROID_ROOT") + "/usr/share/zoneinfo/tzdata", - }; + private static readonly string[] Paths = new string[] { + GetApexTimeDataRoot() + "/etc/tz/tzdata", // Android 10+, TimeData module where the updates land + GetApexRuntimeRoot() + "/etc/tz/tzdata", // Android 10+, Fallback location if the above isn't found or corrupted + Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/tzdata", + Environment.GetEnvironmentVariable("ANDROID_ROOT") + "/usr/share/zoneinfo/tzdata", + }; - private string tzdataPath; - private Stream? data; + private string tzdataPath; + private Stream? data; - private string[]? ids; - private int[]? byteOffsets; - private int[]? lengths; + private string[]? ids; + private int[]? byteOffsets; + private int[]? lengths; - public AndroidTzData() - { - foreach (var path in Paths) + public AndroidTzData() { - if (LoadData(path)) + foreach (var path in Paths) { - tzdataPath = path; - return; + if (LoadData(path)) + { + tzdataPath = path; + return; + } } - } - tzdataPath = "/"; - ids = new[]{ "GMT" }; - } - - private static string GetApexTimeDataRoot() - { - string? ret = Environment.GetEnvironmentVariable("ANDROID_TZDATA_ROOT"); - if (!string.IsNullOrEmpty(ret)) { - return ret; + tzdataPath = "/"; + ids = new[]{ "GMT" }; } - return "/apex/com.android.tzdata"; - } - - private static string GetApexRuntimeRoot() - { - string? ret = Environment.GetEnvironmentVariable("ANDROID_RUNTIME_ROOT"); - if (!string.IsNullOrEmpty(ret)) + private static string GetApexTimeDataRoot() { - return ret; - } - - return "/apex/com.android.runtime"; - } + string? ret = Environment.GetEnvironmentVariable("ANDROID_TZDATA_ROOT"); + if (!string.IsNullOrEmpty(ret)) { + return ret; + } - private bool LoadData(string path) - { - if (!File.Exists(path)) - { - return false; + return "/apex/com.android.tzdata"; } - try - { - data = File.OpenRead(path); - } - catch (IOException) + private static string GetApexRuntimeRoot() { - return false; - } - catch (UnauthorizedAccessException) - { - return false; + string? ret = Environment.GetEnvironmentVariable("ANDROID_RUNTIME_ROOT"); + if (!string.IsNullOrEmpty(ret)) + { + return ret; + } + + return "/apex/com.android.runtime"; } - try + private bool LoadData(string path) { - ReadHeader(); - return true; + if (!File.Exists(path)) + { + return false; + } + + try + { + data = File.OpenRead(path); + } + catch (IOException) + { + return false; + } + catch (UnauthorizedAccessException) + { + return false; + } + + try + { + ReadHeader(); + return true; + } + catch + { + return false; + } } - catch + + private unsafe void ReadHeader() { - return false; - } - } + int size = Math.Max(Marshal.SizeOf(typeof(AndroidTzDataHeader)), Marshal.SizeOf(typeof(AndroidTzDataEntry))); + var buffer = new byte[size]; + var header = ReadAt(0, buffer); - private unsafe void ReadHeader() - { - int size = Math.Max(Marshal.SizeOf(typeof(AndroidTzDataHeader)), Marshal.SizeOf(typeof(AndroidTzDataEntry))); - var buffer = new byte[size]; - var header = ReadAt(0, buffer); + header.indexOffset = NetworkToHostOrder(header.indexOffset); + header.dataOffset = NetworkToHostOrder(header.dataOffset); - header.indexOffset = NetworkToHostOrder(header.indexOffset); - header.dataOffset = NetworkToHostOrder(header.dataOffset); + sbyte* s = (sbyte*)header.signature; + string magic = new string(s, 0, 6, Encoding.ASCII); - sbyte* s = (sbyte*)header.signature; - string magic = new string(s, 0, 6, Encoding.ASCII); + if (magic != "tzdata" || header.signature[11] != 0) + { + var b = new StringBuilder(); + b.Append("bad tzdata magic:"); + for (int i = 0; i < 12; ++i) { + b.Append(' ').Append(((byte)s[i]).ToString("x2")); + } - if (magic != "tzdata" || header.signature[11] != 0) - { - var b = new StringBuilder(); - b.Append("bad tzdata magic:"); - for (int i = 0; i < 12; ++i) { - b.Append(' ').Append(((byte)s[i]).ToString("x2")); + //TODO: Put strings in resource file + throw new InvalidOperationException("bad tzdata magic: " + b.ToString()); } - //TODO: Put strings in resource file - throw new InvalidOperationException("bad tzdata magic: " + b.ToString()); + ReadIndex(header.indexOffset, header.dataOffset, buffer); } - ReadIndex(header.indexOffset, header.dataOffset, buffer); - } - - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2087:UnrecognizedReflectionPattern", - Justification = "Implementation detail of Android TimeZone")] - private unsafe T ReadAt(long position, byte[] buffer) - where T : struct - { - int size = Marshal.SizeOf(typeof(T)); - if (buffer.Length < size) + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2087:UnrecognizedReflectionPattern", + Justification = "Implementation detail of Android TimeZone")] + private unsafe T ReadAt(long position, byte[] buffer) + where T : struct { - //TODO: Put strings in resource file - throw new InvalidOperationException("Internal error: buffer too small"); - } + int size = Marshal.SizeOf(typeof(T)); + if (buffer.Length < size) + { + //TODO: Put strings in resource file + throw new InvalidOperationException("private error: buffer too small"); + } - data!.Position = position; - int r; - if ((r = data!.Read(buffer, 0, size)) < size) - { - //TODO: Put strings in resource file - throw new InvalidOperationException( - string.Format("Error reading '{0}': read {1} bytes, expected {2}", tzdataPath, r, size)); + data!.Position = position; + int r; + if ((r = data!.Read(buffer, 0, size)) < size) + { + //TODO: Put strings in resource file + throw new InvalidOperationException( + string.Format("Error reading '{0}': read {1} bytes, expected {2}", tzdataPath, r, size)); + } + + fixed (byte* b = buffer) + { + return (T)Marshal.PtrToStructure((IntPtr)b, typeof(T))!; + } } - fixed (byte* b = buffer) + private static int NetworkToHostOrder(int value) { - return (T)Marshal.PtrToStructure((IntPtr)b, typeof(T))!; + if (!BitConverter.IsLittleEndian) + return value; + + return + (((value >> 24) & 0xFF) | + ((value >> 08) & 0xFF00) | + ((value << 08) & 0xFF0000) | + ((value << 24))); } - } - private static int NetworkToHostOrder(int value) - { - if (!BitConverter.IsLittleEndian) - return value; - - return - (((value >> 24) & 0xFF) | - ((value >> 08) & 0xFF00) | - ((value << 08) & 0xFF0000) | - ((value << 24))); - } - - private static unsafe int GetStringLength(sbyte* s, int maxLength) - { - int len; - for (len = 0; len < maxLength; len++, s++) + private static unsafe int GetStringLength(sbyte* s, int maxLength) { - if (*s == 0) - break; + int len; + for (len = 0; len < maxLength; len++, s++) + { + if (*s == 0) + break; + } + return len; } - return len; - } - private unsafe void ReadIndex(int indexOffset, int dataOffset, byte[] buffer) - { - int indexSize = dataOffset - indexOffset; - int entrySize = Marshal.SizeOf(typeof(AndroidTzDataEntry)); - int entryCount = indexSize / entrySize; - - byteOffsets = new int[entryCount]; - ids = new string[entryCount]; - lengths = new int[entryCount]; - - for (int i = 0; i < entryCount; ++i) + private unsafe void ReadIndex(int indexOffset, int dataOffset, byte[] buffer) { - var entry = ReadAt(indexOffset + (entrySize*i), buffer); - var p = (sbyte*)entry.id; + int indexSize = dataOffset - indexOffset; + int entrySize = Marshal.SizeOf(typeof(AndroidTzDataEntry)); + int entryCount = indexSize / entrySize; - byteOffsets![i] = NetworkToHostOrder(entry.byteOffset) + dataOffset; - ids![i] = new string(p, 0, GetStringLength(p, 40), Encoding.ASCII); - lengths![i] = NetworkToHostOrder(entry.length); + byteOffsets = new int[entryCount]; + ids = new string[entryCount]; + lengths = new int[entryCount]; - if (lengths![i] < Marshal.SizeOf(typeof(AndroidTzDataHeader))) + for (int i = 0; i < entryCount; ++i) { - //TODO: Put strings in resource file - throw new InvalidOperationException("Length in index file < sizeof(tzhead)"); + var entry = ReadAt(indexOffset + (entrySize*i), buffer); + var p = (sbyte*)entry.id; + + byteOffsets![i] = NetworkToHostOrder(entry.byteOffset) + dataOffset; + ids![i] = new string(p, 0, GetStringLength(p, 40), Encoding.ASCII); + lengths![i] = NetworkToHostOrder(entry.length); + + if (lengths![i] < Marshal.SizeOf(typeof(AndroidTzDataHeader))) + { + //TODO: Put strings in resource file + throw new InvalidOperationException("Length in index file < sizeof(tzhead)"); + } } } - } - - public string[] GetTimeZoneIds() - { - return ids!; - } - public byte[] GetTimeZoneData(string id) - { - int i = Array.BinarySearch(ids!, id, StringComparer.Ordinal); - if (i < 0) + public string[] GetTimeZoneIds() { - //TODO: Put strings in resource file - throw new InvalidOperationException("Error finding the timezone id"); + return ids!; } - int offset = byteOffsets![i]; - int length = lengths![i]; - var buffer = new byte[length]; - - lock (data!) + public byte[] GetTimeZoneData(string id) { - data!.Position = offset; - int r; - if ((r = data!.Read(buffer, 0, buffer.Length)) < buffer.Length) + int i = Array.BinarySearch(ids!, id, StringComparer.Ordinal); + if (i < 0) { //TODO: Put strings in resource file - throw new InvalidOperationException( - string.Format("Unable to fully read from file '{0}' at offset {1} length {2}; read {3} bytes expected {4}.", - tzdataPath, offset, length, r, buffer.Length)); + throw new InvalidOperationException("Error finding the timezone id"); + } + + int offset = byteOffsets![i]; + int length = lengths![i]; + var buffer = new byte[length]; + + lock (data!) + { + data!.Position = offset; + int r; + if ((r = data!.Read(buffer, 0, buffer.Length)) < buffer.Length) + { + //TODO: Put strings in resource file + throw new InvalidOperationException( + string.Format("Unable to fully read from file '{0}' at offset {1} length {2}; read {3} bytes expected {4}.", + tzdataPath, offset, length, r, buffer.Length)); + } } - } - return buffer; + return buffer; + } } } } \ No newline at end of file From 4c82a11a4754a290ef97414a02547da199d233a0 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 6 Jul 2021 16:11:56 -0400 Subject: [PATCH 39/81] Remove static Paths, pass tzFilePath as parameter instead of holding --- .../src/System/TimeZoneInfo.Android.cs | 165 ++++++++---------- 1 file changed, 68 insertions(+), 97 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 1396cb1643d67b..c9df293e1a4201 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -125,13 +125,48 @@ private static TimeZoneInfo GetLocalTimeZoneCore() return Utc; } - // TODO could check all paths in AndroidTzData.Paths, or we move the functions that call this into `TimeZoneInfo.Unix.cs` + private static string GetApexTimeDataRoot() + { + string? ret = Environment.GetEnvironmentVariable("ANDROID_TZDATA_ROOT"); + if (!string.IsNullOrEmpty(ret)) + { + return ret; + } + + return "/apex/com.android.tzdata"; + } + + private static string GetApexRuntimeRoot() + { + string? ret = Environment.GetEnvironmentVariable("ANDROID_RUNTIME_ROOT"); + if (!string.IsNullOrEmpty(ret)) + { + return ret; + } + + return "/apex/com.android.runtime"; + } + private static string GetTimeZoneDirectory() { + // Android 10+, TimeData module where the updates land + if (File.Exists(Path.Combine(GetApexTimeDataRoot() + "/etc/tz/", TimeZoneFileName))) + { + return GetApexTimeDataRoot() + "/etc/tz/"; + } + // Android 10+, Fallback location if the above isn't found or corrupted + if (File.Exists(Path.Combine(GetApexRuntimeRoot() + "/etc/tz/", TimeZoneFileName))) + { + return GetApexRuntimeRoot() + "/etc/tz/"; + } + if (File.Exists(Path.Combine(Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/", TimeZoneFileName))) + { + return Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/"; + } + return Environment.GetEnvironmentVariable("ANDROID_ROOT") + DefaultTimeZoneDirectory; } - //TODO: TryGetTimeZoneFromLocalMachine maps to FindSystemTimeZoneByIdCore in mono/mono implementation private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, out TimeZoneInfo? value, out Exception? e) { value = null; @@ -175,6 +210,7 @@ private unsafe struct AndroidTzDataHeader public fixed byte signature[12]; public int indexOffset; public int dataOffset; + // Do we need zoneTabOFfset? Whats the format of tzdata now vs during mono/mono implementation. } [StructLayout(LayoutKind.Sequential, Pack=1)] @@ -186,92 +222,20 @@ private unsafe struct AndroidTzDataEntry public int rawUtcOffset; } - private static readonly string[] Paths = new string[] { - GetApexTimeDataRoot() + "/etc/tz/tzdata", // Android 10+, TimeData module where the updates land - GetApexRuntimeRoot() + "/etc/tz/tzdata", // Android 10+, Fallback location if the above isn't found or corrupted - Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/tzdata", - Environment.GetEnvironmentVariable("ANDROID_ROOT") + "/usr/share/zoneinfo/tzdata", - }; - - private string tzdataPath; - private Stream? data; - private string[]? ids; private int[]? byteOffsets; private int[]? lengths; public AndroidTzData() { - foreach (var path in Paths) - { - if (LoadData(path)) - { - tzdataPath = path; - return; - } - } - - tzdataPath = "/"; - ids = new[]{ "GMT" }; - } - - private static string GetApexTimeDataRoot() - { - string? ret = Environment.GetEnvironmentVariable("ANDROID_TZDATA_ROOT"); - if (!string.IsNullOrEmpty(ret)) { - return ret; - } - - return "/apex/com.android.tzdata"; - } - - private static string GetApexRuntimeRoot() - { - string? ret = Environment.GetEnvironmentVariable("ANDROID_RUNTIME_ROOT"); - if (!string.IsNullOrEmpty(ret)) - { - return ret; - } - - return "/apex/com.android.runtime"; - } - - private bool LoadData(string path) - { - if (!File.Exists(path)) - { - return false; - } - - try - { - data = File.OpenRead(path); - } - catch (IOException) - { - return false; - } - catch (UnauthorizedAccessException) - { - return false; - } - - try - { - ReadHeader(); - return true; - } - catch - { - return false; - } + ReadHeader(GetTimeZoneDirectory() + TimeZoneFileName); } - private unsafe void ReadHeader() + private unsafe void ReadHeader(string tzFilePath) { int size = Math.Max(Marshal.SizeOf(typeof(AndroidTzDataHeader)), Marshal.SizeOf(typeof(AndroidTzDataEntry))); var buffer = new byte[size]; - var header = ReadAt(0, buffer); + var header = ReadAt(tzFilePath, 0, buffer); header.indexOffset = NetworkToHostOrder(header.indexOffset); header.dataOffset = NetworkToHostOrder(header.dataOffset); @@ -290,13 +254,15 @@ private unsafe void ReadHeader() //TODO: Put strings in resource file throw new InvalidOperationException("bad tzdata magic: " + b.ToString()); } + // What exactly are we considering bad tzdata? Seems like if it doesnt start with "tzdata" or if the signature is filled. + // How does filling the AndroidTzDataHeader work? Shouldn't signature be filled up, so its always != 0? - ReadIndex(header.indexOffset, header.dataOffset, buffer); + ReadIndex(tzFilePath, header.indexOffset, header.dataOffset, buffer); } [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2087:UnrecognizedReflectionPattern", Justification = "Implementation detail of Android TimeZone")] - private unsafe T ReadAt(long position, byte[] buffer) + private unsafe T ReadAt(string tzFilePath, long position, byte[] buffer) where T : struct { int size = Marshal.SizeOf(typeof(T)); @@ -306,18 +272,21 @@ private unsafe T ReadAt(long position, byte[] buffer) throw new InvalidOperationException("private error: buffer too small"); } - data!.Position = position; - int r; - if ((r = data!.Read(buffer, 0, size)) < size) + using (FileStream fs = File.OpenRead(tzFilePath)) { - //TODO: Put strings in resource file - throw new InvalidOperationException( - string.Format("Error reading '{0}': read {1} bytes, expected {2}", tzdataPath, r, size)); - } + fs.Position = position; + int numBytesRead; + if ((numBytesRead = fs.Read(buffer, 0, size)) < size) + { + //TODO: Put strings in resource file + throw new InvalidOperationException( + string.Format("Error reading '{0}': read {1} bytes, expected {2}", GetTimeZoneDirectory() + TimeZoneFileName, numBytesRead, size)); + } - fixed (byte* b = buffer) - { - return (T)Marshal.PtrToStructure((IntPtr)b, typeof(T))!; + fixed (byte* b = buffer) + { + return (T)Marshal.PtrToStructure((IntPtr)b, typeof(T))!; // Is ! the right way to handle Unboxing a possibly null value. Should there be some check instead? + } } } @@ -344,7 +313,8 @@ private static unsafe int GetStringLength(sbyte* s, int maxLength) return len; } - private unsafe void ReadIndex(int indexOffset, int dataOffset, byte[] buffer) + // What does the TZdata index look like? + private unsafe void ReadIndex(string tzFilePath, int indexOffset, int dataOffset, byte[] buffer) { int indexSize = dataOffset - indexOffset; int entrySize = Marshal.SizeOf(typeof(AndroidTzDataEntry)); @@ -356,7 +326,7 @@ private unsafe void ReadIndex(int indexOffset, int dataOffset, byte[] buffer) for (int i = 0; i < entryCount; ++i) { - var entry = ReadAt(indexOffset + (entrySize*i), buffer); + var entry = ReadAt(tzFilePath, indexOffset + (entrySize*i), buffer); var p = (sbyte*)entry.id; byteOffsets![i] = NetworkToHostOrder(entry.byteOffset) + dataOffset; @@ -388,17 +358,18 @@ public byte[] GetTimeZoneData(string id) int offset = byteOffsets![i]; int length = lengths![i]; var buffer = new byte[length]; - - lock (data!) + // Do we need to lock to prevent multithreading issues like the mono/mono implementation? + var tzFilePath = GetTimeZoneDirectory() + TimeZoneFileName; + using (FileStream fs = File.OpenRead(tzFilePath)) { - data!.Position = offset; - int r; - if ((r = data!.Read(buffer, 0, buffer.Length)) < buffer.Length) + fs.Position = offset; + int numBytesRead; + if ((numBytesRead = fs.Read(buffer, 0, buffer.Length)) < buffer.Length) { //TODO: Put strings in resource file throw new InvalidOperationException( string.Format("Unable to fully read from file '{0}' at offset {1} length {2}; read {3} bytes expected {4}.", - tzdataPath, offset, length, r, buffer.Length)); + tzFilePath, offset, length, numBytesRead, buffer.Length)); } } From 3cc1da19c9dd49226aedfea37d76b0a92badb3ad Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 6 Jul 2021 16:57:11 -0400 Subject: [PATCH 40/81] Add ordinal comparison for performance gain --- .../System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index c9df293e1a4201..9c9e255ef6497a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -91,7 +91,7 @@ private static int ParseGMTNumericZone(string name) { return new TimeZoneInfo(id, TimeSpan.FromSeconds(0), id, name, name, null, disableDaylightSavingTime:true); } - if (name.StartsWith("GMT")) + if (name.StartsWith("GMT", StringComparison.Ordinal)) { return new TimeZoneInfo(id, TimeSpan.FromSeconds(ParseGMTNumericZone(name)), id, name, name, null, disableDaylightSavingTime:true); } From 4df973a67dd4e8814ceb120f64cc17d74783d00f Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 6 Jul 2021 16:57:40 -0400 Subject: [PATCH 41/81] Add Softcode Local --- .../System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 9c9e255ef6497a..e6598ed0cc453a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -173,7 +173,7 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, e = null; // mono/mono FindSystemTimeZoneById suggests Local scenario - value = id == "Local" ? GetLocalTimeZoneCore() : GetTimeZone(id, id); + value = id == LocalId ? GetLocalTimeZoneCore() : GetTimeZone(id, id); if (value == null) { From b4d5ba6de610cef02360e212ba4bfa103bf29219 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 6 Jul 2021 16:58:21 -0400 Subject: [PATCH 42/81] Softcode tzdata file path --- .../System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index e6598ed0cc453a..8eee67f9ec0e95 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -280,7 +280,7 @@ private unsafe T ReadAt(string tzFilePath, long position, byte[] buffer) { //TODO: Put strings in resource file throw new InvalidOperationException( - string.Format("Error reading '{0}': read {1} bytes, expected {2}", GetTimeZoneDirectory() + TimeZoneFileName, numBytesRead, size)); + string.Format("Error reading '{0}': read {1} bytes, expected {2}", tzFilePath, numBytesRead, size)); } fixed (byte* b = buffer) From 2bb25ce79e5f77c5e915f659515ba6225ccd8f02 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 6 Jul 2021 16:58:42 -0400 Subject: [PATCH 43/81] Refactor Unix specific code --- .../src/System/TimeZoneInfo.AnyUnix.cs | 301 ------------------ .../src/System/TimeZoneInfo.Unix.cs | 301 ++++++++++++++++++ 2 files changed, 301 insertions(+), 301 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs index 5f66e01189dd44..3300025ff761b7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs @@ -17,7 +17,6 @@ namespace System public sealed partial class TimeZoneInfo { private const string DefaultTimeZoneDirectory = "/usr/share/zoneinfo/"; - private const string TimeZoneEnvironmentVariable = "TZ"; // UTC aliases per https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml // Hard-coded because we need to treat all aliases of UTC the same even when globalization data is not available. @@ -252,306 +251,6 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out return TryGetTimeZoneFromLocalMachineCore(id, out value, out e); } - /// - /// Gets the tzfile raw data for the current 'local' time zone using the following rules. - /// 1. Read the TZ environment variable. If it is set, use it. - /// 2. Look for the data in /etc/localtime. - /// 3. Look for the data in GetTimeZoneDirectory()/localtime. - /// 4. Use UTC if all else fails. - /// - private static bool TryGetLocalTzFile([NotNullWhen(true)] out byte[]? rawData, [NotNullWhen(true)] out string? id) - { - rawData = null; - id = null; - string? tzVariable = GetTzEnvironmentVariable(); - - // If the env var is null, use the localtime file - if (tzVariable == null) - { - return - TryLoadTzFile("/etc/localtime", ref rawData, ref id) || - TryLoadTzFile(Path.Combine(GetTimeZoneDirectory(), "localtime"), ref rawData, ref id); - } - - // If it's empty, use UTC (TryGetLocalTzFile() should return false). - if (tzVariable.Length == 0) - { - return false; - } - - // Otherwise, use the path from the env var. If it's not absolute, make it relative - // to the system timezone directory - string tzFilePath; - if (tzVariable[0] != '/') - { - id = tzVariable; - tzFilePath = Path.Combine(GetTimeZoneDirectory(), tzVariable); - } - else - { - tzFilePath = tzVariable; - } - return TryLoadTzFile(tzFilePath, ref rawData, ref id); - } - - private static string? GetTzEnvironmentVariable() - { - string? result = Environment.GetEnvironmentVariable(TimeZoneEnvironmentVariable); - if (!string.IsNullOrEmpty(result)) - { - if (result[0] == ':') - { - // strip off the ':' prefix - result = result.Substring(1); - } - } - - return result; - } - - private static bool TryLoadTzFile(string tzFilePath, [NotNullWhen(true)] ref byte[]? rawData, [NotNullWhen(true)] ref string? id) - { - if (File.Exists(tzFilePath)) - { - try - { - rawData = File.ReadAllBytes(tzFilePath); - if (string.IsNullOrEmpty(id)) - { - id = FindTimeZoneIdUsingReadLink(tzFilePath); - - if (string.IsNullOrEmpty(id)) - { - id = FindTimeZoneId(rawData); - } - } - return true; - } - catch (IOException) { } - catch (SecurityException) { } - catch (UnauthorizedAccessException) { } - } - return false; - } - - /// - /// Finds the time zone id by using 'readlink' on the path to see if tzFilePath is - /// a symlink to a file. - /// - private static string? FindTimeZoneIdUsingReadLink(string tzFilePath) - { - string? id = null; - - string? symlinkPath = Interop.Sys.ReadLink(tzFilePath); - if (symlinkPath != null) - { - // symlinkPath can be relative path, use Path to get the full absolute path. - symlinkPath = Path.GetFullPath(symlinkPath, Path.GetDirectoryName(tzFilePath)!); - - string timeZoneDirectory = GetTimeZoneDirectory(); - if (symlinkPath.StartsWith(timeZoneDirectory, StringComparison.Ordinal)) - { - id = symlinkPath.Substring(timeZoneDirectory.Length); - } - } - - return id; - } - - private static string? GetDirectoryEntryFullPath(ref Interop.Sys.DirectoryEntry dirent, string currentPath) - { - ReadOnlySpan direntName = dirent.GetName(stackalloc char[Interop.Sys.DirectoryEntry.NameBufferSize]); - - if ((direntName.Length == 1 && direntName[0] == '.') || - (direntName.Length == 2 && direntName[0] == '.' && direntName[1] == '.')) - return null; - - return Path.Join(currentPath.AsSpan(), direntName); - } - - /// - /// Enumerate files - /// - private static unsafe void EnumerateFilesRecursively(string path, Predicate condition) - { - List? toExplore = null; // List used as a stack - - int bufferSize = Interop.Sys.GetReadDirRBufferSize(); - byte[]? dirBuffer = null; - try - { - dirBuffer = ArrayPool.Shared.Rent(bufferSize); - string currentPath = path; - - fixed (byte* dirBufferPtr = dirBuffer) - { - while (true) - { - IntPtr dirHandle = Interop.Sys.OpenDir(currentPath); - if (dirHandle == IntPtr.Zero) - { - throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo(), currentPath, isDirectory: true); - } - - try - { - // Read each entry from the enumerator - Interop.Sys.DirectoryEntry dirent; - while (Interop.Sys.ReadDirR(dirHandle, dirBufferPtr, bufferSize, out dirent) == 0) - { - string? fullPath = GetDirectoryEntryFullPath(ref dirent, currentPath); - if (fullPath == null) - continue; - - // Get from the dir entry whether the entry is a file or directory. - // We classify everything as a file unless we know it to be a directory. - bool isDir; - if (dirent.InodeType == Interop.Sys.NodeType.DT_DIR) - { - // We know it's a directory. - isDir = true; - } - else if (dirent.InodeType == Interop.Sys.NodeType.DT_LNK || dirent.InodeType == Interop.Sys.NodeType.DT_UNKNOWN) - { - // It's a symlink or unknown: stat to it to see if we can resolve it to a directory. - // If we can't (e.g. symlink to a file, broken symlink, etc.), we'll just treat it as a file. - - Interop.Sys.FileStatus fileinfo; - if (Interop.Sys.Stat(fullPath, out fileinfo) >= 0) - { - isDir = (fileinfo.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR; - } - else - { - isDir = false; - } - } - else - { - // Otherwise, treat it as a file. This includes regular files, FIFOs, etc. - isDir = false; - } - - // Yield the result if the user has asked for it. In the case of directories, - // always explore it by pushing it onto the stack, regardless of whether - // we're returning directories. - if (isDir) - { - toExplore ??= new List(); - toExplore.Add(fullPath); - } - else if (condition(fullPath)) - { - return; - } - } - } - finally - { - if (dirHandle != IntPtr.Zero) - Interop.Sys.CloseDir(dirHandle); - } - - if (toExplore == null || toExplore.Count == 0) - break; - - currentPath = toExplore[toExplore.Count - 1]; - toExplore.RemoveAt(toExplore.Count - 1); - } - } - } - finally - { - if (dirBuffer != null) - ArrayPool.Shared.Return(dirBuffer); - } - } - - /// - /// Find the time zone id by searching all the tzfiles for the one that matches rawData - /// and return its file name. - /// - private static string FindTimeZoneId(byte[] rawData) - { - // default to "Local" if we can't find the right tzfile - string id = LocalId; - string timeZoneDirectory = GetTimeZoneDirectory(); - string localtimeFilePath = Path.Combine(timeZoneDirectory, "localtime"); - string posixrulesFilePath = Path.Combine(timeZoneDirectory, "posixrules"); - byte[] buffer = new byte[rawData.Length]; - - try - { - EnumerateFilesRecursively(timeZoneDirectory, (string filePath) => - { - // skip the localtime and posixrules file, since they won't give us the correct id - if (!string.Equals(filePath, localtimeFilePath, StringComparison.OrdinalIgnoreCase) - && !string.Equals(filePath, posixrulesFilePath, StringComparison.OrdinalIgnoreCase)) - { - if (CompareTimeZoneFile(filePath, buffer, rawData)) - { - // if all bytes are the same, this must be the right tz file - id = filePath; - - // strip off the root time zone directory - if (id.StartsWith(timeZoneDirectory, StringComparison.Ordinal)) - { - id = id.Substring(timeZoneDirectory.Length); - } - return true; - } - } - return false; - }); - } - catch (IOException) { } - catch (SecurityException) { } - catch (UnauthorizedAccessException) { } - - return id; - } - - private static bool CompareTimeZoneFile(string filePath, byte[] buffer, byte[] rawData) - { - try - { - // bufferSize == 1 used to avoid unnecessary buffer in FileStream - using (FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1)) - { - if (stream.Length == rawData.Length) - { - int index = 0; - int count = rawData.Length; - - while (count > 0) - { - int n = stream.Read(buffer, index, count); - if (n == 0) - ThrowHelper.ThrowEndOfFileException(); - - int end = index + n; - for (; index < end; index++) - { - if (buffer[index] != rawData[index]) - { - return false; - } - } - - count -= n; - } - - return true; - } - } - } - catch (IOException) { } - catch (SecurityException) { } - catch (UnauthorizedAccessException) { } - - return false; - } - private static TimeZoneInfo? GetTimeZoneFromTzData(byte[]? rawData, string id) { if (rawData != null) 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 4f4edaf2c44798..e4ee98ce5784cc 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -11,6 +11,7 @@ public sealed partial class TimeZoneInfo { private const string TimeZoneFileName = "zone.tab"; private const string TimeZoneDirectoryEnvironmentVariable = "TZDIR"; + private const string TimeZoneEnvironmentVariable = "TZ"; private static TimeZoneInfo GetLocalTimeZoneCore() { @@ -118,6 +119,306 @@ private static List GetTimeZoneIds() return timeZoneIds; } + private static string? GetTzEnvironmentVariable() + { + string? result = Environment.GetEnvironmentVariable(TimeZoneEnvironmentVariable); + if (!string.IsNullOrEmpty(result)) + { + if (result[0] == ':') + { + // strip off the ':' prefix + result = result.Substring(1); + } + } + + return result; + } + + /// + /// Finds the time zone id by using 'readlink' on the path to see if tzFilePath is + /// a symlink to a file. + /// + private static string? FindTimeZoneIdUsingReadLink(string tzFilePath) + { + string? id = null; + + string? symlinkPath = Interop.Sys.ReadLink(tzFilePath); + if (symlinkPath != null) + { + // symlinkPath can be relative path, use Path to get the full absolute path. + symlinkPath = Path.GetFullPath(symlinkPath, Path.GetDirectoryName(tzFilePath)!); + + string timeZoneDirectory = GetTimeZoneDirectory(); + if (symlinkPath.StartsWith(timeZoneDirectory, StringComparison.Ordinal)) + { + id = symlinkPath.Substring(timeZoneDirectory.Length); + } + } + + return id; + } + + private static string? GetDirectoryEntryFullPath(ref Interop.Sys.DirectoryEntry dirent, string currentPath) + { + ReadOnlySpan direntName = dirent.GetName(stackalloc char[Interop.Sys.DirectoryEntry.NameBufferSize]); + + if ((direntName.Length == 1 && direntName[0] == '.') || + (direntName.Length == 2 && direntName[0] == '.' && direntName[1] == '.')) + return null; + + return Path.Join(currentPath.AsSpan(), direntName); + } + + /// + /// Enumerate files + /// + private static unsafe void EnumerateFilesRecursively(string path, Predicate condition) + { + List? toExplore = null; // List used as a stack + + int bufferSize = Interop.Sys.GetReadDirRBufferSize(); + byte[]? dirBuffer = null; + try + { + dirBuffer = ArrayPool.Shared.Rent(bufferSize); + string currentPath = path; + + fixed (byte* dirBufferPtr = dirBuffer) + { + while (true) + { + IntPtr dirHandle = Interop.Sys.OpenDir(currentPath); + if (dirHandle == IntPtr.Zero) + { + throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo(), currentPath, isDirectory: true); + } + + try + { + // Read each entry from the enumerator + Interop.Sys.DirectoryEntry dirent; + while (Interop.Sys.ReadDirR(dirHandle, dirBufferPtr, bufferSize, out dirent) == 0) + { + string? fullPath = GetDirectoryEntryFullPath(ref dirent, currentPath); + if (fullPath == null) + continue; + + // Get from the dir entry whether the entry is a file or directory. + // We classify everything as a file unless we know it to be a directory. + bool isDir; + if (dirent.InodeType == Interop.Sys.NodeType.DT_DIR) + { + // We know it's a directory. + isDir = true; + } + else if (dirent.InodeType == Interop.Sys.NodeType.DT_LNK || dirent.InodeType == Interop.Sys.NodeType.DT_UNKNOWN) + { + // It's a symlink or unknown: stat to it to see if we can resolve it to a directory. + // If we can't (e.g. symlink to a file, broken symlink, etc.), we'll just treat it as a file. + + Interop.Sys.FileStatus fileinfo; + if (Interop.Sys.Stat(fullPath, out fileinfo) >= 0) + { + isDir = (fileinfo.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR; + } + else + { + isDir = false; + } + } + else + { + // Otherwise, treat it as a file. This includes regular files, FIFOs, etc. + isDir = false; + } + + // Yield the result if the user has asked for it. In the case of directories, + // always explore it by pushing it onto the stack, regardless of whether + // we're returning directories. + if (isDir) + { + toExplore ??= new List(); + toExplore.Add(fullPath); + } + else if (condition(fullPath)) + { + return; + } + } + } + finally + { + if (dirHandle != IntPtr.Zero) + Interop.Sys.CloseDir(dirHandle); + } + + if (toExplore == null || toExplore.Count == 0) + break; + + currentPath = toExplore[toExplore.Count - 1]; + toExplore.RemoveAt(toExplore.Count - 1); + } + } + } + finally + { + if (dirBuffer != null) + ArrayPool.Shared.Return(dirBuffer); + } + } + + private static bool CompareTimeZoneFile(string filePath, byte[] buffer, byte[] rawData) + { + try + { + // bufferSize == 1 used to avoid unnecessary buffer in FileStream + using (FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1)) + { + if (stream.Length == rawData.Length) + { + int index = 0; + int count = rawData.Length; + + while (count > 0) + { + int n = stream.Read(buffer, index, count); + if (n == 0) + ThrowHelper.ThrowEndOfFileException(); + + int end = index + n; + for (; index < end; index++) + { + if (buffer[index] != rawData[index]) + { + return false; + } + } + + count -= n; + } + + return true; + } + } + } + catch (IOException) { } + catch (SecurityException) { } + catch (UnauthorizedAccessException) { } + + return false; + } + + /// + /// Find the time zone id by searching all the tzfiles for the one that matches rawData + /// and return its file name. + /// + private static string FindTimeZoneId(byte[] rawData) + { + // default to "Local" if we can't find the right tzfile + string id = LocalId; + string timeZoneDirectory = GetTimeZoneDirectory(); + string localtimeFilePath = Path.Combine(timeZoneDirectory, "localtime"); + string posixrulesFilePath = Path.Combine(timeZoneDirectory, "posixrules"); + byte[] buffer = new byte[rawData.Length]; + + try + { + EnumerateFilesRecursively(timeZoneDirectory, (string filePath) => + { + // skip the localtime and posixrules file, since they won't give us the correct id + if (!string.Equals(filePath, localtimeFilePath, StringComparison.OrdinalIgnoreCase) + && !string.Equals(filePath, posixrulesFilePath, StringComparison.OrdinalIgnoreCase)) + { + if (CompareTimeZoneFile(filePath, buffer, rawData)) + { + // if all bytes are the same, this must be the right tz file + id = filePath; + + // strip off the root time zone directory + if (id.StartsWith(timeZoneDirectory, StringComparison.Ordinal)) + { + id = id.Substring(timeZoneDirectory.Length); + } + return true; + } + } + return false; + }); + } + catch (IOException) { } + catch (SecurityException) { } + catch (UnauthorizedAccessException) { } + + return id; + } + + private static bool TryLoadTzFile(string tzFilePath, [NotNullWhen(true)] ref byte[]? rawData, [NotNullWhen(true)] ref string? id) + { + if (File.Exists(tzFilePath)) + { + try + { + rawData = File.ReadAllBytes(tzFilePath); + if (string.IsNullOrEmpty(id)) + { + id = FindTimeZoneIdUsingReadLink(tzFilePath); + + if (string.IsNullOrEmpty(id)) + { + id = FindTimeZoneId(rawData); + } + } + return true; + } + catch (IOException) { } + catch (SecurityException) { } + catch (UnauthorizedAccessException) { } + } + return false; + } + + /// + /// Gets the tzfile raw data for the current 'local' time zone using the following rules. + /// 1. Read the TZ environment variable. If it is set, use it. + /// 2. Look for the data in /etc/localtime. + /// 3. Look for the data in GetTimeZoneDirectory()/localtime. + /// 4. Use UTC if all else fails. + /// + private static bool TryGetLocalTzFile([NotNullWhen(true)] out byte[]? rawData, [NotNullWhen(true)] out string? id) + { + rawData = null; + id = null; + string? tzVariable = GetTzEnvironmentVariable(); + + // If the env var is null, use the localtime file + if (tzVariable == null) + { + return + TryLoadTzFile("/etc/localtime", ref rawData, ref id) || + TryLoadTzFile(Path.Combine(GetTimeZoneDirectory(), "localtime"), ref rawData, ref id); + } + + // If it's empty, use UTC (TryGetLocalTzFile() should return false). + if (tzVariable.Length == 0) + { + return false; + } + + // Otherwise, use the path from the env var. If it's not absolute, make it relative + // to the system timezone directory + string tzFilePath; + if (tzVariable[0] != '/') + { + id = tzVariable; + tzFilePath = Path.Combine(GetTimeZoneDirectory(), tzVariable); + } + else + { + tzFilePath = tzVariable; + } + return TryLoadTzFile(tzFilePath, ref rawData, ref id); + } + /// /// Helper function used by 'GetLocalTimeZone()' - this function wraps the call /// for loading time zone data from computers without Registry support. From efa339ef278f8f2534a2c947b09318f7c5edc937 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 6 Jul 2021 17:22:22 -0400 Subject: [PATCH 44/81] Lazy allocation of Android TimeZone Data --- .../src/System/TimeZoneInfo.Android.cs | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 8eee67f9ec0e95..c7e576d4049954 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -13,7 +13,27 @@ public sealed partial class TimeZoneInfo { private const string TimeZoneFileName = "tzdata"; - private static AndroidTzData tzData = new AndroidTzData(); + private static AndroidTzData? tzData; + private static readonly object tzDataLock = new object(); + + private static AndroidTzData atzData + { + get + { + if (tzData == null) + { + lock (tzDataLock) + { + if (tzData == null) + { + tzData = new AndroidTzData(); + } + } + } + + return tzData; + } + } // This should be called when name begins with GMT private static int ParseGMTNumericZone(string name) @@ -98,7 +118,7 @@ private static int ParseGMTNumericZone(string name) try { - byte[] buffer = tzData.GetTimeZoneData(name); + byte[] buffer = atzData.GetTimeZoneData(name); return GetTimeZoneFromTzData(buffer, id); } catch @@ -186,7 +206,7 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, private static string[] GetTimeZoneIds() { - return tzData.GetTimeZoneIds(); + return atzData.GetTimeZoneIds(); } /* From 54e3218940b1779004de5b06d6a1f2c148147e99 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 7 Jul 2021 09:38:31 -0400 Subject: [PATCH 45/81] Fix up imports --- .../System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs | 1 - .../System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs index 3300025ff761b7..6824b113137fad 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs @@ -1,7 +1,6 @@ // 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.Binary; using System.Collections.Generic; using System.Diagnostics; 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 e4ee98ce5784cc..6ae232df560894 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -1,9 +1,12 @@ // 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.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text; +using System.Security; namespace System { From 144f4dd01f95cf9ce1235bf6be401b5752b860fa Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 7 Jul 2021 14:50:44 -0400 Subject: [PATCH 46/81] Apply coding style --- .../src/System/TimeZoneInfo.Android.cs | 72 +++++++++---------- .../src/System/TimeZoneInfo.AnyUnix.cs | 2 +- .../src/System/TimeZoneInfo.Unix.cs | 14 ++-- 3 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index c7e576d4049954..cf5b413c866e38 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -11,27 +11,27 @@ namespace System { public sealed partial class TimeZoneInfo { - private const string TimeZoneFileName = "tzdata"; + private const string _timeZoneFileName = "tzdata"; - private static AndroidTzData? tzData; - private static readonly object tzDataLock = new object(); + private static AndroidTzData? s_tzData; + private static readonly object s_tzDataLock = new object(); - private static AndroidTzData atzData + private static AndroidTzData s_atzData { get { - if (tzData == null) + if (s_tzData == null) { - lock (tzDataLock) + lock (s_tzDataLock) { - if (tzData == null) + if (s_tzData == null) { - tzData = new AndroidTzData(); + s_tzData = new AndroidTzData(); } } } - return tzData; + return s_tzData; } } @@ -118,7 +118,7 @@ private static int ParseGMTNumericZone(string name) try { - byte[] buffer = atzData.GetTimeZoneData(name); + byte[] buffer = s_atzData.GetTimeZoneData(name); return GetTimeZoneFromTzData(buffer, id); } catch @@ -130,10 +130,10 @@ private static int ParseGMTNumericZone(string name) private static TimeZoneInfo GetLocalTimeZoneCore() { - var id = Interop.Sys.GetDefaultTimeZone(); + string? id = Interop.Sys.GetDefaultTimeZone(); if (!string.IsNullOrEmpty(id)) { - var defaultTimeZone = GetTimeZone(id, id); + TimeZoneInfo? defaultTimeZone = GetTimeZone(id, id); if (defaultTimeZone != null) { @@ -170,21 +170,21 @@ private static string GetApexRuntimeRoot() private static string GetTimeZoneDirectory() { // Android 10+, TimeData module where the updates land - if (File.Exists(Path.Combine(GetApexTimeDataRoot() + "/etc/tz/", TimeZoneFileName))) + if (File.Exists(Path.Combine(GetApexTimeDataRoot() + "/etc/tz/", _timeZoneFileName))) { return GetApexTimeDataRoot() + "/etc/tz/"; } // Android 10+, Fallback location if the above isn't found or corrupted - if (File.Exists(Path.Combine(GetApexRuntimeRoot() + "/etc/tz/", TimeZoneFileName))) + if (File.Exists(Path.Combine(GetApexRuntimeRoot() + "/etc/tz/", _timeZoneFileName))) { return GetApexRuntimeRoot() + "/etc/tz/"; } - if (File.Exists(Path.Combine(Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/", TimeZoneFileName))) + if (File.Exists(Path.Combine(Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/", _timeZoneFileName))) { return Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/"; } - return Environment.GetEnvironmentVariable("ANDROID_ROOT") + DefaultTimeZoneDirectory; + return Environment.GetEnvironmentVariable("ANDROID_ROOT") + _defaultTimeZoneDirectory; } private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, out TimeZoneInfo? value, out Exception? e) @@ -197,7 +197,7 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, if (value == null) { - e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, GetTimeZoneDirectory() + TimeZoneFileName)); + e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, GetTimeZoneDirectory() + _timeZoneFileName)); return TimeZoneInfoResult.TimeZoneNotFoundException; // Mono/mono throws TimeZoneNotFoundException, runtime throws InvalidTimeZoneException } @@ -206,7 +206,7 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, private static string[] GetTimeZoneIds() { - return atzData.GetTimeZoneIds(); + return s_atzData.GetTimeZoneIds(); } /* @@ -242,20 +242,20 @@ private unsafe struct AndroidTzDataEntry public int rawUtcOffset; } - private string[]? ids; - private int[]? byteOffsets; - private int[]? lengths; + private string[]? _ids; + private int[]? _byteOffsets; + private int[]? _lengths; public AndroidTzData() { - ReadHeader(GetTimeZoneDirectory() + TimeZoneFileName); + ReadHeader(GetTimeZoneDirectory() + _timeZoneFileName); } private unsafe void ReadHeader(string tzFilePath) { int size = Math.Max(Marshal.SizeOf(typeof(AndroidTzDataHeader)), Marshal.SizeOf(typeof(AndroidTzDataEntry))); var buffer = new byte[size]; - var header = ReadAt(tzFilePath, 0, buffer); + AndroidTzDataHeader header = ReadAt(tzFilePath, 0, buffer); header.indexOffset = NetworkToHostOrder(header.indexOffset); header.dataOffset = NetworkToHostOrder(header.dataOffset); @@ -340,20 +340,20 @@ private unsafe void ReadIndex(string tzFilePath, int indexOffset, int dataOffset int entrySize = Marshal.SizeOf(typeof(AndroidTzDataEntry)); int entryCount = indexSize / entrySize; - byteOffsets = new int[entryCount]; - ids = new string[entryCount]; - lengths = new int[entryCount]; + _byteOffsets = new int[entryCount]; + _ids = new string[entryCount]; + _lengths = new int[entryCount]; for (int i = 0; i < entryCount; ++i) { - var entry = ReadAt(tzFilePath, indexOffset + (entrySize*i), buffer); + AndroidTzDataEntry entry = ReadAt(tzFilePath, indexOffset + (entrySize*i), buffer); var p = (sbyte*)entry.id; - byteOffsets![i] = NetworkToHostOrder(entry.byteOffset) + dataOffset; - ids![i] = new string(p, 0, GetStringLength(p, 40), Encoding.ASCII); - lengths![i] = NetworkToHostOrder(entry.length); + _byteOffsets![i] = NetworkToHostOrder(entry.byteOffset) + dataOffset; + _ids![i] = new string(p, 0, GetStringLength(p, 40), Encoding.ASCII); + _lengths![i] = NetworkToHostOrder(entry.length); - if (lengths![i] < Marshal.SizeOf(typeof(AndroidTzDataHeader))) + if (_lengths![i] < Marshal.SizeOf(typeof(AndroidTzDataHeader))) { //TODO: Put strings in resource file throw new InvalidOperationException("Length in index file < sizeof(tzhead)"); @@ -363,23 +363,23 @@ private unsafe void ReadIndex(string tzFilePath, int indexOffset, int dataOffset public string[] GetTimeZoneIds() { - return ids!; + return _ids!; } public byte[] GetTimeZoneData(string id) { - int i = Array.BinarySearch(ids!, id, StringComparer.Ordinal); + int i = Array.BinarySearch(_ids!, id, StringComparer.Ordinal); if (i < 0) { //TODO: Put strings in resource file throw new InvalidOperationException("Error finding the timezone id"); } - int offset = byteOffsets![i]; - int length = lengths![i]; + int offset = _byteOffsets![i]; + int length = _lengths![i]; var buffer = new byte[length]; // Do we need to lock to prevent multithreading issues like the mono/mono implementation? - var tzFilePath = GetTimeZoneDirectory() + TimeZoneFileName; + string tzFilePath = GetTimeZoneDirectory() + _timeZoneFileName; using (FileStream fs = File.OpenRead(tzFilePath)) { fs.Position = offset; diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs index 6824b113137fad..7c2ddea4baf2fa 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs @@ -15,7 +15,7 @@ namespace System { public sealed partial class TimeZoneInfo { - private const string DefaultTimeZoneDirectory = "/usr/share/zoneinfo/"; + private const string _defaultTimeZoneDirectory = "/usr/share/zoneinfo/"; // UTC aliases per https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml // Hard-coded because we need to treat all aliases of UTC the same even when globalization data is not available. 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 6ae232df560894..dc28796eae32d5 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -12,9 +12,9 @@ namespace System { public sealed partial class TimeZoneInfo { - private const string TimeZoneFileName = "zone.tab"; - private const string TimeZoneDirectoryEnvironmentVariable = "TZDIR"; - private const string TimeZoneEnvironmentVariable = "TZ"; + private const string _timeZoneFileName = "zone.tab"; + private const string _timeZoneDirectoryEnvironmentVariable = "TZDIR"; + private const string _timeZoneEnvironmentVariable = "TZ"; private static TimeZoneInfo GetLocalTimeZoneCore() { @@ -78,7 +78,7 @@ private static List GetTimeZoneIds() try { - using (StreamReader sr = new StreamReader(Path.Combine(GetTimeZoneDirectory(), TimeZoneFileName), Encoding.UTF8)) + using (StreamReader sr = new StreamReader(Path.Combine(GetTimeZoneDirectory(), _timeZoneFileName), Encoding.UTF8)) { string? zoneTabFileLine; while ((zoneTabFileLine = sr.ReadLine()) != null) @@ -124,7 +124,7 @@ private static List GetTimeZoneIds() private static string? GetTzEnvironmentVariable() { - string? result = Environment.GetEnvironmentVariable(TimeZoneEnvironmentVariable); + string? result = Environment.GetEnvironmentVariable(_timeZoneEnvironmentVariable); if (!string.IsNullOrEmpty(result)) { if (result[0] == ':') @@ -447,11 +447,11 @@ private static TimeZoneInfo GetLocalTimeZoneFromTzFile() private static string GetTimeZoneDirectory() { - string? tzDirectory = Environment.GetEnvironmentVariable(TimeZoneDirectoryEnvironmentVariable); + string? tzDirectory = Environment.GetEnvironmentVariable(_timeZoneDirectoryEnvironmentVariable); if (tzDirectory == null) { - tzDirectory = DefaultTimeZoneDirectory; + tzDirectory = _defaultTimeZoneDirectory; } else if (!tzDirectory.EndsWith(Path.DirectorySeparatorChar)) { From 0790650384ce1a3cec3a29db3fa8025c77db40b6 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 7 Jul 2021 16:06:02 -0400 Subject: [PATCH 47/81] More clean up --- .../src/System/TimeZoneInfo.Android.cs | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index cf5b413c866e38..0e74834a583c35 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -192,13 +192,12 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, value = null; e = null; - // mono/mono FindSystemTimeZoneById suggests Local scenario value = id == LocalId ? GetLocalTimeZoneCore() : GetTimeZone(id, id); if (value == null) { e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, GetTimeZoneDirectory() + _timeZoneFileName)); - return TimeZoneInfoResult.TimeZoneNotFoundException; // Mono/mono throws TimeZoneNotFoundException, runtime throws InvalidTimeZoneException + return TimeZoneInfoResult.TimeZoneNotFoundException; } return TimeZoneInfoResult.Success; @@ -230,7 +229,6 @@ private unsafe struct AndroidTzDataHeader public fixed byte signature[12]; public int indexOffset; public int dataOffset; - // Do we need zoneTabOFfset? Whats the format of tzdata now vs during mono/mono implementation. } [StructLayout(LayoutKind.Sequential, Pack=1)] @@ -299,8 +297,7 @@ private unsafe T ReadAt(string tzFilePath, long position, byte[] buffer) if ((numBytesRead = fs.Read(buffer, 0, size)) < size) { //TODO: Put strings in resource file - throw new InvalidOperationException( - string.Format("Error reading '{0}': read {1} bytes, expected {2}", tzFilePath, numBytesRead, size)); + throw new InvalidOperationException(string.Format("Error reading '{0}': read {1} bytes, expected {2}", tzFilePath, numBytesRead, size)); } fixed (byte* b = buffer) @@ -315,11 +312,7 @@ private static int NetworkToHostOrder(int value) if (!BitConverter.IsLittleEndian) return value; - return - (((value >> 24) & 0xFF) | - ((value >> 08) & 0xFF00) | - ((value << 08) & 0xFF0000) | - ((value << 24))); + return (((value >> 24) & 0xFF) | ((value >> 08) & 0xFF00) | ((value << 08) & 0xFF0000) | ((value << 24))); } private static unsafe int GetStringLength(sbyte* s, int maxLength) @@ -328,12 +321,13 @@ private static unsafe int GetStringLength(sbyte* s, int maxLength) for (len = 0; len < maxLength; len++, s++) { if (*s == 0) + { break; + } } return len; } - // What does the TZdata index look like? private unsafe void ReadIndex(string tzFilePath, int indexOffset, int dataOffset, byte[] buffer) { int indexSize = dataOffset - indexOffset; @@ -387,9 +381,7 @@ public byte[] GetTimeZoneData(string id) if ((numBytesRead = fs.Read(buffer, 0, buffer.Length)) < buffer.Length) { //TODO: Put strings in resource file - throw new InvalidOperationException( - string.Format("Unable to fully read from file '{0}' at offset {1} length {2}; read {3} bytes expected {4}.", - tzFilePath, offset, length, numBytesRead, buffer.Length)); + throw new InvalidOperationException(string.Format("Unable to fully read from file '{0}' at offset {1} length {2}; read {3} bytes expected {4}.", tzFilePath, offset, length, numBytesRead, buffer.Length)); } } From 5f4fae71a87f60cd901e8faab0039628ea209e9e Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Thu, 8 Jul 2021 12:06:05 -0400 Subject: [PATCH 48/81] Rename property --- .../src/System/TimeZoneInfo.Android.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 0e74834a583c35..1acfaa7aa0a363 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -16,7 +16,7 @@ public sealed partial class TimeZoneInfo private static AndroidTzData? s_tzData; private static readonly object s_tzDataLock = new object(); - private static AndroidTzData s_atzData + private static AndroidTzData s_androidTzDataInstance { get { @@ -118,7 +118,7 @@ private static int ParseGMTNumericZone(string name) try { - byte[] buffer = s_atzData.GetTimeZoneData(name); + byte[] buffer = s_androidTzDataInstance.GetTimeZoneData(name); return GetTimeZoneFromTzData(buffer, id); } catch @@ -205,7 +205,7 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, private static string[] GetTimeZoneIds() { - return s_atzData.GetTimeZoneIds(); + return s_androidTzDataInstance.GetTimeZoneIds(); } /* From d1070d121a46b51a6486b9ade1c53cea51e9f6cd Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Thu, 8 Jul 2021 12:08:13 -0400 Subject: [PATCH 49/81] Update TimeZone data header and entry structs Maintain parallel structure with https://github.com/aosp-mirror/platform_bionic/blob/master/libc/tzcode/bionic.cpp --- .../src/System/TimeZoneInfo.Android.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 1acfaa7aa0a363..c2948351d30497 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -226,9 +226,10 @@ private sealed class AndroidTzData [StructLayout(LayoutKind.Sequential, Pack=1)] private unsafe struct AndroidTzDataHeader { - public fixed byte signature[12]; + public fixed byte signature[12]; // "tzdata2012f\0" public int indexOffset; public int dataOffset; + public int finalOffset; } [StructLayout(LayoutKind.Sequential, Pack=1)] @@ -237,7 +238,7 @@ private unsafe struct AndroidTzDataEntry public fixed byte id[40]; public int byteOffset; public int length; - public int rawUtcOffset; + public int unused; // Was raw GMT offset; always 0 since tzdata2014f (L). } private string[]? _ids; From e20c6cbb009fa7ac0b2376f0329074bbc09ae329 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Thu, 8 Jul 2021 14:17:51 -0400 Subject: [PATCH 50/81] Refactor header correctness check Add resource strings for exception throwing --- .../src/Resources/Strings.resx | 15 ++++++++ .../src/System/TimeZoneInfo.Android.cs | 36 +++++++------------ 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index 7c80c23f632414..d81405c3a4ef50 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -3787,4 +3787,19 @@ A MemberInfo that matches '{0}' could not be found. + + Bad magic in '{0}': Header starts with '{1}' instead of 'tzdata' + + + private error: buffer too small + + + Unable to fully read from file '{0}' at offset {1} length {2}; read {3} bytes expected {4}. + + + Length in index file < sizeof(tzhead) + + + Error finding the timezone id + diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index c2948351d30497..57dbdd71ab4cc0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -260,21 +260,14 @@ private unsafe void ReadHeader(string tzFilePath) header.dataOffset = NetworkToHostOrder(header.dataOffset); sbyte* s = (sbyte*)header.signature; - string magic = new string(s, 0, 6, Encoding.ASCII); - - if (magic != "tzdata" || header.signature[11] != 0) - { - var b = new StringBuilder(); - b.Append("bad tzdata magic:"); - for (int i = 0; i < 12; ++i) { - b.Append(' ').Append(((byte)s[i]).ToString("x2")); - } - - //TODO: Put strings in resource file - throw new InvalidOperationException("bad tzdata magic: " + b.ToString()); + var b = new StringBuilder(); + for (int i = 0; i < 12; ++i) { + b.Append(' ').Append(((byte)s[i]).ToString("x2")); } - // What exactly are we considering bad tzdata? Seems like if it doesnt start with "tzdata" or if the signature is filled. - // How does filling the AndroidTzDataHeader work? Shouldn't signature be filled up, so its always != 0? + var signature = b.ToString(); + + if (!signature.StartsWith("tzdata") || header.signature[11] != 0) + throw new InvalidOperationException(SR.Format(SR.InvalidOperation_BadTZHeader, tzFilePath, signature)); ReadIndex(tzFilePath, header.indexOffset, header.dataOffset, buffer); } @@ -287,8 +280,7 @@ private unsafe T ReadAt(string tzFilePath, long position, byte[] buffer) int size = Marshal.SizeOf(typeof(T)); if (buffer.Length < size) { - //TODO: Put strings in resource file - throw new InvalidOperationException("private error: buffer too small"); + throw new InvalidOperationException(SR.InvalidOperation_BadBuffer); } using (FileStream fs = File.OpenRead(tzFilePath)) @@ -297,8 +289,7 @@ private unsafe T ReadAt(string tzFilePath, long position, byte[] buffer) int numBytesRead; if ((numBytesRead = fs.Read(buffer, 0, size)) < size) { - //TODO: Put strings in resource file - throw new InvalidOperationException(string.Format("Error reading '{0}': read {1} bytes, expected {2}", tzFilePath, numBytesRead, size)); + throw new InvalidOperationException(SR.Format(SR.InvalidOperation_ReadTZError, tzFilePath, position, size, numBytesRead, size)); } fixed (byte* b = buffer) @@ -350,8 +341,7 @@ private unsafe void ReadIndex(string tzFilePath, int indexOffset, int dataOffset if (_lengths![i] < Marshal.SizeOf(typeof(AndroidTzDataHeader))) { - //TODO: Put strings in resource file - throw new InvalidOperationException("Length in index file < sizeof(tzhead)"); + throw new InvalidOperationException(SR.InvalidOperation_BadIndexLength); } } } @@ -366,8 +356,7 @@ public byte[] GetTimeZoneData(string id) int i = Array.BinarySearch(_ids!, id, StringComparer.Ordinal); if (i < 0) { - //TODO: Put strings in resource file - throw new InvalidOperationException("Error finding the timezone id"); + throw new InvalidOperationException(SR.InvalidOperation_TimeZoneIDNotFound); } int offset = _byteOffsets![i]; @@ -381,8 +370,7 @@ public byte[] GetTimeZoneData(string id) int numBytesRead; if ((numBytesRead = fs.Read(buffer, 0, buffer.Length)) < buffer.Length) { - //TODO: Put strings in resource file - throw new InvalidOperationException(string.Format("Unable to fully read from file '{0}' at offset {1} length {2}; read {3} bytes expected {4}.", tzFilePath, offset, length, numBytesRead, buffer.Length)); + throw new InvalidOperationException(string.Format(SR.InvalidOperation_ReadTZError, tzFilePath, offset, length, numBytesRead, buffer.Length)); } } From 4b316844dff3d910017c5e81866baa48a19f40b6 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Thu, 8 Jul 2021 14:32:56 -0400 Subject: [PATCH 51/81] Fix resource string message --- src/libraries/System.Private.CoreLib/src/Resources/Strings.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index d81405c3a4ef50..0ee50030c5b95e 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -3797,7 +3797,7 @@ Unable to fully read from file '{0}' at offset {1} length {2}; read {3} bytes expected {4}. - Length in index file < sizeof(tzhead) + Length in index file less than AndroidTzDataHeader Error finding the timezone id From 2616ca05c1c686fe6ded3b425c3d2bd7ad8cf593 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Thu, 8 Jul 2021 15:41:19 -0400 Subject: [PATCH 52/81] Revert to original header check --- .../src/System/TimeZoneInfo.Android.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 57dbdd71ab4cc0..406a1080a66381 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -260,14 +260,17 @@ private unsafe void ReadHeader(string tzFilePath) header.dataOffset = NetworkToHostOrder(header.dataOffset); sbyte* s = (sbyte*)header.signature; - var b = new StringBuilder(); - for (int i = 0; i < 12; ++i) { - b.Append(' ').Append(((byte)s[i]).ToString("x2")); - } - var signature = b.ToString(); + string magic = new string(s, 0, 6, Encoding.ASCII); - if (!signature.StartsWith("tzdata") || header.signature[11] != 0) - throw new InvalidOperationException(SR.Format(SR.InvalidOperation_BadTZHeader, tzFilePath, signature)); + if (magic != "tzdata" || header.signature[11] != 0) + { + var b = new StringBuilder(); + for (int i = 0; i < 12; ++i) { + b.Append(' ').Append(((byte)s[i]).ToString("x2")); + } + + throw new InvalidOperationException(SR.Format(SR.InvalidOperation_BadTZHeader, tzFilePath, b.ToString())); + } ReadIndex(tzFilePath, header.indexOffset, header.dataOffset, buffer); } From ecf6f70fa68394393828d587f85d608bc775bc06 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Thu, 8 Jul 2021 16:50:19 -0400 Subject: [PATCH 53/81] Cleanup comments --- .../src/System/TimeZoneInfo.Android.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 406a1080a66381..1115a5b2cff20b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -128,6 +128,9 @@ private static int ParseGMTNumericZone(string name) } } + // Core logic to retrieve the local system time zone. + // Obtains Android's system local time zone id to get the corresponding time zone + // Defaults to Utc if local time zone cannot be found private static TimeZoneInfo GetLocalTimeZoneCore() { string? id = Interop.Sys.GetDefaultTimeZone(); @@ -141,7 +144,6 @@ private static TimeZoneInfo GetLocalTimeZoneCore() } } - // If we can't find a default time zone, return UTC return Utc; } @@ -167,6 +169,9 @@ private static string GetApexRuntimeRoot() return "/apex/com.android.runtime"; } + // On Android, time zone data is found in tzdata + // Based on https://github.com/mono/mono/blob/main/mcs/class/corlib/System/TimeZoneInfo.Android.cs + // Also follows the locations found at the bottom of https://github.com/aosp-mirror/platform_bionic/blob/master/libc/tzcode/bionic.cpp private static string GetTimeZoneDirectory() { // Android 10+, TimeData module where the updates land @@ -252,13 +257,14 @@ public AndroidTzData() private unsafe void ReadHeader(string tzFilePath) { - int size = Math.Max(Marshal.SizeOf(typeof(AndroidTzDataHeader)), Marshal.SizeOf(typeof(AndroidTzDataEntry))); + int size = Math.Max(Marshal.SizeOf(typeof(AndroidTzDataHeader)), Marshal.SizeOf(typeof(AndroidTzDataEntry))); var buffer = new byte[size]; AndroidTzDataHeader header = ReadAt(tzFilePath, 0, buffer); header.indexOffset = NetworkToHostOrder(header.indexOffset); header.dataOffset = NetworkToHostOrder(header.dataOffset); + // tzdata files are expected to start with the form of "tzdata2012f\0" depending on the year of the tzdata used which is 2012 in this example sbyte* s = (sbyte*)header.signature; string magic = new string(s, 0, 6, Encoding.ASCII); From 3b424dc8cc39e9584bee71968b2f82716ce0cd52 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 9 Jul 2021 10:53:23 -0400 Subject: [PATCH 54/81] Expect Ansi strings --- .../Unix/System.Native/Interop.GetDefaultTimeZone.Android.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetDefaultTimeZone.Android.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetDefaultTimeZone.Android.cs index 2791a653ae8e5b..b9a9cd50409897 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetDefaultTimeZone.Android.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetDefaultTimeZone.Android.cs @@ -8,7 +8,7 @@ internal static partial class Interop { internal static partial class Sys { - [DllImport(Interop.Libraries.SystemNative, EntryPoint = "SystemNative_GetDefaultTimeZone")] + [DllImport(Interop.Libraries.SystemNative, EntryPoint = "SystemNative_GetDefaultTimeZone", CharSet = CharSet.Ansi, SetLastError = true)] internal static extern string? GetDefaultTimeZone(); } } From ed3355eff76acdcc9f8f0b6bc894d311cafb424f Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 9 Jul 2021 12:18:46 -0400 Subject: [PATCH 55/81] Fix casing --- .../src/System/TimeZoneInfo.Android.cs | 23 +++++++++---------- .../src/System/TimeZoneInfo.AnyUnix.cs | 2 +- .../src/System/TimeZoneInfo.Unix.cs | 12 +++++----- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 1115a5b2cff20b..6efd2f3cebedf4 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -11,12 +11,12 @@ namespace System { public sealed partial class TimeZoneInfo { - private const string _timeZoneFileName = "tzdata"; + private const string TimeZoneFileName = "tzdata"; private static AndroidTzData? s_tzData; private static readonly object s_tzDataLock = new object(); - private static AndroidTzData s_androidTzDataInstance + private static AndroidTzData AndroidTzDataInstance { get { @@ -118,12 +118,11 @@ private static int ParseGMTNumericZone(string name) try { - byte[] buffer = s_androidTzDataInstance.GetTimeZoneData(name); + byte[] buffer = AndroidTzDataInstance.GetTimeZoneData(name); return GetTimeZoneFromTzData(buffer, id); } catch { - // How should we handle return null; } } @@ -175,21 +174,21 @@ private static string GetApexRuntimeRoot() private static string GetTimeZoneDirectory() { // Android 10+, TimeData module where the updates land - if (File.Exists(Path.Combine(GetApexTimeDataRoot() + "/etc/tz/", _timeZoneFileName))) + if (File.Exists(Path.Combine(GetApexTimeDataRoot() + "/etc/tz/", TimeZoneFileName))) { return GetApexTimeDataRoot() + "/etc/tz/"; } // Android 10+, Fallback location if the above isn't found or corrupted - if (File.Exists(Path.Combine(GetApexRuntimeRoot() + "/etc/tz/", _timeZoneFileName))) + if (File.Exists(Path.Combine(GetApexRuntimeRoot() + "/etc/tz/", TimeZoneFileName))) { return GetApexRuntimeRoot() + "/etc/tz/"; } - if (File.Exists(Path.Combine(Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/", _timeZoneFileName))) + if (File.Exists(Path.Combine(Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/", TimeZoneFileName))) { return Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/"; } - return Environment.GetEnvironmentVariable("ANDROID_ROOT") + _defaultTimeZoneDirectory; + return Environment.GetEnvironmentVariable("ANDROID_ROOT") + DefaultTimeZoneDirectory; } private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, out TimeZoneInfo? value, out Exception? e) @@ -201,7 +200,7 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, if (value == null) { - e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, GetTimeZoneDirectory() + _timeZoneFileName)); + e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, GetTimeZoneDirectory() + TimeZoneFileName)); return TimeZoneInfoResult.TimeZoneNotFoundException; } @@ -210,7 +209,7 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, private static string[] GetTimeZoneIds() { - return s_androidTzDataInstance.GetTimeZoneIds(); + return AndroidTzDataInstance.GetTimeZoneIds(); } /* @@ -252,7 +251,7 @@ private unsafe struct AndroidTzDataEntry public AndroidTzData() { - ReadHeader(GetTimeZoneDirectory() + _timeZoneFileName); + ReadHeader(GetTimeZoneDirectory() + TimeZoneFileName); } private unsafe void ReadHeader(string tzFilePath) @@ -372,7 +371,7 @@ public byte[] GetTimeZoneData(string id) int length = _lengths![i]; var buffer = new byte[length]; // Do we need to lock to prevent multithreading issues like the mono/mono implementation? - string tzFilePath = GetTimeZoneDirectory() + _timeZoneFileName; + string tzFilePath = GetTimeZoneDirectory() + TimeZoneFileName; using (FileStream fs = File.OpenRead(tzFilePath)) { fs.Position = offset; diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs index 7c2ddea4baf2fa..6824b113137fad 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs @@ -15,7 +15,7 @@ namespace System { public sealed partial class TimeZoneInfo { - private const string _defaultTimeZoneDirectory = "/usr/share/zoneinfo/"; + private const string DefaultTimeZoneDirectory = "/usr/share/zoneinfo/"; // UTC aliases per https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml // Hard-coded because we need to treat all aliases of UTC the same even when globalization data is not available. 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 dc28796eae32d5..6b02d5c5af1613 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -12,9 +12,9 @@ namespace System { public sealed partial class TimeZoneInfo { - private const string _timeZoneFileName = "zone.tab"; - private const string _timeZoneDirectoryEnvironmentVariable = "TZDIR"; - private const string _timeZoneEnvironmentVariable = "TZ"; + private const string TimeZoneFileName = "zone.tab"; + private const string TimeZoneDirectoryEnvironmentVariable = "TZDIR"; + private const string TimeZoneEnvironmentVariable = "TZ"; private static TimeZoneInfo GetLocalTimeZoneCore() { @@ -78,7 +78,7 @@ private static List GetTimeZoneIds() try { - using (StreamReader sr = new StreamReader(Path.Combine(GetTimeZoneDirectory(), _timeZoneFileName), Encoding.UTF8)) + using (StreamReader sr = new StreamReader(Path.Combine(GetTimeZoneDirectory(), TimeZoneFileName), Encoding.UTF8)) { string? zoneTabFileLine; while ((zoneTabFileLine = sr.ReadLine()) != null) @@ -124,7 +124,7 @@ private static List GetTimeZoneIds() private static string? GetTzEnvironmentVariable() { - string? result = Environment.GetEnvironmentVariable(_timeZoneEnvironmentVariable); + string? result = Environment.GetEnvironmentVariable(TimeZoneEnvironmentVariable); if (!string.IsNullOrEmpty(result)) { if (result[0] == ':') @@ -447,7 +447,7 @@ private static TimeZoneInfo GetLocalTimeZoneFromTzFile() private static string GetTimeZoneDirectory() { - string? tzDirectory = Environment.GetEnvironmentVariable(_timeZoneDirectoryEnvironmentVariable); + string? tzDirectory = Environment.GetEnvironmentVariable(TimeZoneDirectoryEnvironmentVariable); if (tzDirectory == null) { From 2bfb9e47dbeac01c064665b1e1d0e9edb23f0973 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 9 Jul 2021 12:39:29 -0400 Subject: [PATCH 56/81] Stackalloc tzdata buffer --- .../src/System/TimeZoneInfo.Android.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 6efd2f3cebedf4..c280eda8d8f6aa 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -257,7 +257,7 @@ public AndroidTzData() private unsafe void ReadHeader(string tzFilePath) { int size = Math.Max(Marshal.SizeOf(typeof(AndroidTzDataHeader)), Marshal.SizeOf(typeof(AndroidTzDataEntry))); - var buffer = new byte[size]; + Span buffer = stackalloc byte[size]; AndroidTzDataHeader header = ReadAt(tzFilePath, 0, buffer); header.indexOffset = NetworkToHostOrder(header.indexOffset); @@ -282,7 +282,7 @@ private unsafe void ReadHeader(string tzFilePath) [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2087:UnrecognizedReflectionPattern", Justification = "Implementation detail of Android TimeZone")] - private unsafe T ReadAt(string tzFilePath, long position, byte[] buffer) + private unsafe T ReadAt(string tzFilePath, long position, Span buffer) where T : struct { int size = Marshal.SizeOf(typeof(T)); @@ -295,7 +295,7 @@ private unsafe T ReadAt(string tzFilePath, long position, byte[] buffer) { fs.Position = position; int numBytesRead; - if ((numBytesRead = fs.Read(buffer, 0, size)) < size) + if ((numBytesRead = fs.Read(buffer)) < size) { throw new InvalidOperationException(SR.Format(SR.InvalidOperation_ReadTZError, tzFilePath, position, size, numBytesRead, size)); } @@ -328,7 +328,7 @@ private static unsafe int GetStringLength(sbyte* s, int maxLength) return len; } - private unsafe void ReadIndex(string tzFilePath, int indexOffset, int dataOffset, byte[] buffer) + private unsafe void ReadIndex(string tzFilePath, int indexOffset, int dataOffset, Span buffer) { int indexSize = dataOffset - indexOffset; int entrySize = Marshal.SizeOf(typeof(AndroidTzDataEntry)); @@ -376,7 +376,7 @@ public byte[] GetTimeZoneData(string id) { fs.Position = offset; int numBytesRead; - if ((numBytesRead = fs.Read(buffer, 0, buffer.Length)) < buffer.Length) + if ((numBytesRead = fs.Read(buffer)) < buffer.Length) { throw new InvalidOperationException(string.Format(SR.InvalidOperation_ReadTZError, tzFilePath, offset, length, numBytesRead, buffer.Length)); } From 5edd7ea1ad789bace18ef680e7fae4fe69591a31 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 9 Jul 2021 12:49:20 -0400 Subject: [PATCH 57/81] Remove redundant exception message --- .../System.Private.CoreLib/src/Resources/Strings.resx | 3 --- .../System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index 0ee50030c5b95e..8569f8409c266f 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -3799,7 +3799,4 @@ Length in index file less than AndroidTzDataHeader - - Error finding the timezone id - diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index c280eda8d8f6aa..055c82273eedbb 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -364,7 +364,7 @@ public byte[] GetTimeZoneData(string id) int i = Array.BinarySearch(_ids!, id, StringComparer.Ordinal); if (i < 0) { - throw new InvalidOperationException(SR.InvalidOperation_TimeZoneIDNotFound); + throw new InvalidOperationException(SR.Format(SR.TimeZoneNotFound_MissingData, id)); } int offset = _byteOffsets![i]; From 966aee559aa06d86c1899290ad9a3a3e3c2a6940 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 9 Jul 2021 12:49:41 -0400 Subject: [PATCH 58/81] Remove nullable from fields --- .../src/System/TimeZoneInfo.Android.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 055c82273eedbb..0f99fb927df22a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -245,15 +245,18 @@ private unsafe struct AndroidTzDataEntry public int unused; // Was raw GMT offset; always 0 since tzdata2014f (L). } - private string[]? _ids; - private int[]? _byteOffsets; - private int[]? _lengths; + private string[] _ids; + private int[] _byteOffsets; + private int[] _lengths; public AndroidTzData() { ReadHeader(GetTimeZoneDirectory() + TimeZoneFileName); } + [MemberNotNull(nameof(_ids))] + [MemberNotNull(nameof(_byteOffsets))] + [MemberNotNull(nameof(_lengths))] private unsafe void ReadHeader(string tzFilePath) { int size = Math.Max(Marshal.SizeOf(typeof(AndroidTzDataHeader)), Marshal.SizeOf(typeof(AndroidTzDataEntry))); @@ -328,6 +331,9 @@ private static unsafe int GetStringLength(sbyte* s, int maxLength) return len; } + [MemberNotNull(nameof(_ids))] + [MemberNotNull(nameof(_byteOffsets))] + [MemberNotNull(nameof(_lengths))] private unsafe void ReadIndex(string tzFilePath, int indexOffset, int dataOffset, Span buffer) { int indexSize = dataOffset - indexOffset; From 290caa232394a61cffb4ae35630a1fc45d05027f Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 9 Jul 2021 13:05:53 -0400 Subject: [PATCH 59/81] Add missing namechange --- .../System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6b02d5c5af1613..6ae232df560894 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -451,7 +451,7 @@ private static string GetTimeZoneDirectory() if (tzDirectory == null) { - tzDirectory = _defaultTimeZoneDirectory; + tzDirectory = DefaultTimeZoneDirectory; } else if (!tzDirectory.EndsWith(Path.DirectorySeparatorChar)) { From fc551d20229b29e3cf8b53e56372bde166a819c4 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 9 Jul 2021 13:32:43 -0400 Subject: [PATCH 60/81] Pass stream through ReadHeader and only open file once --- .../src/System/TimeZoneInfo.Android.cs | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs index 0f99fb927df22a..3d94ac44e7b739 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs @@ -251,17 +251,21 @@ private unsafe struct AndroidTzDataEntry public AndroidTzData() { - ReadHeader(GetTimeZoneDirectory() + TimeZoneFileName); + string tzFilePath = GetTimeZoneDirectory() + TimeZoneFileName; + using (FileStream fs = File.OpenRead(tzFilePath)) + { + ReadHeader(tzFilePath, fs); + } } [MemberNotNull(nameof(_ids))] [MemberNotNull(nameof(_byteOffsets))] [MemberNotNull(nameof(_lengths))] - private unsafe void ReadHeader(string tzFilePath) + private unsafe void ReadHeader(string tzFilePath, Stream fs) { int size = Math.Max(Marshal.SizeOf(typeof(AndroidTzDataHeader)), Marshal.SizeOf(typeof(AndroidTzDataEntry))); Span buffer = stackalloc byte[size]; - AndroidTzDataHeader header = ReadAt(tzFilePath, 0, buffer); + AndroidTzDataHeader header = ReadAt(tzFilePath, fs, 0, buffer); header.indexOffset = NetworkToHostOrder(header.indexOffset); header.dataOffset = NetworkToHostOrder(header.dataOffset); @@ -280,12 +284,12 @@ private unsafe void ReadHeader(string tzFilePath) throw new InvalidOperationException(SR.Format(SR.InvalidOperation_BadTZHeader, tzFilePath, b.ToString())); } - ReadIndex(tzFilePath, header.indexOffset, header.dataOffset, buffer); + ReadIndex(tzFilePath, fs, header.indexOffset, header.dataOffset, buffer); } [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2087:UnrecognizedReflectionPattern", Justification = "Implementation detail of Android TimeZone")] - private unsafe T ReadAt(string tzFilePath, long position, Span buffer) + private unsafe T ReadAt(string tzFilePath, Stream fs, long position, Span buffer) where T : struct { int size = Marshal.SizeOf(typeof(T)); @@ -294,19 +298,16 @@ private unsafe T ReadAt(string tzFilePath, long position, Span buffer) throw new InvalidOperationException(SR.InvalidOperation_BadBuffer); } - using (FileStream fs = File.OpenRead(tzFilePath)) + fs.Position = position; + int numBytesRead; + if ((numBytesRead = fs.Read(buffer)) < size) { - fs.Position = position; - int numBytesRead; - if ((numBytesRead = fs.Read(buffer)) < size) - { - throw new InvalidOperationException(SR.Format(SR.InvalidOperation_ReadTZError, tzFilePath, position, size, numBytesRead, size)); - } + throw new InvalidOperationException(SR.Format(SR.InvalidOperation_ReadTZError, tzFilePath, position, size, numBytesRead, size)); + } - fixed (byte* b = buffer) - { - return (T)Marshal.PtrToStructure((IntPtr)b, typeof(T))!; // Is ! the right way to handle Unboxing a possibly null value. Should there be some check instead? - } + fixed (byte* b = buffer) + { + return (T)Marshal.PtrToStructure((IntPtr)b, typeof(T))!; // Is ! the right way to handle Unboxing a possibly null value. Should there be some check instead? } } @@ -334,7 +335,7 @@ private static unsafe int GetStringLength(sbyte* s, int maxLength) [MemberNotNull(nameof(_ids))] [MemberNotNull(nameof(_byteOffsets))] [MemberNotNull(nameof(_lengths))] - private unsafe void ReadIndex(string tzFilePath, int indexOffset, int dataOffset, Span buffer) + private unsafe void ReadIndex(string tzFilePath, Stream fs, int indexOffset, int dataOffset, Span buffer) { int indexSize = dataOffset - indexOffset; int entrySize = Marshal.SizeOf(typeof(AndroidTzDataEntry)); @@ -346,7 +347,7 @@ private unsafe void ReadIndex(string tzFilePath, int indexOffset, int dataOffset for (int i = 0; i < entryCount; ++i) { - AndroidTzDataEntry entry = ReadAt(tzFilePath, indexOffset + (entrySize*i), buffer); + AndroidTzDataEntry entry = ReadAt(tzFilePath, fs, indexOffset + (entrySize*i), buffer); var p = (sbyte*)entry.id; _byteOffsets![i] = NetworkToHostOrder(entry.byteOffset) + dataOffset; From 6fb31331b1ce6600ebd6c159ac3d311ae2bf275a Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 9 Jul 2021 13:55:37 -0400 Subject: [PATCH 61/81] Rename files --- .../System.Private.CoreLib.Shared.projitems | 6 +- .../src/System/TimeZoneInfo.AnyUnix.cs | 1371 --------------- ...ndroid.cs => TimeZoneInfo.Unix.Android.cs} | 0 .../System/TimeZoneInfo.Unix.NonAndroid.cs | 464 +++++ .../src/System/TimeZoneInfo.Unix.cs | 1517 +++++++++++++---- 5 files changed, 1679 insertions(+), 1679 deletions(-) delete mode 100644 src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs rename src/libraries/System.Private.CoreLib/src/System/{TimeZoneInfo.Android.cs => TimeZoneInfo.Unix.Android.cs} (100%) create mode 100644 src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.NonAndroid.cs diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 8e907907ef4947..9f70fd22b741f9 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -2087,9 +2087,9 @@ - - - + + + diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs deleted file mode 100644 index 6824b113137fad..00000000000000 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AnyUnix.cs +++ /dev/null @@ -1,1371 +0,0 @@ -// 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.Binary; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.IO; -using System.Text; -using System.Threading; -using System.Security; - -namespace System -{ - public sealed partial class TimeZoneInfo - { - private const string DefaultTimeZoneDirectory = "/usr/share/zoneinfo/"; - - // UTC aliases per https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml - // Hard-coded because we need to treat all aliases of UTC the same even when globalization data is not available. - // (This list is not likely to change.) - private static readonly string[] s_UtcAliases = new[] { - "Etc/UTC", - "Etc/UCT", - "Etc/Universal", - "Etc/Zulu", - "UCT", - "UTC", - "Universal", - "Zulu" - }; - - private static readonly TimeZoneInfo s_utcTimeZone = CreateUtcTimeZone(); - - private TimeZoneInfo(byte[] data, string id, bool dstDisabled) - { - _id = id; - - HasIanaId = true; - - // Handle UTC and its aliases - if (StringArrayContains(_id, s_UtcAliases, StringComparison.OrdinalIgnoreCase)) - { - _standardDisplayName = GetUtcStandardDisplayName(); - _daylightDisplayName = _standardDisplayName; - _displayName = GetUtcFullDisplayName(_id, _standardDisplayName); - _baseUtcOffset = TimeSpan.Zero; - _adjustmentRules = Array.Empty(); - return; - } - - TZifHead t; - DateTime[] dts; - byte[] typeOfLocalTime; - TZifType[] transitionType; - string zoneAbbreviations; - bool[] StandardTime; - bool[] GmtTime; - string? futureTransitionsPosixFormat; - string? standardAbbrevName = null; - string? daylightAbbrevName = null; - - // parse the raw TZif bytes; this method can throw ArgumentException when the data is malformed. - TZif_ParseRaw(data, out t, out dts, out typeOfLocalTime, out transitionType, out zoneAbbreviations, out StandardTime, out GmtTime, out futureTransitionsPosixFormat); - - // find the best matching baseUtcOffset and display strings based on the current utcNow value. - // NOTE: read the Standard and Daylight display strings from the tzfile now in case they can't be loaded later - // from the globalization data. - DateTime utcNow = DateTime.UtcNow; - for (int i = 0; i < dts.Length && dts[i] <= utcNow; i++) - { - int type = typeOfLocalTime[i]; - if (!transitionType[type].IsDst) - { - _baseUtcOffset = transitionType[type].UtcOffset; - standardAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[type].AbbreviationIndex); - } - else - { - daylightAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[type].AbbreviationIndex); - } - } - - if (dts.Length == 0) - { - // time zones like Africa/Bujumbura and Etc/GMT* have no transition times but still contain - // TZifType entries that may contain a baseUtcOffset and display strings - for (int i = 0; i < transitionType.Length; i++) - { - if (!transitionType[i].IsDst) - { - _baseUtcOffset = transitionType[i].UtcOffset; - standardAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[i].AbbreviationIndex); - } - else - { - daylightAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[i].AbbreviationIndex); - } - } - } - - // Set fallback values using abbreviations, base offset, and id - // These are expected in environments without time zone globalization data - _standardDisplayName = standardAbbrevName; - _daylightDisplayName = daylightAbbrevName ?? standardAbbrevName; - _displayName = $"(UTC{(_baseUtcOffset >= TimeSpan.Zero ? '+' : '-')}{_baseUtcOffset:hh\\:mm}) {_id}"; - - // Try to populate the display names from the globalization data - TryPopulateTimeZoneDisplayNamesFromGlobalizationData(_id, _baseUtcOffset, ref _standardDisplayName, ref _daylightDisplayName, ref _displayName); - - // TZif supports seconds-level granularity with offsets but TimeZoneInfo only supports minutes since it aligns - // with DateTimeOffset, SQL Server, and the W3C XML Specification - if (_baseUtcOffset.Ticks % TimeSpan.TicksPerMinute != 0) - { - _baseUtcOffset = new TimeSpan(_baseUtcOffset.Hours, _baseUtcOffset.Minutes, 0); - } - - if (!dstDisabled) - { - // only create the adjustment rule if DST is enabled - TZif_GenerateAdjustmentRules(out _adjustmentRules, _baseUtcOffset, dts, typeOfLocalTime, transitionType, StandardTime, GmtTime, futureTransitionsPosixFormat); - } - - ValidateTimeZoneInfo(_id, _baseUtcOffset, _adjustmentRules, out _supportsDaylightSavingTime); - } - - // The TransitionTime fields are not used when AdjustmentRule.NoDaylightTransitions == true. - // However, there are some cases in the past where DST = true, and the daylight savings offset - // now equals what the current BaseUtcOffset is. In that case, the AdjustmentRule.DaylightOffset - // is going to be TimeSpan.Zero. But we still need to return 'true' from AdjustmentRule.HasDaylightSaving. - // To ensure we always return true from HasDaylightSaving, make a "special" dstStart that will make the logic - // in HasDaylightSaving return true. - private static readonly TransitionTime s_daylightRuleMarker = TransitionTime.CreateFixedDateRule(DateTime.MinValue.AddMilliseconds(2), 1, 1); - - // Truncate the date and the time to Milliseconds precision - private static DateTime GetTimeOnlyInMillisecondsPrecision(DateTime input) => new DateTime((input.TimeOfDay.Ticks / TimeSpan.TicksPerMillisecond) * TimeSpan.TicksPerMillisecond); - - /// - /// Returns a cloned array of AdjustmentRule objects - /// - public AdjustmentRule[] GetAdjustmentRules() - { - if (_adjustmentRules == null) - { - return Array.Empty(); - } - - // The rules we use in Unix care mostly about the start and end dates but don't fill the transition start and end info. - // as the rules now is public, we should fill it properly so the caller doesn't have to know how we use it internally - // and can use it as it is used in Windows - - List rulesList = new List(_adjustmentRules.Length); - - for (int i = 0; i < _adjustmentRules.Length; i++) - { - AdjustmentRule rule = _adjustmentRules[i]; - - if (rule.NoDaylightTransitions && - rule.DaylightTransitionStart != s_daylightRuleMarker && - rule.DaylightDelta == TimeSpan.Zero && rule.BaseUtcOffsetDelta == TimeSpan.Zero) - { - // This rule has no time transition, ignore it. - continue; - } - - DateTime start = rule.DateStart.Kind == DateTimeKind.Utc ? - // At the daylight start we didn't start the daylight saving yet then we convert to Local time - // by adding the _baseUtcOffset to the UTC time - new DateTime(rule.DateStart.Ticks + _baseUtcOffset.Ticks, DateTimeKind.Unspecified) : - rule.DateStart; - DateTime end = rule.DateEnd.Kind == DateTimeKind.Utc ? - // At the daylight saving end, the UTC time is mapped to local time which is already shifted by the daylight delta - // we calculate the local time by adding _baseUtcOffset + DaylightDelta to the UTC time - new DateTime(rule.DateEnd.Ticks + _baseUtcOffset.Ticks + rule.DaylightDelta.Ticks, DateTimeKind.Unspecified) : - rule.DateEnd; - - if (start.Year == end.Year || !rule.NoDaylightTransitions) - { - // If the rule is covering only one year then the start and end transitions would occur in that year, we don't need to split the rule. - // Also, rule.NoDaylightTransitions be false in case the rule was created from a POSIX time zone string and having a DST transition. We can represent this in one rule too - TransitionTime startTransition = rule.NoDaylightTransitions ? TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(start), start.Month, start.Day) : rule.DaylightTransitionStart; - TransitionTime endTransition = rule.NoDaylightTransitions ? TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(end), end.Month, end.Day) : rule.DaylightTransitionEnd; - rulesList.Add(AdjustmentRule.CreateAdjustmentRule(start.Date, end.Date, rule.DaylightDelta, startTransition, endTransition, rule.BaseUtcOffsetDelta)); - } - else - { - // For rules spanning more than one year. The time transition inside this rule would apply for the whole time spanning these years - // and not for partial time of every year. - // AdjustmentRule cannot express such rule using the DaylightTransitionStart and DaylightTransitionEnd because - // the DaylightTransitionStart and DaylightTransitionEnd express the transition for every year. - // We split the rule into more rules. The first rule will start from the start year of the original rule and ends at the end of the same year. - // The second splitted rule would cover the middle range of the original rule and ranging from the year start+1 to - // year end-1. The transition time in this rule would start from Jan 1st to end of December. - // The last splitted rule would start from the Jan 1st of the end year of the original rule and ends at the end transition time of the original rule. - - // Add the first rule. - DateTime endForFirstRule = new DateTime(start.Year + 1, 1, 1).AddMilliseconds(-1); // At the end of the first year - TransitionTime startTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(start), start.Month, start.Day); - TransitionTime endTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(endForFirstRule), endForFirstRule.Month, endForFirstRule.Day); - rulesList.Add(AdjustmentRule.CreateAdjustmentRule(start.Date, endForFirstRule.Date, rule.DaylightDelta, startTransition, endTransition, rule.BaseUtcOffsetDelta)); - - // Check if there is range of years between the start and the end years - if (end.Year - start.Year > 1) - { - // Add the middle rule. - DateTime middleYearStart = new DateTime(start.Year + 1, 1, 1); - DateTime middleYearEnd = new DateTime(end.Year, 1, 1).AddMilliseconds(-1); - startTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(middleYearStart), middleYearStart.Month, middleYearStart.Day); - endTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(middleYearEnd), middleYearEnd.Month, middleYearEnd.Day); - rulesList.Add(AdjustmentRule.CreateAdjustmentRule(middleYearStart.Date, middleYearEnd.Date, rule.DaylightDelta, startTransition, endTransition, rule.BaseUtcOffsetDelta)); - } - - // Add the end rule. - DateTime endYearStart = new DateTime(end.Year, 1, 1); // At the beginning of the last year - startTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(endYearStart), endYearStart.Month, endYearStart.Day); - endTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(end), end.Month, end.Day); - rulesList.Add(AdjustmentRule.CreateAdjustmentRule(endYearStart.Date, end.Date, rule.DaylightDelta, startTransition, endTransition, rule.BaseUtcOffsetDelta)); - } - } - - return rulesList.ToArray(); - } - - private static void PopulateAllSystemTimeZones(CachedData cachedData) - { - Debug.Assert(Monitor.IsEntered(cachedData)); - - foreach (string timeZoneId in GetTimeZoneIds()) - { - TryGetTimeZone(timeZoneId, false, out _, out _, cachedData, alwaysFallbackToLocalMachine: true); // populate the cache - } - } - - /// - /// Helper function for retrieving the local system time zone. - /// May throw COMException, TimeZoneNotFoundException, InvalidTimeZoneException. - /// Assumes cachedData lock is taken. - /// - /// A new TimeZoneInfo instance. - private static TimeZoneInfo GetLocalTimeZone(CachedData cachedData) - { - Debug.Assert(Monitor.IsEntered(cachedData)); - - return GetLocalTimeZoneCore(); - } - - private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out TimeZoneInfo? value, out Exception? e) - { - return TryGetTimeZoneFromLocalMachineCore(id, out value, out e); - } - - private static TimeZoneInfo? GetTimeZoneFromTzData(byte[]? rawData, string id) - { - if (rawData != null) - { - try - { - return new TimeZoneInfo(rawData, id, dstDisabled: false); // create a TimeZoneInfo instance from the TZif data w/ DST support - } - catch (ArgumentException) { } - catch (InvalidTimeZoneException) { } - - try - { - return new TimeZoneInfo(rawData, id, dstDisabled: true); // create a TimeZoneInfo instance from the TZif data w/o DST support - } - catch (ArgumentException) { } - catch (InvalidTimeZoneException) { } - } - return null; - } - - - /// - /// Helper function for retrieving a TimeZoneInfo object by time_zone_name. - /// This function wraps the logic necessary to keep the private - /// SystemTimeZones cache in working order - /// - /// This function will either return a valid TimeZoneInfo instance or - /// it will throw 'InvalidTimeZoneException' / 'TimeZoneNotFoundException'. - /// - public static TimeZoneInfo FindSystemTimeZoneById(string id) - { - // Special case for Utc as it will not exist in the dictionary with the rest - // of the system time zones. There is no need to do this check for Local.Id - // since Local is a real time zone that exists in the dictionary cache - if (string.Equals(id, UtcId, StringComparison.OrdinalIgnoreCase)) - { - return Utc; - } - - if (id == null) - { - throw new ArgumentNullException(nameof(id)); - } - else if (id.Length == 0 || id.Contains('\0')) - { - throw new TimeZoneNotFoundException(SR.Format(SR.TimeZoneNotFound_MissingData, id)); - } - - TimeZoneInfo? value; - Exception? e; - - TimeZoneInfoResult result; - - CachedData cachedData = s_cachedData; - - lock (cachedData) - { - result = TryGetTimeZone(id, false, out value, out e, cachedData, alwaysFallbackToLocalMachine: true); - } - - if (result == TimeZoneInfoResult.Success) - { - return value!; - } - else if (result == TimeZoneInfoResult.InvalidTimeZoneException) - { - Debug.Assert(e is InvalidTimeZoneException, - "TryGetTimeZone must create an InvalidTimeZoneException when it returns TimeZoneInfoResult.InvalidTimeZoneException"); - throw e; - } - else if (result == TimeZoneInfoResult.SecurityException) - { - throw new SecurityException(SR.Format(SR.Security_CannotReadFileData, id), e); - } - else - { - throw new TimeZoneNotFoundException(SR.Format(SR.TimeZoneNotFound_MissingData, id), e); - } - } - - // DateTime.Now fast path that avoids allocating an historically accurate TimeZoneInfo.Local and just creates a 1-year (current year) accurate time zone - internal static TimeSpan GetDateTimeNowUtcOffsetFromUtc(DateTime time, out bool isAmbiguousLocalDst) - { - bool isDaylightSavings; - // Use the standard code path for Unix since there isn't a faster way of handling current-year-only time zones - return GetUtcOffsetFromUtc(time, Local, out isDaylightSavings, out isAmbiguousLocalDst); - } - - // TZFILE(5) BSD File Formats Manual TZFILE(5) - // - // NAME - // tzfile -- timezone information - // - // SYNOPSIS - // #include "/usr/src/lib/libc/stdtime/tzfile.h" - // - // DESCRIPTION - // The time zone information files used by tzset(3) begin with the magic - // characters ``TZif'' to identify them as time zone information files, fol- - // lowed by sixteen bytes reserved for future use, followed by four four- - // byte values written in a ``standard'' byte order (the high-order byte of - // the value is written first). These values are, in order: - // - // tzh_ttisgmtcnt The number of UTC/local indicators stored in the file. - // tzh_ttisstdcnt The number of standard/wall indicators stored in the - // file. - // tzh_leapcnt The number of leap seconds for which data is stored in - // the file. - // tzh_timecnt The number of ``transition times'' for which data is - // stored in the file. - // tzh_typecnt The number of ``local time types'' for which data is - // stored in the file (must not be zero). - // tzh_charcnt The number of characters of ``time zone abbreviation - // strings'' stored in the file. - // - // The above header is followed by tzh_timecnt four-byte values of type - // long, sorted in ascending order. These values are written in ``stan- - // dard'' byte order. Each is used as a transition time (as returned by - // time(3)) at which the rules for computing local time change. Next come - // tzh_timecnt one-byte values of type unsigned char; each one tells which - // of the different types of ``local time'' types described in the file is - // associated with the same-indexed transition time. These values serve as - // indices into an array of ttinfo structures that appears next in the file; - // these structures are defined as follows: - // - // struct ttinfo { - // long tt_gmtoff; - // int tt_isdst; - // unsigned int tt_abbrind; - // }; - // - // Each structure is written as a four-byte value for tt_gmtoff of type - // long, in a standard byte order, followed by a one-byte value for tt_isdst - // and a one-byte value for tt_abbrind. In each structure, tt_gmtoff gives - // the number of seconds to be added to UTC, tt_isdst tells whether tm_isdst - // should be set by localtime(3) and tt_abbrind serves as an index into the - // array of time zone abbreviation characters that follow the ttinfo struc- - // ture(s) in the file. - // - // Then there are tzh_leapcnt pairs of four-byte values, written in standard - // byte order; the first value of each pair gives the time (as returned by - // time(3)) at which a leap second occurs; the second gives the total number - // of leap seconds to be applied after the given time. The pairs of values - // are sorted in ascending order by time.b - // - // Then there are tzh_ttisstdcnt standard/wall indicators, each stored as a - // one-byte value; they tell whether the transition times associated with - // local time types were specified as standard time or wall clock time, and - // are used when a time zone file is used in handling POSIX-style time zone - // environment variables. - // - // Finally there are tzh_ttisgmtcnt UTC/local indicators, each stored as a - // one-byte value; they tell whether the transition times associated with - // local time types were specified as UTC or local time, and are used when a - // time zone file is used in handling POSIX-style time zone environment - // variables. - // - // localtime uses the first standard-time ttinfo structure in the file (or - // simply the first ttinfo structure in the absence of a standard-time - // structure) if either tzh_timecnt is zero or the time argument is less - // than the first transition time recorded in the file. - // - // SEE ALSO - // ctime(3), time2posix(3), zic(8) - // - // BSD September 13, 1994 BSD - // - // - // - // TIME(3) BSD Library Functions Manual TIME(3) - // - // NAME - // time -- get time of day - // - // LIBRARY - // Standard C Library (libc, -lc) - // - // SYNOPSIS - // #include - // - // time_t - // time(time_t *tloc); - // - // DESCRIPTION - // The time() function returns the value of time in seconds since 0 hours, 0 - // minutes, 0 seconds, January 1, 1970, Coordinated Universal Time, without - // including leap seconds. If an error occurs, time() returns the value - // (time_t)-1. - // - // The return value is also stored in *tloc, provided that tloc is non-null. - // - // ERRORS - // The time() function may fail for any of the reasons described in - // gettimeofday(2). - // - // SEE ALSO - // gettimeofday(2), ctime(3) - // - // STANDARDS - // The time function conforms to IEEE Std 1003.1-2001 (``POSIX.1''). - // - // BUGS - // Neither ISO/IEC 9899:1999 (``ISO C99'') nor IEEE Std 1003.1-2001 - // (``POSIX.1'') requires time() to set errno on failure; thus, it is impos- - // sible for an application to distinguish the valid time value -1 (repre- - // senting the last UTC second of 1969) from the error return value. - // - // Systems conforming to earlier versions of the C and POSIX standards - // (including older versions of FreeBSD) did not set *tloc in the error - // case. - // - // HISTORY - // A time() function appeared in Version 6 AT&T UNIX. - // - // BSD July 18, 2003 BSD - // - // - private static void TZif_GenerateAdjustmentRules(out AdjustmentRule[]? rules, TimeSpan baseUtcOffset, DateTime[] dts, byte[] typeOfLocalTime, - TZifType[] transitionType, bool[] StandardTime, bool[] GmtTime, string? futureTransitionsPosixFormat) - { - rules = null; - - if (dts.Length > 0) - { - int index = 0; - List rulesList = new List(); - - while (index <= dts.Length) - { - TZif_GenerateAdjustmentRule(ref index, baseUtcOffset, rulesList, dts, typeOfLocalTime, transitionType, StandardTime, GmtTime, futureTransitionsPosixFormat); - } - - rules = rulesList.ToArray(); - if (rules != null && rules.Length == 0) - { - rules = null; - } - } - } - - private static void TZif_GenerateAdjustmentRule(ref int index, TimeSpan timeZoneBaseUtcOffset, List rulesList, DateTime[] dts, - byte[] typeOfLocalTime, TZifType[] transitionTypes, bool[] StandardTime, bool[] GmtTime, string? futureTransitionsPosixFormat) - { - // To generate AdjustmentRules, use the following approach: - // The first AdjustmentRule will go from DateTime.MinValue to the first transition time greater than DateTime.MinValue. - // Each middle AdjustmentRule wil go from dts[index-1] to dts[index]. - // The last AdjustmentRule will go from dts[dts.Length-1] to Datetime.MaxValue. - - // 0. Skip any DateTime.MinValue transition times. In newer versions of the tzfile, there - // is a "big bang" transition time, which is before the year 0001. Since any times before year 0001 - // cannot be represented by DateTime, there is no reason to make AdjustmentRules for these unrepresentable time periods. - // 1. If there are no DateTime.MinValue times, the first AdjustmentRule goes from DateTime.MinValue - // to the first transition and uses the first standard transitionType (or the first transitionType if none of them are standard) - // 2. Create an AdjustmentRule for each transition, i.e. from dts[index - 1] to dts[index]. - // This rule uses the transitionType[index - 1] and the whole AdjustmentRule only describes a single offset - either - // all daylight savings, or all standard time. - // 3. After all the transitions are filled out, the last AdjustmentRule is created from either: - // a. a POSIX-style timezone description ("futureTransitionsPosixFormat"), if there is one or - // b. continue the last transition offset until DateTime.Max - - while (index < dts.Length && dts[index] == DateTime.MinValue) - { - index++; - } - - if (rulesList.Count == 0 && index < dts.Length) - { - TZifType transitionType = TZif_GetEarlyDateTransitionType(transitionTypes); - DateTime endTransitionDate = dts[index]; - - TimeSpan transitionOffset = TZif_CalculateTransitionOffsetFromBase(transitionType.UtcOffset, timeZoneBaseUtcOffset); - TimeSpan daylightDelta = transitionType.IsDst ? transitionOffset : TimeSpan.Zero; - TimeSpan baseUtcDelta = transitionType.IsDst ? TimeSpan.Zero : transitionOffset; - - AdjustmentRule r = AdjustmentRule.CreateAdjustmentRule( - DateTime.MinValue, - endTransitionDate.AddTicks(-1), - daylightDelta, - default, - default, - baseUtcDelta, - noDaylightTransitions: true); - - if (!IsValidAdjustmentRuleOffset(timeZoneBaseUtcOffset, r)) - { - NormalizeAdjustmentRuleOffset(timeZoneBaseUtcOffset, ref r); - } - - rulesList.Add(r); - } - else if (index < dts.Length) - { - DateTime startTransitionDate = dts[index - 1]; - TZifType startTransitionType = transitionTypes[typeOfLocalTime[index - 1]]; - - DateTime endTransitionDate = dts[index]; - - TimeSpan transitionOffset = TZif_CalculateTransitionOffsetFromBase(startTransitionType.UtcOffset, timeZoneBaseUtcOffset); - TimeSpan daylightDelta = startTransitionType.IsDst ? transitionOffset : TimeSpan.Zero; - TimeSpan baseUtcDelta = startTransitionType.IsDst ? TimeSpan.Zero : transitionOffset; - - TransitionTime dstStart; - if (startTransitionType.IsDst) - { - // the TransitionTime fields are not used when AdjustmentRule.NoDaylightTransitions == true. - // However, there are some cases in the past where DST = true, and the daylight savings offset - // now equals what the current BaseUtcOffset is. In that case, the AdjustmentRule.DaylightOffset - // is going to be TimeSpan.Zero. But we still need to return 'true' from AdjustmentRule.HasDaylightSaving. - // To ensure we always return true from HasDaylightSaving, make a "special" dstStart that will make the logic - // in HasDaylightSaving return true. - dstStart = s_daylightRuleMarker; - } - else - { - dstStart = default; - } - - AdjustmentRule r = AdjustmentRule.CreateAdjustmentRule( - startTransitionDate, - endTransitionDate.AddTicks(-1), - daylightDelta, - dstStart, - default, - baseUtcDelta, - noDaylightTransitions: true); - - if (!IsValidAdjustmentRuleOffset(timeZoneBaseUtcOffset, r)) - { - NormalizeAdjustmentRuleOffset(timeZoneBaseUtcOffset, ref r); - } - - rulesList.Add(r); - } - else - { - // create the AdjustmentRule that will be used for all DateTimes after the last transition - - // NOTE: index == dts.Length - DateTime startTransitionDate = dts[index - 1]; - - AdjustmentRule? r = !string.IsNullOrEmpty(futureTransitionsPosixFormat) ? - TZif_CreateAdjustmentRuleForPosixFormat(futureTransitionsPosixFormat, startTransitionDate, timeZoneBaseUtcOffset) : - null; - - if (r == null) - { - // just use the last transition as the rule which will be used until the end of time - - TZifType transitionType = transitionTypes[typeOfLocalTime[index - 1]]; - TimeSpan transitionOffset = TZif_CalculateTransitionOffsetFromBase(transitionType.UtcOffset, timeZoneBaseUtcOffset); - TimeSpan daylightDelta = transitionType.IsDst ? transitionOffset : TimeSpan.Zero; - TimeSpan baseUtcDelta = transitionType.IsDst ? TimeSpan.Zero : transitionOffset; - - r = AdjustmentRule.CreateAdjustmentRule( - startTransitionDate, - DateTime.MaxValue, - daylightDelta, - default, - default, - baseUtcDelta, - noDaylightTransitions: true); - } - - if (!IsValidAdjustmentRuleOffset(timeZoneBaseUtcOffset, r)) - { - NormalizeAdjustmentRuleOffset(timeZoneBaseUtcOffset, ref r); - } - - rulesList.Add(r); - } - - index++; - } - - private static TimeSpan TZif_CalculateTransitionOffsetFromBase(TimeSpan transitionOffset, TimeSpan timeZoneBaseUtcOffset) - { - TimeSpan result = transitionOffset - timeZoneBaseUtcOffset; - - // TZif supports seconds-level granularity with offsets but TimeZoneInfo only supports minutes since it aligns - // with DateTimeOffset, SQL Server, and the W3C XML Specification - if (result.Ticks % TimeSpan.TicksPerMinute != 0) - { - result = new TimeSpan(result.Hours, result.Minutes, 0); - } - - return result; - } - - /// - /// Gets the first standard-time transition type, or simply the first transition type - /// if there are no standard transition types. - /// > - /// - /// from 'man tzfile': - /// localtime(3) uses the first standard-time ttinfo structure in the file - /// (or simply the first ttinfo structure in the absence of a standard-time - /// structure) if either tzh_timecnt is zero or the time argument is less - /// than the first transition time recorded in the file. - /// - private static TZifType TZif_GetEarlyDateTransitionType(TZifType[] transitionTypes) - { - foreach (TZifType transitionType in transitionTypes) - { - if (!transitionType.IsDst) - { - return transitionType; - } - } - - if (transitionTypes.Length > 0) - { - return transitionTypes[0]; - } - - throw new InvalidTimeZoneException(SR.InvalidTimeZone_NoTTInfoStructures); - } - - /// - /// Creates an AdjustmentRule given the POSIX TZ environment variable string. - /// - /// - /// See http://man7.org/linux/man-pages/man3/tzset.3.html for the format and semantics of this POSIX string. - /// - private static AdjustmentRule? TZif_CreateAdjustmentRuleForPosixFormat(string posixFormat, DateTime startTransitionDate, TimeSpan timeZoneBaseUtcOffset) - { - if (TZif_ParsePosixFormat(posixFormat, - out ReadOnlySpan standardName, - out ReadOnlySpan standardOffset, - out ReadOnlySpan daylightSavingsName, - out ReadOnlySpan daylightSavingsOffset, - out ReadOnlySpan start, - out ReadOnlySpan startTime, - out ReadOnlySpan end, - out ReadOnlySpan endTime)) - { - // a valid posixFormat has at least standardName and standardOffset - - TimeSpan? parsedBaseOffset = TZif_ParseOffsetString(standardOffset); - if (parsedBaseOffset.HasValue) - { - TimeSpan baseOffset = parsedBaseOffset.GetValueOrDefault().Negate(); // offsets are backwards in POSIX notation - baseOffset = TZif_CalculateTransitionOffsetFromBase(baseOffset, timeZoneBaseUtcOffset); - - // having a daylightSavingsName means there is a DST rule - if (!daylightSavingsName.IsEmpty) - { - TimeSpan? parsedDaylightSavings = TZif_ParseOffsetString(daylightSavingsOffset); - TimeSpan daylightSavingsTimeSpan; - if (!parsedDaylightSavings.HasValue) - { - // default DST to 1 hour if it isn't specified - daylightSavingsTimeSpan = new TimeSpan(1, 0, 0); - } - else - { - daylightSavingsTimeSpan = parsedDaylightSavings.GetValueOrDefault().Negate(); // offsets are backwards in POSIX notation - daylightSavingsTimeSpan = TZif_CalculateTransitionOffsetFromBase(daylightSavingsTimeSpan, timeZoneBaseUtcOffset); - daylightSavingsTimeSpan = TZif_CalculateTransitionOffsetFromBase(daylightSavingsTimeSpan, baseOffset); - } - - TransitionTime? dstStart = TZif_CreateTransitionTimeFromPosixRule(start, startTime); - TransitionTime? dstEnd = TZif_CreateTransitionTimeFromPosixRule(end, endTime); - - if (dstStart == null || dstEnd == null) - { - return null; - } - - return AdjustmentRule.CreateAdjustmentRule( - startTransitionDate, - DateTime.MaxValue, - daylightSavingsTimeSpan, - dstStart.GetValueOrDefault(), - dstEnd.GetValueOrDefault(), - baseOffset, - noDaylightTransitions: false); - } - else - { - // if there is no daylightSavingsName, the whole AdjustmentRule should be with no transitions - just the baseOffset - return AdjustmentRule.CreateAdjustmentRule( - startTransitionDate, - DateTime.MaxValue, - TimeSpan.Zero, - default, - default, - baseOffset, - noDaylightTransitions: true); - } - } - } - - return null; - } - - private static TimeSpan? TZif_ParseOffsetString(ReadOnlySpan offset) - { - TimeSpan? result = null; - - if (offset.Length > 0) - { - bool negative = offset[0] == '-'; - if (negative || offset[0] == '+') - { - offset = offset.Slice(1); - } - - // Try parsing just hours first. - // Note, TimeSpan.TryParseExact "%h" can't be used here because some time zones using values - // like "26" or "144" and TimeSpan parsing would turn that into 26 or 144 *days* instead of hours. - int hours; - if (int.TryParse(offset, out hours)) - { - result = new TimeSpan(hours, 0, 0); - } - else - { - TimeSpan parsedTimeSpan; - if (TimeSpan.TryParseExact(offset, "g", CultureInfo.InvariantCulture, out parsedTimeSpan)) - { - result = parsedTimeSpan; - } - } - - if (result.HasValue && negative) - { - result = result.GetValueOrDefault().Negate(); - } - } - - return result; - } - - private static DateTime ParseTimeOfDay(ReadOnlySpan time) - { - DateTime timeOfDay; - TimeSpan? timeOffset = TZif_ParseOffsetString(time); - if (timeOffset.HasValue) - { - // This logic isn't correct and can't be corrected until https://github.com/dotnet/runtime/issues/14966 is fixed. - // Some time zones use time values like, "26", "144", or "-2". - // This allows the week to sometimes be week 4 and sometimes week 5 in the month. - // For now, strip off any 'days' in the offset, and just get the time of day correct - timeOffset = new TimeSpan(timeOffset.GetValueOrDefault().Hours, timeOffset.GetValueOrDefault().Minutes, timeOffset.GetValueOrDefault().Seconds); - if (timeOffset.GetValueOrDefault() < TimeSpan.Zero) - { - timeOfDay = new DateTime(1, 1, 2, 0, 0, 0); - } - else - { - timeOfDay = new DateTime(1, 1, 1, 0, 0, 0); - } - - timeOfDay += timeOffset.GetValueOrDefault(); - } - else - { - // default to 2AM. - timeOfDay = new DateTime(1, 1, 1, 2, 0, 0); - } - - return timeOfDay; - } - - private static TransitionTime? TZif_CreateTransitionTimeFromPosixRule(ReadOnlySpan date, ReadOnlySpan time) - { - if (date.IsEmpty) - { - return null; - } - - if (date[0] == 'M') - { - // Mm.w.d - // This specifies day d of week w of month m. The day d must be between 0(Sunday) and 6.The week w must be between 1 and 5; - // week 1 is the first week in which day d occurs, and week 5 specifies the last d day in the month. The month m should be between 1 and 12. - - int month; - int week; - DayOfWeek day; - if (!TZif_ParseMDateRule(date, out month, out week, out day)) - { - throw new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_UnparseablePosixMDateString, date.ToString())); - } - - return TransitionTime.CreateFloatingDateRule(ParseTimeOfDay(time), month, week, day); - } - else - { - if (date[0] != 'J') - { - // 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 - // the year is a leap year or not. - // - // In leap year, n would be counted as: - // - // 0 30 31 59 60 90 335 365 - // |-------Jan--------|-------Feb--------|-------Mar--------|....|-------Dec--------| - // - // while in non leap year we'll have - // - // 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. - // - // 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 (int.TryParse(date, out int julianDay) && julianDay < 59) - { - 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; - } - - // Julian day - TZif_ParseJulianDay(date, out int month, out int day); - return TransitionTime.CreateFixedDateRule(ParseTimeOfDay(time), month, day); - } - } - - /// - /// Parses a string like Jn into month and day values. - /// - private static void TZif_ParseJulianDay(ReadOnlySpan date, out int month, out int day) - { - // Jn - // This specifies the Julian day, with n between 1 and 365.February 29 is never counted, even in leap years. - Debug.Assert(!date.IsEmpty); - Debug.Assert(date[0] == 'J'); - month = day = 0; - - int index = 1; - - if (index >= date.Length || ((uint)(date[index] - '0') > '9'-'0')) - { - throw new InvalidTimeZoneException(SR.InvalidTimeZone_InvalidJulianDay); - } - - int julianDay = 0; - - do - { - julianDay = julianDay * 10 + (int) (date[index] - '0'); - index++; - } while (index < date.Length && ((uint)(date[index] - '0') <= '9'-'0')); - - int[] days = GregorianCalendarHelper.DaysToMonth365; - - if (julianDay == 0 || julianDay > days[days.Length - 1]) - { - throw new InvalidTimeZoneException(SR.InvalidTimeZone_InvalidJulianDay); - } - - int i = 1; - while (i < days.Length && julianDay > days[i]) - { - i++; - } - - Debug.Assert(i > 0 && i < days.Length); - - month = i; - day = julianDay - days[i - 1]; - } - - /// - /// Parses a string like Mm.w.d into month, week and DayOfWeek values. - /// - /// - /// true if the parsing succeeded; otherwise, false. - /// - private static bool TZif_ParseMDateRule(ReadOnlySpan dateRule, out int month, out int week, out DayOfWeek dayOfWeek) - { - if (dateRule[0] == 'M') - { - int monthWeekDotIndex = dateRule.IndexOf('.'); - if (monthWeekDotIndex > 0) - { - ReadOnlySpan weekDaySpan = dateRule.Slice(monthWeekDotIndex + 1); - int weekDayDotIndex = weekDaySpan.IndexOf('.'); - if (weekDayDotIndex > 0) - { - if (int.TryParse(dateRule.Slice(1, monthWeekDotIndex - 1), out month) && - int.TryParse(weekDaySpan.Slice(0, weekDayDotIndex), out week) && - int.TryParse(weekDaySpan.Slice(weekDayDotIndex + 1), out int day)) - { - dayOfWeek = (DayOfWeek)day; - return true; - } - } - } - } - - month = 0; - week = 0; - dayOfWeek = default; - return false; - } - - private static bool TZif_ParsePosixFormat( - ReadOnlySpan posixFormat, - out ReadOnlySpan standardName, - out ReadOnlySpan standardOffset, - out ReadOnlySpan daylightSavingsName, - out ReadOnlySpan daylightSavingsOffset, - out ReadOnlySpan start, - out ReadOnlySpan startTime, - out ReadOnlySpan end, - out ReadOnlySpan endTime) - { - standardName = null; - standardOffset = null; - daylightSavingsName = null; - daylightSavingsOffset = null; - start = null; - startTime = null; - end = null; - endTime = null; - - int index = 0; - standardName = TZif_ParsePosixName(posixFormat, ref index); - standardOffset = TZif_ParsePosixOffset(posixFormat, ref index); - - daylightSavingsName = TZif_ParsePosixName(posixFormat, ref index); - if (!daylightSavingsName.IsEmpty) - { - daylightSavingsOffset = TZif_ParsePosixOffset(posixFormat, ref index); - - if (index < posixFormat.Length && posixFormat[index] == ',') - { - index++; - TZif_ParsePosixDateTime(posixFormat, ref index, out start, out startTime); - - if (index < posixFormat.Length && posixFormat[index] == ',') - { - index++; - TZif_ParsePosixDateTime(posixFormat, ref index, out end, out endTime); - } - } - } - - return !standardName.IsEmpty && !standardOffset.IsEmpty; - } - - private static ReadOnlySpan TZif_ParsePosixName(ReadOnlySpan posixFormat, ref int index) - { - bool isBracketEnclosed = index < posixFormat.Length && posixFormat[index] == '<'; - if (isBracketEnclosed) - { - // move past the opening bracket - index++; - - ReadOnlySpan result = TZif_ParsePosixString(posixFormat, ref index, c => c == '>'); - - // move past the closing bracket - if (index < posixFormat.Length && posixFormat[index] == '>') - { - index++; - } - - return result; - } - else - { - return TZif_ParsePosixString( - posixFormat, - ref index, - c => char.IsDigit(c) || c == '+' || c == '-' || c == ','); - } - } - - private static ReadOnlySpan TZif_ParsePosixOffset(ReadOnlySpan posixFormat, ref int index) => - TZif_ParsePosixString(posixFormat, ref index, c => !char.IsDigit(c) && c != '+' && c != '-' && c != ':'); - - private static void TZif_ParsePosixDateTime(ReadOnlySpan posixFormat, ref int index, out ReadOnlySpan date, out ReadOnlySpan time) - { - time = null; - - date = TZif_ParsePosixDate(posixFormat, ref index); - if (index < posixFormat.Length && posixFormat[index] == '/') - { - index++; - time = TZif_ParsePosixTime(posixFormat, ref index); - } - } - - private static ReadOnlySpan TZif_ParsePosixDate(ReadOnlySpan posixFormat, ref int index) => - TZif_ParsePosixString(posixFormat, ref index, c => c == '/' || c == ','); - - private static ReadOnlySpan TZif_ParsePosixTime(ReadOnlySpan posixFormat, ref int index) => - TZif_ParsePosixString(posixFormat, ref index, c => c == ','); - - private static ReadOnlySpan TZif_ParsePosixString(ReadOnlySpan posixFormat, ref int index, Func breakCondition) - { - int startIndex = index; - for (; index < posixFormat.Length; index++) - { - char current = posixFormat[index]; - if (breakCondition(current)) - { - break; - } - } - - return posixFormat.Slice(startIndex, index - startIndex); - } - - // Returns the Substring from zoneAbbreviations starting at index and ending at '\0' - // zoneAbbreviations is expected to be in the form: "PST\0PDT\0PWT\0\PPT" - private static string TZif_GetZoneAbbreviation(string zoneAbbreviations, int index) - { - int lastIndex = zoneAbbreviations.IndexOf('\0', index); - return lastIndex > 0 ? - zoneAbbreviations.Substring(index, lastIndex - index) : - zoneAbbreviations.Substring(index); - } - - // Converts an array of bytes into an int - always using standard byte order (Big Endian) - // per TZif file standard - private static int TZif_ToInt32(byte[] value, int startIndex) - => BinaryPrimitives.ReadInt32BigEndian(value.AsSpan(startIndex)); - - // Converts an array of bytes into a long - always using standard byte order (Big Endian) - // per TZif file standard - private static long TZif_ToInt64(byte[] value, int startIndex) - => BinaryPrimitives.ReadInt64BigEndian(value.AsSpan(startIndex)); - - private static long TZif_ToUnixTime(byte[] value, int startIndex, TZVersion version) => - version != TZVersion.V1 ? - TZif_ToInt64(value, startIndex) : - TZif_ToInt32(value, startIndex); - - private static DateTime TZif_UnixTimeToDateTime(long unixTime) => - unixTime < DateTimeOffset.UnixMinSeconds ? DateTime.MinValue : - unixTime > DateTimeOffset.UnixMaxSeconds ? DateTime.MaxValue : - DateTimeOffset.FromUnixTimeSeconds(unixTime).UtcDateTime; - - private static void TZif_ParseRaw(byte[] data, out TZifHead t, out DateTime[] dts, out byte[] typeOfLocalTime, out TZifType[] transitionType, - out string zoneAbbreviations, out bool[] StandardTime, out bool[] GmtTime, out string? futureTransitionsPosixFormat) - { - // initialize the out parameters in case the TZifHead ctor throws - dts = null!; - typeOfLocalTime = null!; - transitionType = null!; - zoneAbbreviations = string.Empty; - StandardTime = null!; - GmtTime = null!; - futureTransitionsPosixFormat = null; - - // read in the 44-byte TZ header containing the count/length fields - // - int index = 0; - t = new TZifHead(data, index); - index += TZifHead.Length; - - int timeValuesLength = 4; // the first version uses 4-bytes to specify times - if (t.Version != TZVersion.V1) - { - // move index past the V1 information to read the V2 information - index += (int)((timeValuesLength * t.TimeCount) + t.TimeCount + (6 * t.TypeCount) + ((timeValuesLength + 4) * t.LeapCount) + t.IsStdCount + t.IsGmtCount + t.CharCount); - - // read the V2 header - t = new TZifHead(data, index); - index += TZifHead.Length; - timeValuesLength = 8; // the second version uses 8-bytes - } - - // initialize the containers for the rest of the TZ data - dts = new DateTime[t.TimeCount]; - typeOfLocalTime = new byte[t.TimeCount]; - transitionType = new TZifType[t.TypeCount]; - zoneAbbreviations = string.Empty; - StandardTime = new bool[t.TypeCount]; - GmtTime = new bool[t.TypeCount]; - - // read in the UTC transition points and convert them to Windows - // - for (int i = 0; i < t.TimeCount; i++) - { - long unixTime = TZif_ToUnixTime(data, index, t.Version); - dts[i] = TZif_UnixTimeToDateTime(unixTime); - index += timeValuesLength; - } - - // read in the Type Indices; there is a 1:1 mapping of UTC transition points to Type Indices - // these indices directly map to the array index in the transitionType array below - // - for (int i = 0; i < t.TimeCount; i++) - { - typeOfLocalTime[i] = data[index]; - index++; - } - - // read in the Type table. Each 6-byte entry represents - // {UtcOffset, IsDst, AbbreviationIndex} - // - // each AbbreviationIndex is a character index into the zoneAbbreviations string below - // - for (int i = 0; i < t.TypeCount; i++) - { - transitionType[i] = new TZifType(data, index); - index += 6; - } - - // read in the Abbreviation ASCII string. This string will be in the form: - // "PST\0PDT\0PWT\0\PPT" - // - Encoding enc = Encoding.UTF8; - zoneAbbreviations = enc.GetString(data, index, (int)t.CharCount); - index += (int)t.CharCount; - - // skip ahead of the Leap-Seconds Adjustment data. In a future release, consider adding - // support for Leap-Seconds - // - index += (int)(t.LeapCount * (timeValuesLength + 4)); // skip the leap second transition times - - // read in the Standard Time table. There should be a 1:1 mapping between Type-Index and Standard - // Time table entries. - // - // TRUE = transition time is standard time - // FALSE = transition time is wall clock time - // ABSENT = transition time is wall clock time - // - for (int i = 0; i < t.IsStdCount && i < t.TypeCount && index < data.Length; i++) - { - StandardTime[i] = (data[index++] != 0); - } - - // read in the GMT Time table. There should be a 1:1 mapping between Type-Index and GMT Time table - // entries. - // - // TRUE = transition time is UTC - // FALSE = transition time is local time - // ABSENT = transition time is local time - // - for (int i = 0; i < t.IsGmtCount && i < t.TypeCount && index < data.Length; i++) - { - GmtTime[i] = (data[index++] != 0); - } - - if (t.Version != TZVersion.V1) - { - // read the POSIX-style format, which should be wrapped in newlines with the last newline at the end of the file - if (data[index++] == '\n' && data[data.Length - 1] == '\n') - { - futureTransitionsPosixFormat = enc.GetString(data, index, data.Length - index - 1); - } - } - } - - /// - /// Normalize adjustment rule offset so that it is within valid range - /// This method should not be called at all but is here in case something changes in the future - /// or if really old time zones are present on the OS (no combination is known at the moment) - /// - private static void NormalizeAdjustmentRuleOffset(TimeSpan baseUtcOffset, [NotNull] ref AdjustmentRule adjustmentRule) - { - // Certain time zones such as: - // Time Zone start date end date offset - // ----------------------------------------------------- - // America/Yakutat 0001-01-01 1867-10-18 14:41:00 - // America/Yakutat 1867-10-18 1900-08-20 14:41:00 - // America/Sitka 0001-01-01 1867-10-18 14:58:00 - // America/Sitka 1867-10-18 1900-08-20 14:58:00 - // Asia/Manila 0001-01-01 1844-12-31 -15:56:00 - // Pacific/Guam 0001-01-01 1845-01-01 -14:21:00 - // Pacific/Saipan 0001-01-01 1845-01-01 -14:21:00 - // - // have larger offset than currently supported by framework. - // If for whatever reason we find that time zone exceeding max - // offset of 14h this function will truncate it to the max valid offset. - // Updating max offset may cause problems with interacting with SQL server - // which uses SQL DATETIMEOFFSET field type which was originally designed to be - // bit-for-bit compatible with DateTimeOffset. - - TimeSpan utcOffset = GetUtcOffset(baseUtcOffset, adjustmentRule); - - // utc base offset delta increment - TimeSpan adjustment = TimeSpan.Zero; - - if (utcOffset > MaxOffset) - { - adjustment = MaxOffset - utcOffset; - } - else if (utcOffset < MinOffset) - { - adjustment = MinOffset - utcOffset; - } - - if (adjustment != TimeSpan.Zero) - { - adjustmentRule = AdjustmentRule.CreateAdjustmentRule( - adjustmentRule.DateStart, - adjustmentRule.DateEnd, - adjustmentRule.DaylightDelta, - adjustmentRule.DaylightTransitionStart, - adjustmentRule.DaylightTransitionEnd, - adjustmentRule.BaseUtcOffsetDelta + adjustment, - adjustmentRule.NoDaylightTransitions); - } - } - - private struct TZifType - { - public const int Length = 6; - - public readonly TimeSpan UtcOffset; - public readonly bool IsDst; - public readonly byte AbbreviationIndex; - - public TZifType(byte[] data, int index) - { - if (data == null || data.Length < index + Length) - { - throw new ArgumentException(SR.Argument_TimeZoneInfoInvalidTZif, nameof(data)); - } - UtcOffset = new TimeSpan(0, 0, TZif_ToInt32(data, index + 00)); - IsDst = (data[index + 4] != 0); - AbbreviationIndex = data[index + 5]; - } - } - - private struct TZifHead - { - public const int Length = 44; - - public readonly uint Magic; // TZ_MAGIC "TZif" - public readonly TZVersion Version; // 1 byte for a \0 or 2 or 3 - // public byte[15] Reserved; // reserved for future use - public readonly uint IsGmtCount; // number of transition time flags - public readonly uint IsStdCount; // number of transition time flags - public readonly uint LeapCount; // number of leap seconds - public readonly uint TimeCount; // number of transition times - public readonly uint TypeCount; // number of local time types - public readonly uint CharCount; // number of abbreviated characters - - public TZifHead(byte[] data, int index) - { - if (data == null || data.Length < Length) - { - throw new ArgumentException("bad data", nameof(data)); - } - - Magic = (uint)TZif_ToInt32(data, index + 00); - - if (Magic != 0x545A6966) - { - // 0x545A6966 = {0x54, 0x5A, 0x69, 0x66} = "TZif" - throw new ArgumentException(SR.Argument_TimeZoneInfoBadTZif, nameof(data)); - } - - byte version = data[index + 04]; - Version = - version == '2' ? TZVersion.V2 : - version == '3' ? TZVersion.V3 : - TZVersion.V1; // default/fallback to V1 to guard against future, unsupported version numbers - - // skip the 15 byte reserved field - - // don't use the BitConverter class which parses data - // based on the Endianess of the machine architecture. - // this data is expected to always be in "standard byte order", - // regardless of the machine it is being processed on. - - IsGmtCount = (uint)TZif_ToInt32(data, index + 20); - IsStdCount = (uint)TZif_ToInt32(data, index + 24); - LeapCount = (uint)TZif_ToInt32(data, index + 28); - TimeCount = (uint)TZif_ToInt32(data, index + 32); - TypeCount = (uint)TZif_ToInt32(data, index + 36); - CharCount = (uint)TZif_ToInt32(data, index + 40); - } - } - - private enum TZVersion : byte - { - V1 = 0, - V2, - V3, - // when adding more versions, ensure all the logic using TZVersion is still correct - } - - // Helper function for string array search. (LINQ is not available here.) - private static bool StringArrayContains(string value, string[] source, StringComparison comparison) - { - foreach (string s in source) - { - if (string.Equals(s, value, comparison)) - { - return true; - } - } - - return false; - } - } -} diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs similarity index 100% rename from src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Android.cs rename to src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.NonAndroid.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.NonAndroid.cs new file mode 100644 index 00000000000000..6ae232df560894 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.NonAndroid.cs @@ -0,0 +1,464 @@ +// 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.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Text; +using System.Security; + +namespace System +{ + public sealed partial class TimeZoneInfo + { + private const string TimeZoneFileName = "zone.tab"; + private const string TimeZoneDirectoryEnvironmentVariable = "TZDIR"; + private const string TimeZoneEnvironmentVariable = "TZ"; + + private static TimeZoneInfo GetLocalTimeZoneCore() + { + // Without Registry support, create the TimeZoneInfo from a TZ file + return GetLocalTimeZoneFromTzFile(); + } + + private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, out TimeZoneInfo? value, out Exception? e) + { + value = null; + e = null; + + string timeZoneDirectory = GetTimeZoneDirectory(); + string timeZoneFilePath = Path.Combine(timeZoneDirectory, id); + byte[] rawData; + try + { + rawData = File.ReadAllBytes(timeZoneFilePath); + } + catch (UnauthorizedAccessException ex) + { + e = ex; + return TimeZoneInfoResult.SecurityException; + } + catch (FileNotFoundException ex) + { + e = ex; + return TimeZoneInfoResult.TimeZoneNotFoundException; + } + catch (DirectoryNotFoundException ex) + { + e = ex; + return TimeZoneInfoResult.TimeZoneNotFoundException; + } + catch (IOException ex) + { + e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, timeZoneFilePath), ex); + return TimeZoneInfoResult.InvalidTimeZoneException; + } + + value = GetTimeZoneFromTzData(rawData, id); + + if (value == null) + { + e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, timeZoneFilePath)); + return TimeZoneInfoResult.InvalidTimeZoneException; + } + + return TimeZoneInfoResult.Success; + } + + /// + /// Returns a collection of TimeZone Id values from the time zone file in the timeZoneDirectory. + /// + /// + /// Lines that start with # are comments and are skipped. + /// + private static List GetTimeZoneIds() + { + List timeZoneIds = new List(); + + try + { + using (StreamReader sr = new StreamReader(Path.Combine(GetTimeZoneDirectory(), TimeZoneFileName), Encoding.UTF8)) + { + string? zoneTabFileLine; + while ((zoneTabFileLine = sr.ReadLine()) != null) + { + if (!string.IsNullOrEmpty(zoneTabFileLine) && zoneTabFileLine[0] != '#') + { + // the format of the line is "country-code \t coordinates \t TimeZone Id \t comments" + + int firstTabIndex = zoneTabFileLine.IndexOf('\t'); + if (firstTabIndex != -1) + { + int secondTabIndex = zoneTabFileLine.IndexOf('\t', firstTabIndex + 1); + if (secondTabIndex != -1) + { + string timeZoneId; + int startIndex = secondTabIndex + 1; + int thirdTabIndex = zoneTabFileLine.IndexOf('\t', startIndex); + if (thirdTabIndex != -1) + { + int length = thirdTabIndex - startIndex; + timeZoneId = zoneTabFileLine.Substring(startIndex, length); + } + else + { + timeZoneId = zoneTabFileLine.Substring(startIndex); + } + + if (!string.IsNullOrEmpty(timeZoneId)) + { + timeZoneIds.Add(timeZoneId); + } + } + } + } + } + } + } + catch (IOException) { } + catch (UnauthorizedAccessException) { } + + return timeZoneIds; + } + + private static string? GetTzEnvironmentVariable() + { + string? result = Environment.GetEnvironmentVariable(TimeZoneEnvironmentVariable); + if (!string.IsNullOrEmpty(result)) + { + if (result[0] == ':') + { + // strip off the ':' prefix + result = result.Substring(1); + } + } + + return result; + } + + /// + /// Finds the time zone id by using 'readlink' on the path to see if tzFilePath is + /// a symlink to a file. + /// + private static string? FindTimeZoneIdUsingReadLink(string tzFilePath) + { + string? id = null; + + string? symlinkPath = Interop.Sys.ReadLink(tzFilePath); + if (symlinkPath != null) + { + // symlinkPath can be relative path, use Path to get the full absolute path. + symlinkPath = Path.GetFullPath(symlinkPath, Path.GetDirectoryName(tzFilePath)!); + + string timeZoneDirectory = GetTimeZoneDirectory(); + if (symlinkPath.StartsWith(timeZoneDirectory, StringComparison.Ordinal)) + { + id = symlinkPath.Substring(timeZoneDirectory.Length); + } + } + + return id; + } + + private static string? GetDirectoryEntryFullPath(ref Interop.Sys.DirectoryEntry dirent, string currentPath) + { + ReadOnlySpan direntName = dirent.GetName(stackalloc char[Interop.Sys.DirectoryEntry.NameBufferSize]); + + if ((direntName.Length == 1 && direntName[0] == '.') || + (direntName.Length == 2 && direntName[0] == '.' && direntName[1] == '.')) + return null; + + return Path.Join(currentPath.AsSpan(), direntName); + } + + /// + /// Enumerate files + /// + private static unsafe void EnumerateFilesRecursively(string path, Predicate condition) + { + List? toExplore = null; // List used as a stack + + int bufferSize = Interop.Sys.GetReadDirRBufferSize(); + byte[]? dirBuffer = null; + try + { + dirBuffer = ArrayPool.Shared.Rent(bufferSize); + string currentPath = path; + + fixed (byte* dirBufferPtr = dirBuffer) + { + while (true) + { + IntPtr dirHandle = Interop.Sys.OpenDir(currentPath); + if (dirHandle == IntPtr.Zero) + { + throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo(), currentPath, isDirectory: true); + } + + try + { + // Read each entry from the enumerator + Interop.Sys.DirectoryEntry dirent; + while (Interop.Sys.ReadDirR(dirHandle, dirBufferPtr, bufferSize, out dirent) == 0) + { + string? fullPath = GetDirectoryEntryFullPath(ref dirent, currentPath); + if (fullPath == null) + continue; + + // Get from the dir entry whether the entry is a file or directory. + // We classify everything as a file unless we know it to be a directory. + bool isDir; + if (dirent.InodeType == Interop.Sys.NodeType.DT_DIR) + { + // We know it's a directory. + isDir = true; + } + else if (dirent.InodeType == Interop.Sys.NodeType.DT_LNK || dirent.InodeType == Interop.Sys.NodeType.DT_UNKNOWN) + { + // It's a symlink or unknown: stat to it to see if we can resolve it to a directory. + // If we can't (e.g. symlink to a file, broken symlink, etc.), we'll just treat it as a file. + + Interop.Sys.FileStatus fileinfo; + if (Interop.Sys.Stat(fullPath, out fileinfo) >= 0) + { + isDir = (fileinfo.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR; + } + else + { + isDir = false; + } + } + else + { + // Otherwise, treat it as a file. This includes regular files, FIFOs, etc. + isDir = false; + } + + // Yield the result if the user has asked for it. In the case of directories, + // always explore it by pushing it onto the stack, regardless of whether + // we're returning directories. + if (isDir) + { + toExplore ??= new List(); + toExplore.Add(fullPath); + } + else if (condition(fullPath)) + { + return; + } + } + } + finally + { + if (dirHandle != IntPtr.Zero) + Interop.Sys.CloseDir(dirHandle); + } + + if (toExplore == null || toExplore.Count == 0) + break; + + currentPath = toExplore[toExplore.Count - 1]; + toExplore.RemoveAt(toExplore.Count - 1); + } + } + } + finally + { + if (dirBuffer != null) + ArrayPool.Shared.Return(dirBuffer); + } + } + + private static bool CompareTimeZoneFile(string filePath, byte[] buffer, byte[] rawData) + { + try + { + // bufferSize == 1 used to avoid unnecessary buffer in FileStream + using (FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1)) + { + if (stream.Length == rawData.Length) + { + int index = 0; + int count = rawData.Length; + + while (count > 0) + { + int n = stream.Read(buffer, index, count); + if (n == 0) + ThrowHelper.ThrowEndOfFileException(); + + int end = index + n; + for (; index < end; index++) + { + if (buffer[index] != rawData[index]) + { + return false; + } + } + + count -= n; + } + + return true; + } + } + } + catch (IOException) { } + catch (SecurityException) { } + catch (UnauthorizedAccessException) { } + + return false; + } + + /// + /// Find the time zone id by searching all the tzfiles for the one that matches rawData + /// and return its file name. + /// + private static string FindTimeZoneId(byte[] rawData) + { + // default to "Local" if we can't find the right tzfile + string id = LocalId; + string timeZoneDirectory = GetTimeZoneDirectory(); + string localtimeFilePath = Path.Combine(timeZoneDirectory, "localtime"); + string posixrulesFilePath = Path.Combine(timeZoneDirectory, "posixrules"); + byte[] buffer = new byte[rawData.Length]; + + try + { + EnumerateFilesRecursively(timeZoneDirectory, (string filePath) => + { + // skip the localtime and posixrules file, since they won't give us the correct id + if (!string.Equals(filePath, localtimeFilePath, StringComparison.OrdinalIgnoreCase) + && !string.Equals(filePath, posixrulesFilePath, StringComparison.OrdinalIgnoreCase)) + { + if (CompareTimeZoneFile(filePath, buffer, rawData)) + { + // if all bytes are the same, this must be the right tz file + id = filePath; + + // strip off the root time zone directory + if (id.StartsWith(timeZoneDirectory, StringComparison.Ordinal)) + { + id = id.Substring(timeZoneDirectory.Length); + } + return true; + } + } + return false; + }); + } + catch (IOException) { } + catch (SecurityException) { } + catch (UnauthorizedAccessException) { } + + return id; + } + + private static bool TryLoadTzFile(string tzFilePath, [NotNullWhen(true)] ref byte[]? rawData, [NotNullWhen(true)] ref string? id) + { + if (File.Exists(tzFilePath)) + { + try + { + rawData = File.ReadAllBytes(tzFilePath); + if (string.IsNullOrEmpty(id)) + { + id = FindTimeZoneIdUsingReadLink(tzFilePath); + + if (string.IsNullOrEmpty(id)) + { + id = FindTimeZoneId(rawData); + } + } + return true; + } + catch (IOException) { } + catch (SecurityException) { } + catch (UnauthorizedAccessException) { } + } + return false; + } + + /// + /// Gets the tzfile raw data for the current 'local' time zone using the following rules. + /// 1. Read the TZ environment variable. If it is set, use it. + /// 2. Look for the data in /etc/localtime. + /// 3. Look for the data in GetTimeZoneDirectory()/localtime. + /// 4. Use UTC if all else fails. + /// + private static bool TryGetLocalTzFile([NotNullWhen(true)] out byte[]? rawData, [NotNullWhen(true)] out string? id) + { + rawData = null; + id = null; + string? tzVariable = GetTzEnvironmentVariable(); + + // If the env var is null, use the localtime file + if (tzVariable == null) + { + return + TryLoadTzFile("/etc/localtime", ref rawData, ref id) || + TryLoadTzFile(Path.Combine(GetTimeZoneDirectory(), "localtime"), ref rawData, ref id); + } + + // If it's empty, use UTC (TryGetLocalTzFile() should return false). + if (tzVariable.Length == 0) + { + return false; + } + + // Otherwise, use the path from the env var. If it's not absolute, make it relative + // to the system timezone directory + string tzFilePath; + if (tzVariable[0] != '/') + { + id = tzVariable; + tzFilePath = Path.Combine(GetTimeZoneDirectory(), tzVariable); + } + else + { + tzFilePath = tzVariable; + } + return TryLoadTzFile(tzFilePath, ref rawData, ref id); + } + + /// + /// Helper function used by 'GetLocalTimeZone()' - this function wraps the call + /// for loading time zone data from computers without Registry support. + /// + /// The TryGetLocalTzFile() call returns a Byte[] containing the compiled tzfile. + /// + private static TimeZoneInfo GetLocalTimeZoneFromTzFile() + { + byte[]? rawData; + string? id; + if (TryGetLocalTzFile(out rawData, out id)) + { + TimeZoneInfo? result = GetTimeZoneFromTzData(rawData, id); + if (result != null) + { + return result; + } + } + + // if we can't find a local time zone, return UTC + return Utc; + } + + private static string GetTimeZoneDirectory() + { + string? tzDirectory = Environment.GetEnvironmentVariable(TimeZoneDirectoryEnvironmentVariable); + + if (tzDirectory == null) + { + tzDirectory = DefaultTimeZoneDirectory; + } + else if (!tzDirectory.EndsWith(Path.DirectorySeparatorChar)) + { + tzDirectory += PathInternal.DirectorySeparatorCharAsString; + } + + return tzDirectory; + } + } +} 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 6ae232df560894..6824b113137fad 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -1,464 +1,1371 @@ // 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.Binary; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.IO; using System.Text; +using System.Threading; using System.Security; namespace System { public sealed partial class TimeZoneInfo { - private const string TimeZoneFileName = "zone.tab"; - private const string TimeZoneDirectoryEnvironmentVariable = "TZDIR"; - private const string TimeZoneEnvironmentVariable = "TZ"; + private const string DefaultTimeZoneDirectory = "/usr/share/zoneinfo/"; - private static TimeZoneInfo GetLocalTimeZoneCore() - { - // Without Registry support, create the TimeZoneInfo from a TZ file - return GetLocalTimeZoneFromTzFile(); - } + // UTC aliases per https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml + // Hard-coded because we need to treat all aliases of UTC the same even when globalization data is not available. + // (This list is not likely to change.) + private static readonly string[] s_UtcAliases = new[] { + "Etc/UTC", + "Etc/UCT", + "Etc/Universal", + "Etc/Zulu", + "UCT", + "UTC", + "Universal", + "Zulu" + }; + + private static readonly TimeZoneInfo s_utcTimeZone = CreateUtcTimeZone(); - private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, out TimeZoneInfo? value, out Exception? e) + private TimeZoneInfo(byte[] data, string id, bool dstDisabled) { - value = null; - e = null; + _id = id; - string timeZoneDirectory = GetTimeZoneDirectory(); - string timeZoneFilePath = Path.Combine(timeZoneDirectory, id); - byte[] rawData; - try - { - rawData = File.ReadAllBytes(timeZoneFilePath); - } - catch (UnauthorizedAccessException ex) + HasIanaId = true; + + // Handle UTC and its aliases + if (StringArrayContains(_id, s_UtcAliases, StringComparison.OrdinalIgnoreCase)) { - e = ex; - return TimeZoneInfoResult.SecurityException; + _standardDisplayName = GetUtcStandardDisplayName(); + _daylightDisplayName = _standardDisplayName; + _displayName = GetUtcFullDisplayName(_id, _standardDisplayName); + _baseUtcOffset = TimeSpan.Zero; + _adjustmentRules = Array.Empty(); + return; } - catch (FileNotFoundException ex) + + TZifHead t; + DateTime[] dts; + byte[] typeOfLocalTime; + TZifType[] transitionType; + string zoneAbbreviations; + bool[] StandardTime; + bool[] GmtTime; + string? futureTransitionsPosixFormat; + string? standardAbbrevName = null; + string? daylightAbbrevName = null; + + // parse the raw TZif bytes; this method can throw ArgumentException when the data is malformed. + TZif_ParseRaw(data, out t, out dts, out typeOfLocalTime, out transitionType, out zoneAbbreviations, out StandardTime, out GmtTime, out futureTransitionsPosixFormat); + + // find the best matching baseUtcOffset and display strings based on the current utcNow value. + // NOTE: read the Standard and Daylight display strings from the tzfile now in case they can't be loaded later + // from the globalization data. + DateTime utcNow = DateTime.UtcNow; + for (int i = 0; i < dts.Length && dts[i] <= utcNow; i++) { - e = ex; - return TimeZoneInfoResult.TimeZoneNotFoundException; + int type = typeOfLocalTime[i]; + if (!transitionType[type].IsDst) + { + _baseUtcOffset = transitionType[type].UtcOffset; + standardAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[type].AbbreviationIndex); + } + else + { + daylightAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[type].AbbreviationIndex); + } } - catch (DirectoryNotFoundException ex) + + if (dts.Length == 0) { - e = ex; - return TimeZoneInfoResult.TimeZoneNotFoundException; + // time zones like Africa/Bujumbura and Etc/GMT* have no transition times but still contain + // TZifType entries that may contain a baseUtcOffset and display strings + for (int i = 0; i < transitionType.Length; i++) + { + if (!transitionType[i].IsDst) + { + _baseUtcOffset = transitionType[i].UtcOffset; + standardAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[i].AbbreviationIndex); + } + else + { + daylightAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[i].AbbreviationIndex); + } + } } - catch (IOException ex) + + // Set fallback values using abbreviations, base offset, and id + // These are expected in environments without time zone globalization data + _standardDisplayName = standardAbbrevName; + _daylightDisplayName = daylightAbbrevName ?? standardAbbrevName; + _displayName = $"(UTC{(_baseUtcOffset >= TimeSpan.Zero ? '+' : '-')}{_baseUtcOffset:hh\\:mm}) {_id}"; + + // Try to populate the display names from the globalization data + TryPopulateTimeZoneDisplayNamesFromGlobalizationData(_id, _baseUtcOffset, ref _standardDisplayName, ref _daylightDisplayName, ref _displayName); + + // TZif supports seconds-level granularity with offsets but TimeZoneInfo only supports minutes since it aligns + // with DateTimeOffset, SQL Server, and the W3C XML Specification + if (_baseUtcOffset.Ticks % TimeSpan.TicksPerMinute != 0) { - e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, timeZoneFilePath), ex); - return TimeZoneInfoResult.InvalidTimeZoneException; + _baseUtcOffset = new TimeSpan(_baseUtcOffset.Hours, _baseUtcOffset.Minutes, 0); } - value = GetTimeZoneFromTzData(rawData, id); - - if (value == null) + if (!dstDisabled) { - e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, timeZoneFilePath)); - return TimeZoneInfoResult.InvalidTimeZoneException; + // only create the adjustment rule if DST is enabled + TZif_GenerateAdjustmentRules(out _adjustmentRules, _baseUtcOffset, dts, typeOfLocalTime, transitionType, StandardTime, GmtTime, futureTransitionsPosixFormat); } - return TimeZoneInfoResult.Success; + ValidateTimeZoneInfo(_id, _baseUtcOffset, _adjustmentRules, out _supportsDaylightSavingTime); } + // The TransitionTime fields are not used when AdjustmentRule.NoDaylightTransitions == true. + // However, there are some cases in the past where DST = true, and the daylight savings offset + // now equals what the current BaseUtcOffset is. In that case, the AdjustmentRule.DaylightOffset + // is going to be TimeSpan.Zero. But we still need to return 'true' from AdjustmentRule.HasDaylightSaving. + // To ensure we always return true from HasDaylightSaving, make a "special" dstStart that will make the logic + // in HasDaylightSaving return true. + private static readonly TransitionTime s_daylightRuleMarker = TransitionTime.CreateFixedDateRule(DateTime.MinValue.AddMilliseconds(2), 1, 1); + + // Truncate the date and the time to Milliseconds precision + private static DateTime GetTimeOnlyInMillisecondsPrecision(DateTime input) => new DateTime((input.TimeOfDay.Ticks / TimeSpan.TicksPerMillisecond) * TimeSpan.TicksPerMillisecond); + /// - /// Returns a collection of TimeZone Id values from the time zone file in the timeZoneDirectory. + /// Returns a cloned array of AdjustmentRule objects /// - /// - /// Lines that start with # are comments and are skipped. - /// - private static List GetTimeZoneIds() + public AdjustmentRule[] GetAdjustmentRules() { - List timeZoneIds = new List(); + if (_adjustmentRules == null) + { + return Array.Empty(); + } + + // The rules we use in Unix care mostly about the start and end dates but don't fill the transition start and end info. + // as the rules now is public, we should fill it properly so the caller doesn't have to know how we use it internally + // and can use it as it is used in Windows - try + List rulesList = new List(_adjustmentRules.Length); + + for (int i = 0; i < _adjustmentRules.Length; i++) { - using (StreamReader sr = new StreamReader(Path.Combine(GetTimeZoneDirectory(), TimeZoneFileName), Encoding.UTF8)) + AdjustmentRule rule = _adjustmentRules[i]; + + if (rule.NoDaylightTransitions && + rule.DaylightTransitionStart != s_daylightRuleMarker && + rule.DaylightDelta == TimeSpan.Zero && rule.BaseUtcOffsetDelta == TimeSpan.Zero) { - string? zoneTabFileLine; - while ((zoneTabFileLine = sr.ReadLine()) != null) + // This rule has no time transition, ignore it. + continue; + } + + DateTime start = rule.DateStart.Kind == DateTimeKind.Utc ? + // At the daylight start we didn't start the daylight saving yet then we convert to Local time + // by adding the _baseUtcOffset to the UTC time + new DateTime(rule.DateStart.Ticks + _baseUtcOffset.Ticks, DateTimeKind.Unspecified) : + rule.DateStart; + DateTime end = rule.DateEnd.Kind == DateTimeKind.Utc ? + // At the daylight saving end, the UTC time is mapped to local time which is already shifted by the daylight delta + // we calculate the local time by adding _baseUtcOffset + DaylightDelta to the UTC time + new DateTime(rule.DateEnd.Ticks + _baseUtcOffset.Ticks + rule.DaylightDelta.Ticks, DateTimeKind.Unspecified) : + rule.DateEnd; + + if (start.Year == end.Year || !rule.NoDaylightTransitions) + { + // If the rule is covering only one year then the start and end transitions would occur in that year, we don't need to split the rule. + // Also, rule.NoDaylightTransitions be false in case the rule was created from a POSIX time zone string and having a DST transition. We can represent this in one rule too + TransitionTime startTransition = rule.NoDaylightTransitions ? TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(start), start.Month, start.Day) : rule.DaylightTransitionStart; + TransitionTime endTransition = rule.NoDaylightTransitions ? TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(end), end.Month, end.Day) : rule.DaylightTransitionEnd; + rulesList.Add(AdjustmentRule.CreateAdjustmentRule(start.Date, end.Date, rule.DaylightDelta, startTransition, endTransition, rule.BaseUtcOffsetDelta)); + } + else + { + // For rules spanning more than one year. The time transition inside this rule would apply for the whole time spanning these years + // and not for partial time of every year. + // AdjustmentRule cannot express such rule using the DaylightTransitionStart and DaylightTransitionEnd because + // the DaylightTransitionStart and DaylightTransitionEnd express the transition for every year. + // We split the rule into more rules. The first rule will start from the start year of the original rule and ends at the end of the same year. + // The second splitted rule would cover the middle range of the original rule and ranging from the year start+1 to + // year end-1. The transition time in this rule would start from Jan 1st to end of December. + // The last splitted rule would start from the Jan 1st of the end year of the original rule and ends at the end transition time of the original rule. + + // Add the first rule. + DateTime endForFirstRule = new DateTime(start.Year + 1, 1, 1).AddMilliseconds(-1); // At the end of the first year + TransitionTime startTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(start), start.Month, start.Day); + TransitionTime endTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(endForFirstRule), endForFirstRule.Month, endForFirstRule.Day); + rulesList.Add(AdjustmentRule.CreateAdjustmentRule(start.Date, endForFirstRule.Date, rule.DaylightDelta, startTransition, endTransition, rule.BaseUtcOffsetDelta)); + + // Check if there is range of years between the start and the end years + if (end.Year - start.Year > 1) { - if (!string.IsNullOrEmpty(zoneTabFileLine) && zoneTabFileLine[0] != '#') - { - // the format of the line is "country-code \t coordinates \t TimeZone Id \t comments" - - int firstTabIndex = zoneTabFileLine.IndexOf('\t'); - if (firstTabIndex != -1) - { - int secondTabIndex = zoneTabFileLine.IndexOf('\t', firstTabIndex + 1); - if (secondTabIndex != -1) - { - string timeZoneId; - int startIndex = secondTabIndex + 1; - int thirdTabIndex = zoneTabFileLine.IndexOf('\t', startIndex); - if (thirdTabIndex != -1) - { - int length = thirdTabIndex - startIndex; - timeZoneId = zoneTabFileLine.Substring(startIndex, length); - } - else - { - timeZoneId = zoneTabFileLine.Substring(startIndex); - } - - if (!string.IsNullOrEmpty(timeZoneId)) - { - timeZoneIds.Add(timeZoneId); - } - } - } - } + // Add the middle rule. + DateTime middleYearStart = new DateTime(start.Year + 1, 1, 1); + DateTime middleYearEnd = new DateTime(end.Year, 1, 1).AddMilliseconds(-1); + startTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(middleYearStart), middleYearStart.Month, middleYearStart.Day); + endTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(middleYearEnd), middleYearEnd.Month, middleYearEnd.Day); + rulesList.Add(AdjustmentRule.CreateAdjustmentRule(middleYearStart.Date, middleYearEnd.Date, rule.DaylightDelta, startTransition, endTransition, rule.BaseUtcOffsetDelta)); } + + // Add the end rule. + DateTime endYearStart = new DateTime(end.Year, 1, 1); // At the beginning of the last year + startTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(endYearStart), endYearStart.Month, endYearStart.Day); + endTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(end), end.Month, end.Day); + rulesList.Add(AdjustmentRule.CreateAdjustmentRule(endYearStart.Date, end.Date, rule.DaylightDelta, startTransition, endTransition, rule.BaseUtcOffsetDelta)); } } - catch (IOException) { } - catch (UnauthorizedAccessException) { } - return timeZoneIds; + return rulesList.ToArray(); } - private static string? GetTzEnvironmentVariable() + private static void PopulateAllSystemTimeZones(CachedData cachedData) { - string? result = Environment.GetEnvironmentVariable(TimeZoneEnvironmentVariable); - if (!string.IsNullOrEmpty(result)) + Debug.Assert(Monitor.IsEntered(cachedData)); + + foreach (string timeZoneId in GetTimeZoneIds()) + { + TryGetTimeZone(timeZoneId, false, out _, out _, cachedData, alwaysFallbackToLocalMachine: true); // populate the cache + } + } + + /// + /// Helper function for retrieving the local system time zone. + /// May throw COMException, TimeZoneNotFoundException, InvalidTimeZoneException. + /// Assumes cachedData lock is taken. + /// + /// A new TimeZoneInfo instance. + private static TimeZoneInfo GetLocalTimeZone(CachedData cachedData) + { + Debug.Assert(Monitor.IsEntered(cachedData)); + + return GetLocalTimeZoneCore(); + } + + private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out TimeZoneInfo? value, out Exception? e) + { + return TryGetTimeZoneFromLocalMachineCore(id, out value, out e); + } + + private static TimeZoneInfo? GetTimeZoneFromTzData(byte[]? rawData, string id) + { + if (rawData != null) { - if (result[0] == ':') + try { - // strip off the ':' prefix - result = result.Substring(1); + return new TimeZoneInfo(rawData, id, dstDisabled: false); // create a TimeZoneInfo instance from the TZif data w/ DST support } - } + catch (ArgumentException) { } + catch (InvalidTimeZoneException) { } - return result; + try + { + return new TimeZoneInfo(rawData, id, dstDisabled: true); // create a TimeZoneInfo instance from the TZif data w/o DST support + } + catch (ArgumentException) { } + catch (InvalidTimeZoneException) { } + } + return null; } + /// - /// Finds the time zone id by using 'readlink' on the path to see if tzFilePath is - /// a symlink to a file. + /// Helper function for retrieving a TimeZoneInfo object by time_zone_name. + /// This function wraps the logic necessary to keep the private + /// SystemTimeZones cache in working order + /// + /// This function will either return a valid TimeZoneInfo instance or + /// it will throw 'InvalidTimeZoneException' / 'TimeZoneNotFoundException'. /// - private static string? FindTimeZoneIdUsingReadLink(string tzFilePath) + public static TimeZoneInfo FindSystemTimeZoneById(string id) { - string? id = null; + // Special case for Utc as it will not exist in the dictionary with the rest + // of the system time zones. There is no need to do this check for Local.Id + // since Local is a real time zone that exists in the dictionary cache + if (string.Equals(id, UtcId, StringComparison.OrdinalIgnoreCase)) + { + return Utc; + } + + if (id == null) + { + throw new ArgumentNullException(nameof(id)); + } + else if (id.Length == 0 || id.Contains('\0')) + { + throw new TimeZoneNotFoundException(SR.Format(SR.TimeZoneNotFound_MissingData, id)); + } + + TimeZoneInfo? value; + Exception? e; + + TimeZoneInfoResult result; - string? symlinkPath = Interop.Sys.ReadLink(tzFilePath); - if (symlinkPath != null) + CachedData cachedData = s_cachedData; + + lock (cachedData) { - // symlinkPath can be relative path, use Path to get the full absolute path. - symlinkPath = Path.GetFullPath(symlinkPath, Path.GetDirectoryName(tzFilePath)!); + result = TryGetTimeZone(id, false, out value, out e, cachedData, alwaysFallbackToLocalMachine: true); + } - string timeZoneDirectory = GetTimeZoneDirectory(); - if (symlinkPath.StartsWith(timeZoneDirectory, StringComparison.Ordinal)) + if (result == TimeZoneInfoResult.Success) + { + return value!; + } + else if (result == TimeZoneInfoResult.InvalidTimeZoneException) + { + Debug.Assert(e is InvalidTimeZoneException, + "TryGetTimeZone must create an InvalidTimeZoneException when it returns TimeZoneInfoResult.InvalidTimeZoneException"); + throw e; + } + else if (result == TimeZoneInfoResult.SecurityException) + { + throw new SecurityException(SR.Format(SR.Security_CannotReadFileData, id), e); + } + else + { + throw new TimeZoneNotFoundException(SR.Format(SR.TimeZoneNotFound_MissingData, id), e); + } + } + + // DateTime.Now fast path that avoids allocating an historically accurate TimeZoneInfo.Local and just creates a 1-year (current year) accurate time zone + internal static TimeSpan GetDateTimeNowUtcOffsetFromUtc(DateTime time, out bool isAmbiguousLocalDst) + { + bool isDaylightSavings; + // Use the standard code path for Unix since there isn't a faster way of handling current-year-only time zones + return GetUtcOffsetFromUtc(time, Local, out isDaylightSavings, out isAmbiguousLocalDst); + } + + // TZFILE(5) BSD File Formats Manual TZFILE(5) + // + // NAME + // tzfile -- timezone information + // + // SYNOPSIS + // #include "/usr/src/lib/libc/stdtime/tzfile.h" + // + // DESCRIPTION + // The time zone information files used by tzset(3) begin with the magic + // characters ``TZif'' to identify them as time zone information files, fol- + // lowed by sixteen bytes reserved for future use, followed by four four- + // byte values written in a ``standard'' byte order (the high-order byte of + // the value is written first). These values are, in order: + // + // tzh_ttisgmtcnt The number of UTC/local indicators stored in the file. + // tzh_ttisstdcnt The number of standard/wall indicators stored in the + // file. + // tzh_leapcnt The number of leap seconds for which data is stored in + // the file. + // tzh_timecnt The number of ``transition times'' for which data is + // stored in the file. + // tzh_typecnt The number of ``local time types'' for which data is + // stored in the file (must not be zero). + // tzh_charcnt The number of characters of ``time zone abbreviation + // strings'' stored in the file. + // + // The above header is followed by tzh_timecnt four-byte values of type + // long, sorted in ascending order. These values are written in ``stan- + // dard'' byte order. Each is used as a transition time (as returned by + // time(3)) at which the rules for computing local time change. Next come + // tzh_timecnt one-byte values of type unsigned char; each one tells which + // of the different types of ``local time'' types described in the file is + // associated with the same-indexed transition time. These values serve as + // indices into an array of ttinfo structures that appears next in the file; + // these structures are defined as follows: + // + // struct ttinfo { + // long tt_gmtoff; + // int tt_isdst; + // unsigned int tt_abbrind; + // }; + // + // Each structure is written as a four-byte value for tt_gmtoff of type + // long, in a standard byte order, followed by a one-byte value for tt_isdst + // and a one-byte value for tt_abbrind. In each structure, tt_gmtoff gives + // the number of seconds to be added to UTC, tt_isdst tells whether tm_isdst + // should be set by localtime(3) and tt_abbrind serves as an index into the + // array of time zone abbreviation characters that follow the ttinfo struc- + // ture(s) in the file. + // + // Then there are tzh_leapcnt pairs of four-byte values, written in standard + // byte order; the first value of each pair gives the time (as returned by + // time(3)) at which a leap second occurs; the second gives the total number + // of leap seconds to be applied after the given time. The pairs of values + // are sorted in ascending order by time.b + // + // Then there are tzh_ttisstdcnt standard/wall indicators, each stored as a + // one-byte value; they tell whether the transition times associated with + // local time types were specified as standard time or wall clock time, and + // are used when a time zone file is used in handling POSIX-style time zone + // environment variables. + // + // Finally there are tzh_ttisgmtcnt UTC/local indicators, each stored as a + // one-byte value; they tell whether the transition times associated with + // local time types were specified as UTC or local time, and are used when a + // time zone file is used in handling POSIX-style time zone environment + // variables. + // + // localtime uses the first standard-time ttinfo structure in the file (or + // simply the first ttinfo structure in the absence of a standard-time + // structure) if either tzh_timecnt is zero or the time argument is less + // than the first transition time recorded in the file. + // + // SEE ALSO + // ctime(3), time2posix(3), zic(8) + // + // BSD September 13, 1994 BSD + // + // + // + // TIME(3) BSD Library Functions Manual TIME(3) + // + // NAME + // time -- get time of day + // + // LIBRARY + // Standard C Library (libc, -lc) + // + // SYNOPSIS + // #include + // + // time_t + // time(time_t *tloc); + // + // DESCRIPTION + // The time() function returns the value of time in seconds since 0 hours, 0 + // minutes, 0 seconds, January 1, 1970, Coordinated Universal Time, without + // including leap seconds. If an error occurs, time() returns the value + // (time_t)-1. + // + // The return value is also stored in *tloc, provided that tloc is non-null. + // + // ERRORS + // The time() function may fail for any of the reasons described in + // gettimeofday(2). + // + // SEE ALSO + // gettimeofday(2), ctime(3) + // + // STANDARDS + // The time function conforms to IEEE Std 1003.1-2001 (``POSIX.1''). + // + // BUGS + // Neither ISO/IEC 9899:1999 (``ISO C99'') nor IEEE Std 1003.1-2001 + // (``POSIX.1'') requires time() to set errno on failure; thus, it is impos- + // sible for an application to distinguish the valid time value -1 (repre- + // senting the last UTC second of 1969) from the error return value. + // + // Systems conforming to earlier versions of the C and POSIX standards + // (including older versions of FreeBSD) did not set *tloc in the error + // case. + // + // HISTORY + // A time() function appeared in Version 6 AT&T UNIX. + // + // BSD July 18, 2003 BSD + // + // + private static void TZif_GenerateAdjustmentRules(out AdjustmentRule[]? rules, TimeSpan baseUtcOffset, DateTime[] dts, byte[] typeOfLocalTime, + TZifType[] transitionType, bool[] StandardTime, bool[] GmtTime, string? futureTransitionsPosixFormat) + { + rules = null; + + if (dts.Length > 0) + { + int index = 0; + List rulesList = new List(); + + while (index <= dts.Length) + { + TZif_GenerateAdjustmentRule(ref index, baseUtcOffset, rulesList, dts, typeOfLocalTime, transitionType, StandardTime, GmtTime, futureTransitionsPosixFormat); + } + + rules = rulesList.ToArray(); + if (rules != null && rules.Length == 0) + { + rules = null; + } + } + } + + private static void TZif_GenerateAdjustmentRule(ref int index, TimeSpan timeZoneBaseUtcOffset, List rulesList, DateTime[] dts, + byte[] typeOfLocalTime, TZifType[] transitionTypes, bool[] StandardTime, bool[] GmtTime, string? futureTransitionsPosixFormat) + { + // To generate AdjustmentRules, use the following approach: + // The first AdjustmentRule will go from DateTime.MinValue to the first transition time greater than DateTime.MinValue. + // Each middle AdjustmentRule wil go from dts[index-1] to dts[index]. + // The last AdjustmentRule will go from dts[dts.Length-1] to Datetime.MaxValue. + + // 0. Skip any DateTime.MinValue transition times. In newer versions of the tzfile, there + // is a "big bang" transition time, which is before the year 0001. Since any times before year 0001 + // cannot be represented by DateTime, there is no reason to make AdjustmentRules for these unrepresentable time periods. + // 1. If there are no DateTime.MinValue times, the first AdjustmentRule goes from DateTime.MinValue + // to the first transition and uses the first standard transitionType (or the first transitionType if none of them are standard) + // 2. Create an AdjustmentRule for each transition, i.e. from dts[index - 1] to dts[index]. + // This rule uses the transitionType[index - 1] and the whole AdjustmentRule only describes a single offset - either + // all daylight savings, or all standard time. + // 3. After all the transitions are filled out, the last AdjustmentRule is created from either: + // a. a POSIX-style timezone description ("futureTransitionsPosixFormat"), if there is one or + // b. continue the last transition offset until DateTime.Max + + while (index < dts.Length && dts[index] == DateTime.MinValue) + { + index++; + } + + if (rulesList.Count == 0 && index < dts.Length) + { + TZifType transitionType = TZif_GetEarlyDateTransitionType(transitionTypes); + DateTime endTransitionDate = dts[index]; + + TimeSpan transitionOffset = TZif_CalculateTransitionOffsetFromBase(transitionType.UtcOffset, timeZoneBaseUtcOffset); + TimeSpan daylightDelta = transitionType.IsDst ? transitionOffset : TimeSpan.Zero; + TimeSpan baseUtcDelta = transitionType.IsDst ? TimeSpan.Zero : transitionOffset; + + AdjustmentRule r = AdjustmentRule.CreateAdjustmentRule( + DateTime.MinValue, + endTransitionDate.AddTicks(-1), + daylightDelta, + default, + default, + baseUtcDelta, + noDaylightTransitions: true); + + if (!IsValidAdjustmentRuleOffset(timeZoneBaseUtcOffset, r)) + { + NormalizeAdjustmentRuleOffset(timeZoneBaseUtcOffset, ref r); + } + + rulesList.Add(r); + } + else if (index < dts.Length) + { + DateTime startTransitionDate = dts[index - 1]; + TZifType startTransitionType = transitionTypes[typeOfLocalTime[index - 1]]; + + DateTime endTransitionDate = dts[index]; + + TimeSpan transitionOffset = TZif_CalculateTransitionOffsetFromBase(startTransitionType.UtcOffset, timeZoneBaseUtcOffset); + TimeSpan daylightDelta = startTransitionType.IsDst ? transitionOffset : TimeSpan.Zero; + TimeSpan baseUtcDelta = startTransitionType.IsDst ? TimeSpan.Zero : transitionOffset; + + TransitionTime dstStart; + if (startTransitionType.IsDst) + { + // the TransitionTime fields are not used when AdjustmentRule.NoDaylightTransitions == true. + // However, there are some cases in the past where DST = true, and the daylight savings offset + // now equals what the current BaseUtcOffset is. In that case, the AdjustmentRule.DaylightOffset + // is going to be TimeSpan.Zero. But we still need to return 'true' from AdjustmentRule.HasDaylightSaving. + // To ensure we always return true from HasDaylightSaving, make a "special" dstStart that will make the logic + // in HasDaylightSaving return true. + dstStart = s_daylightRuleMarker; + } + else { - id = symlinkPath.Substring(timeZoneDirectory.Length); + dstStart = default; } + + AdjustmentRule r = AdjustmentRule.CreateAdjustmentRule( + startTransitionDate, + endTransitionDate.AddTicks(-1), + daylightDelta, + dstStart, + default, + baseUtcDelta, + noDaylightTransitions: true); + + if (!IsValidAdjustmentRuleOffset(timeZoneBaseUtcOffset, r)) + { + NormalizeAdjustmentRuleOffset(timeZoneBaseUtcOffset, ref r); + } + + rulesList.Add(r); } + else + { + // create the AdjustmentRule that will be used for all DateTimes after the last transition - return id; + // NOTE: index == dts.Length + DateTime startTransitionDate = dts[index - 1]; + + AdjustmentRule? r = !string.IsNullOrEmpty(futureTransitionsPosixFormat) ? + TZif_CreateAdjustmentRuleForPosixFormat(futureTransitionsPosixFormat, startTransitionDate, timeZoneBaseUtcOffset) : + null; + + if (r == null) + { + // just use the last transition as the rule which will be used until the end of time + + TZifType transitionType = transitionTypes[typeOfLocalTime[index - 1]]; + TimeSpan transitionOffset = TZif_CalculateTransitionOffsetFromBase(transitionType.UtcOffset, timeZoneBaseUtcOffset); + TimeSpan daylightDelta = transitionType.IsDst ? transitionOffset : TimeSpan.Zero; + TimeSpan baseUtcDelta = transitionType.IsDst ? TimeSpan.Zero : transitionOffset; + + r = AdjustmentRule.CreateAdjustmentRule( + startTransitionDate, + DateTime.MaxValue, + daylightDelta, + default, + default, + baseUtcDelta, + noDaylightTransitions: true); + } + + if (!IsValidAdjustmentRuleOffset(timeZoneBaseUtcOffset, r)) + { + NormalizeAdjustmentRuleOffset(timeZoneBaseUtcOffset, ref r); + } + + rulesList.Add(r); + } + + index++; } - private static string? GetDirectoryEntryFullPath(ref Interop.Sys.DirectoryEntry dirent, string currentPath) + private static TimeSpan TZif_CalculateTransitionOffsetFromBase(TimeSpan transitionOffset, TimeSpan timeZoneBaseUtcOffset) { - ReadOnlySpan direntName = dirent.GetName(stackalloc char[Interop.Sys.DirectoryEntry.NameBufferSize]); + TimeSpan result = transitionOffset - timeZoneBaseUtcOffset; - if ((direntName.Length == 1 && direntName[0] == '.') || - (direntName.Length == 2 && direntName[0] == '.' && direntName[1] == '.')) - return null; + // TZif supports seconds-level granularity with offsets but TimeZoneInfo only supports minutes since it aligns + // with DateTimeOffset, SQL Server, and the W3C XML Specification + if (result.Ticks % TimeSpan.TicksPerMinute != 0) + { + result = new TimeSpan(result.Hours, result.Minutes, 0); + } - return Path.Join(currentPath.AsSpan(), direntName); + return result; } /// - /// Enumerate files - /// - private static unsafe void EnumerateFilesRecursively(string path, Predicate condition) + /// Gets the first standard-time transition type, or simply the first transition type + /// if there are no standard transition types. + /// > + /// + /// from 'man tzfile': + /// localtime(3) uses the first standard-time ttinfo structure in the file + /// (or simply the first ttinfo structure in the absence of a standard-time + /// structure) if either tzh_timecnt is zero or the time argument is less + /// than the first transition time recorded in the file. + /// + private static TZifType TZif_GetEarlyDateTransitionType(TZifType[] transitionTypes) { - List? toExplore = null; // List used as a stack + foreach (TZifType transitionType in transitionTypes) + { + if (!transitionType.IsDst) + { + return transitionType; + } + } + + if (transitionTypes.Length > 0) + { + return transitionTypes[0]; + } + + throw new InvalidTimeZoneException(SR.InvalidTimeZone_NoTTInfoStructures); + } - int bufferSize = Interop.Sys.GetReadDirRBufferSize(); - byte[]? dirBuffer = null; - try + /// + /// Creates an AdjustmentRule given the POSIX TZ environment variable string. + /// + /// + /// See http://man7.org/linux/man-pages/man3/tzset.3.html for the format and semantics of this POSIX string. + /// + private static AdjustmentRule? TZif_CreateAdjustmentRuleForPosixFormat(string posixFormat, DateTime startTransitionDate, TimeSpan timeZoneBaseUtcOffset) + { + if (TZif_ParsePosixFormat(posixFormat, + out ReadOnlySpan standardName, + out ReadOnlySpan standardOffset, + out ReadOnlySpan daylightSavingsName, + out ReadOnlySpan daylightSavingsOffset, + out ReadOnlySpan start, + out ReadOnlySpan startTime, + out ReadOnlySpan end, + out ReadOnlySpan endTime)) { - dirBuffer = ArrayPool.Shared.Rent(bufferSize); - string currentPath = path; + // a valid posixFormat has at least standardName and standardOffset - fixed (byte* dirBufferPtr = dirBuffer) + TimeSpan? parsedBaseOffset = TZif_ParseOffsetString(standardOffset); + if (parsedBaseOffset.HasValue) { - while (true) + TimeSpan baseOffset = parsedBaseOffset.GetValueOrDefault().Negate(); // offsets are backwards in POSIX notation + baseOffset = TZif_CalculateTransitionOffsetFromBase(baseOffset, timeZoneBaseUtcOffset); + + // having a daylightSavingsName means there is a DST rule + if (!daylightSavingsName.IsEmpty) { - IntPtr dirHandle = Interop.Sys.OpenDir(currentPath); - if (dirHandle == IntPtr.Zero) + TimeSpan? parsedDaylightSavings = TZif_ParseOffsetString(daylightSavingsOffset); + TimeSpan daylightSavingsTimeSpan; + if (!parsedDaylightSavings.HasValue) { - throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo(), currentPath, isDirectory: true); + // default DST to 1 hour if it isn't specified + daylightSavingsTimeSpan = new TimeSpan(1, 0, 0); } - - try + else { - // Read each entry from the enumerator - Interop.Sys.DirectoryEntry dirent; - while (Interop.Sys.ReadDirR(dirHandle, dirBufferPtr, bufferSize, out dirent) == 0) - { - string? fullPath = GetDirectoryEntryFullPath(ref dirent, currentPath); - if (fullPath == null) - continue; - - // Get from the dir entry whether the entry is a file or directory. - // We classify everything as a file unless we know it to be a directory. - bool isDir; - if (dirent.InodeType == Interop.Sys.NodeType.DT_DIR) - { - // We know it's a directory. - isDir = true; - } - else if (dirent.InodeType == Interop.Sys.NodeType.DT_LNK || dirent.InodeType == Interop.Sys.NodeType.DT_UNKNOWN) - { - // It's a symlink or unknown: stat to it to see if we can resolve it to a directory. - // If we can't (e.g. symlink to a file, broken symlink, etc.), we'll just treat it as a file. - - Interop.Sys.FileStatus fileinfo; - if (Interop.Sys.Stat(fullPath, out fileinfo) >= 0) - { - isDir = (fileinfo.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR; - } - else - { - isDir = false; - } - } - else - { - // Otherwise, treat it as a file. This includes regular files, FIFOs, etc. - isDir = false; - } - - // Yield the result if the user has asked for it. In the case of directories, - // always explore it by pushing it onto the stack, regardless of whether - // we're returning directories. - if (isDir) - { - toExplore ??= new List(); - toExplore.Add(fullPath); - } - else if (condition(fullPath)) - { - return; - } - } + daylightSavingsTimeSpan = parsedDaylightSavings.GetValueOrDefault().Negate(); // offsets are backwards in POSIX notation + daylightSavingsTimeSpan = TZif_CalculateTransitionOffsetFromBase(daylightSavingsTimeSpan, timeZoneBaseUtcOffset); + daylightSavingsTimeSpan = TZif_CalculateTransitionOffsetFromBase(daylightSavingsTimeSpan, baseOffset); } - finally + + TransitionTime? dstStart = TZif_CreateTransitionTimeFromPosixRule(start, startTime); + TransitionTime? dstEnd = TZif_CreateTransitionTimeFromPosixRule(end, endTime); + + if (dstStart == null || dstEnd == null) { - if (dirHandle != IntPtr.Zero) - Interop.Sys.CloseDir(dirHandle); + return null; } - if (toExplore == null || toExplore.Count == 0) - break; + return AdjustmentRule.CreateAdjustmentRule( + startTransitionDate, + DateTime.MaxValue, + daylightSavingsTimeSpan, + dstStart.GetValueOrDefault(), + dstEnd.GetValueOrDefault(), + baseOffset, + noDaylightTransitions: false); + } + else + { + // if there is no daylightSavingsName, the whole AdjustmentRule should be with no transitions - just the baseOffset + return AdjustmentRule.CreateAdjustmentRule( + startTransitionDate, + DateTime.MaxValue, + TimeSpan.Zero, + default, + default, + baseOffset, + noDaylightTransitions: true); + } + } + } + + return null; + } + + private static TimeSpan? TZif_ParseOffsetString(ReadOnlySpan offset) + { + TimeSpan? result = null; - currentPath = toExplore[toExplore.Count - 1]; - toExplore.RemoveAt(toExplore.Count - 1); + if (offset.Length > 0) + { + bool negative = offset[0] == '-'; + if (negative || offset[0] == '+') + { + offset = offset.Slice(1); + } + + // Try parsing just hours first. + // Note, TimeSpan.TryParseExact "%h" can't be used here because some time zones using values + // like "26" or "144" and TimeSpan parsing would turn that into 26 or 144 *days* instead of hours. + int hours; + if (int.TryParse(offset, out hours)) + { + result = new TimeSpan(hours, 0, 0); + } + else + { + TimeSpan parsedTimeSpan; + if (TimeSpan.TryParseExact(offset, "g", CultureInfo.InvariantCulture, out parsedTimeSpan)) + { + result = parsedTimeSpan; } } + + if (result.HasValue && negative) + { + result = result.GetValueOrDefault().Negate(); + } + } + + return result; + } + + private static DateTime ParseTimeOfDay(ReadOnlySpan time) + { + DateTime timeOfDay; + TimeSpan? timeOffset = TZif_ParseOffsetString(time); + if (timeOffset.HasValue) + { + // This logic isn't correct and can't be corrected until https://github.com/dotnet/runtime/issues/14966 is fixed. + // Some time zones use time values like, "26", "144", or "-2". + // This allows the week to sometimes be week 4 and sometimes week 5 in the month. + // For now, strip off any 'days' in the offset, and just get the time of day correct + timeOffset = new TimeSpan(timeOffset.GetValueOrDefault().Hours, timeOffset.GetValueOrDefault().Minutes, timeOffset.GetValueOrDefault().Seconds); + if (timeOffset.GetValueOrDefault() < TimeSpan.Zero) + { + timeOfDay = new DateTime(1, 1, 2, 0, 0, 0); + } + else + { + timeOfDay = new DateTime(1, 1, 1, 0, 0, 0); + } + + timeOfDay += timeOffset.GetValueOrDefault(); } - finally + else { - if (dirBuffer != null) - ArrayPool.Shared.Return(dirBuffer); + // default to 2AM. + timeOfDay = new DateTime(1, 1, 1, 2, 0, 0); } + + return timeOfDay; } - private static bool CompareTimeZoneFile(string filePath, byte[] buffer, byte[] rawData) + private static TransitionTime? TZif_CreateTransitionTimeFromPosixRule(ReadOnlySpan date, ReadOnlySpan time) { - try + if (date.IsEmpty) + { + return null; + } + + if (date[0] == 'M') + { + // Mm.w.d + // This specifies day d of week w of month m. The day d must be between 0(Sunday) and 6.The week w must be between 1 and 5; + // week 1 is the first week in which day d occurs, and week 5 specifies the last d day in the month. The month m should be between 1 and 12. + + int month; + int week; + DayOfWeek day; + if (!TZif_ParseMDateRule(date, out month, out week, out day)) + { + throw new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_UnparseablePosixMDateString, date.ToString())); + } + + return TransitionTime.CreateFloatingDateRule(ParseTimeOfDay(time), month, week, day); + } + else { - // bufferSize == 1 used to avoid unnecessary buffer in FileStream - using (FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1)) + if (date[0] != 'J') { - if (stream.Length == rawData.Length) - { - int index = 0; - int count = rawData.Length; + // 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 + // the year is a leap year or not. + // + // In leap year, n would be counted as: + // + // 0 30 31 59 60 90 335 365 + // |-------Jan--------|-------Feb--------|-------Mar--------|....|-------Dec--------| + // + // while in non leap year we'll have + // + // 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. + // + // 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. - while (count > 0) + if (int.TryParse(date, out int julianDay) && julianDay < 59) + { + int d, m; + if (julianDay <= 30) // January { - int n = stream.Read(buffer, index, count); - if (n == 0) - ThrowHelper.ThrowEndOfFileException(); - - int end = index + n; - for (; index < end; index++) - { - if (buffer[index] != rawData[index]) - { - return false; - } - } - - count -= n; + m = 1; + d = julianDay + 1; + } + else // February + { + m = 2; + d = julianDay - 30; } - return true; + 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; } - } - catch (IOException) { } - catch (SecurityException) { } - catch (UnauthorizedAccessException) { } - return false; + // Julian day + TZif_ParseJulianDay(date, out int month, out int day); + return TransitionTime.CreateFixedDateRule(ParseTimeOfDay(time), month, day); + } } /// - /// Find the time zone id by searching all the tzfiles for the one that matches rawData - /// and return its file name. + /// Parses a string like Jn into month and day values. /// - private static string FindTimeZoneId(byte[] rawData) + private static void TZif_ParseJulianDay(ReadOnlySpan date, out int month, out int day) { - // default to "Local" if we can't find the right tzfile - string id = LocalId; - string timeZoneDirectory = GetTimeZoneDirectory(); - string localtimeFilePath = Path.Combine(timeZoneDirectory, "localtime"); - string posixrulesFilePath = Path.Combine(timeZoneDirectory, "posixrules"); - byte[] buffer = new byte[rawData.Length]; + // Jn + // This specifies the Julian day, with n between 1 and 365.February 29 is never counted, even in leap years. + Debug.Assert(!date.IsEmpty); + Debug.Assert(date[0] == 'J'); + month = day = 0; + + int index = 1; + + if (index >= date.Length || ((uint)(date[index] - '0') > '9'-'0')) + { + throw new InvalidTimeZoneException(SR.InvalidTimeZone_InvalidJulianDay); + } + + int julianDay = 0; + + do + { + julianDay = julianDay * 10 + (int) (date[index] - '0'); + index++; + } while (index < date.Length && ((uint)(date[index] - '0') <= '9'-'0')); + + int[] days = GregorianCalendarHelper.DaysToMonth365; - try + if (julianDay == 0 || julianDay > days[days.Length - 1]) + { + throw new InvalidTimeZoneException(SR.InvalidTimeZone_InvalidJulianDay); + } + + int i = 1; + while (i < days.Length && julianDay > days[i]) + { + i++; + } + + Debug.Assert(i > 0 && i < days.Length); + + month = i; + day = julianDay - days[i - 1]; + } + + /// + /// Parses a string like Mm.w.d into month, week and DayOfWeek values. + /// + /// + /// true if the parsing succeeded; otherwise, false. + /// + private static bool TZif_ParseMDateRule(ReadOnlySpan dateRule, out int month, out int week, out DayOfWeek dayOfWeek) + { + if (dateRule[0] == 'M') { - EnumerateFilesRecursively(timeZoneDirectory, (string filePath) => + int monthWeekDotIndex = dateRule.IndexOf('.'); + if (monthWeekDotIndex > 0) { - // skip the localtime and posixrules file, since they won't give us the correct id - if (!string.Equals(filePath, localtimeFilePath, StringComparison.OrdinalIgnoreCase) - && !string.Equals(filePath, posixrulesFilePath, StringComparison.OrdinalIgnoreCase)) + ReadOnlySpan weekDaySpan = dateRule.Slice(monthWeekDotIndex + 1); + int weekDayDotIndex = weekDaySpan.IndexOf('.'); + if (weekDayDotIndex > 0) { - if (CompareTimeZoneFile(filePath, buffer, rawData)) + if (int.TryParse(dateRule.Slice(1, monthWeekDotIndex - 1), out month) && + int.TryParse(weekDaySpan.Slice(0, weekDayDotIndex), out week) && + int.TryParse(weekDaySpan.Slice(weekDayDotIndex + 1), out int day)) { - // if all bytes are the same, this must be the right tz file - id = filePath; - - // strip off the root time zone directory - if (id.StartsWith(timeZoneDirectory, StringComparison.Ordinal)) - { - id = id.Substring(timeZoneDirectory.Length); - } + dayOfWeek = (DayOfWeek)day; return true; } } - return false; - }); + } } - catch (IOException) { } - catch (SecurityException) { } - catch (UnauthorizedAccessException) { } - return id; + month = 0; + week = 0; + dayOfWeek = default; + return false; } - private static bool TryLoadTzFile(string tzFilePath, [NotNullWhen(true)] ref byte[]? rawData, [NotNullWhen(true)] ref string? id) + private static bool TZif_ParsePosixFormat( + ReadOnlySpan posixFormat, + out ReadOnlySpan standardName, + out ReadOnlySpan standardOffset, + out ReadOnlySpan daylightSavingsName, + out ReadOnlySpan daylightSavingsOffset, + out ReadOnlySpan start, + out ReadOnlySpan startTime, + out ReadOnlySpan end, + out ReadOnlySpan endTime) { - if (File.Exists(tzFilePath)) + standardName = null; + standardOffset = null; + daylightSavingsName = null; + daylightSavingsOffset = null; + start = null; + startTime = null; + end = null; + endTime = null; + + int index = 0; + standardName = TZif_ParsePosixName(posixFormat, ref index); + standardOffset = TZif_ParsePosixOffset(posixFormat, ref index); + + daylightSavingsName = TZif_ParsePosixName(posixFormat, ref index); + if (!daylightSavingsName.IsEmpty) { - try + daylightSavingsOffset = TZif_ParsePosixOffset(posixFormat, ref index); + + if (index < posixFormat.Length && posixFormat[index] == ',') { - rawData = File.ReadAllBytes(tzFilePath); - if (string.IsNullOrEmpty(id)) - { - id = FindTimeZoneIdUsingReadLink(tzFilePath); + index++; + TZif_ParsePosixDateTime(posixFormat, ref index, out start, out startTime); - if (string.IsNullOrEmpty(id)) - { - id = FindTimeZoneId(rawData); - } + if (index < posixFormat.Length && posixFormat[index] == ',') + { + index++; + TZif_ParsePosixDateTime(posixFormat, ref index, out end, out endTime); } - return true; } - catch (IOException) { } - catch (SecurityException) { } - catch (UnauthorizedAccessException) { } } - return false; + + return !standardName.IsEmpty && !standardOffset.IsEmpty; } - /// - /// Gets the tzfile raw data for the current 'local' time zone using the following rules. - /// 1. Read the TZ environment variable. If it is set, use it. - /// 2. Look for the data in /etc/localtime. - /// 3. Look for the data in GetTimeZoneDirectory()/localtime. - /// 4. Use UTC if all else fails. - /// - private static bool TryGetLocalTzFile([NotNullWhen(true)] out byte[]? rawData, [NotNullWhen(true)] out string? id) + private static ReadOnlySpan TZif_ParsePosixName(ReadOnlySpan posixFormat, ref int index) { - rawData = null; - id = null; - string? tzVariable = GetTzEnvironmentVariable(); + bool isBracketEnclosed = index < posixFormat.Length && posixFormat[index] == '<'; + if (isBracketEnclosed) + { + // move past the opening bracket + index++; - // If the env var is null, use the localtime file - if (tzVariable == null) + ReadOnlySpan result = TZif_ParsePosixString(posixFormat, ref index, c => c == '>'); + + // move past the closing bracket + if (index < posixFormat.Length && posixFormat[index] == '>') + { + index++; + } + + return result; + } + else { - return - TryLoadTzFile("/etc/localtime", ref rawData, ref id) || - TryLoadTzFile(Path.Combine(GetTimeZoneDirectory(), "localtime"), ref rawData, ref id); + return TZif_ParsePosixString( + posixFormat, + ref index, + c => char.IsDigit(c) || c == '+' || c == '-' || c == ','); } + } + + private static ReadOnlySpan TZif_ParsePosixOffset(ReadOnlySpan posixFormat, ref int index) => + TZif_ParsePosixString(posixFormat, ref index, c => !char.IsDigit(c) && c != '+' && c != '-' && c != ':'); - // If it's empty, use UTC (TryGetLocalTzFile() should return false). - if (tzVariable.Length == 0) + private static void TZif_ParsePosixDateTime(ReadOnlySpan posixFormat, ref int index, out ReadOnlySpan date, out ReadOnlySpan time) + { + time = null; + + date = TZif_ParsePosixDate(posixFormat, ref index); + if (index < posixFormat.Length && posixFormat[index] == '/') { - return false; + index++; + time = TZif_ParsePosixTime(posixFormat, ref index); } + } - // Otherwise, use the path from the env var. If it's not absolute, make it relative - // to the system timezone directory - string tzFilePath; - if (tzVariable[0] != '/') + private static ReadOnlySpan TZif_ParsePosixDate(ReadOnlySpan posixFormat, ref int index) => + TZif_ParsePosixString(posixFormat, ref index, c => c == '/' || c == ','); + + private static ReadOnlySpan TZif_ParsePosixTime(ReadOnlySpan posixFormat, ref int index) => + TZif_ParsePosixString(posixFormat, ref index, c => c == ','); + + private static ReadOnlySpan TZif_ParsePosixString(ReadOnlySpan posixFormat, ref int index, Func breakCondition) + { + int startIndex = index; + for (; index < posixFormat.Length; index++) { - id = tzVariable; - tzFilePath = Path.Combine(GetTimeZoneDirectory(), tzVariable); + char current = posixFormat[index]; + if (breakCondition(current)) + { + break; + } } - else + + return posixFormat.Slice(startIndex, index - startIndex); + } + + // Returns the Substring from zoneAbbreviations starting at index and ending at '\0' + // zoneAbbreviations is expected to be in the form: "PST\0PDT\0PWT\0\PPT" + private static string TZif_GetZoneAbbreviation(string zoneAbbreviations, int index) + { + int lastIndex = zoneAbbreviations.IndexOf('\0', index); + return lastIndex > 0 ? + zoneAbbreviations.Substring(index, lastIndex - index) : + zoneAbbreviations.Substring(index); + } + + // Converts an array of bytes into an int - always using standard byte order (Big Endian) + // per TZif file standard + private static int TZif_ToInt32(byte[] value, int startIndex) + => BinaryPrimitives.ReadInt32BigEndian(value.AsSpan(startIndex)); + + // Converts an array of bytes into a long - always using standard byte order (Big Endian) + // per TZif file standard + private static long TZif_ToInt64(byte[] value, int startIndex) + => BinaryPrimitives.ReadInt64BigEndian(value.AsSpan(startIndex)); + + private static long TZif_ToUnixTime(byte[] value, int startIndex, TZVersion version) => + version != TZVersion.V1 ? + TZif_ToInt64(value, startIndex) : + TZif_ToInt32(value, startIndex); + + private static DateTime TZif_UnixTimeToDateTime(long unixTime) => + unixTime < DateTimeOffset.UnixMinSeconds ? DateTime.MinValue : + unixTime > DateTimeOffset.UnixMaxSeconds ? DateTime.MaxValue : + DateTimeOffset.FromUnixTimeSeconds(unixTime).UtcDateTime; + + private static void TZif_ParseRaw(byte[] data, out TZifHead t, out DateTime[] dts, out byte[] typeOfLocalTime, out TZifType[] transitionType, + out string zoneAbbreviations, out bool[] StandardTime, out bool[] GmtTime, out string? futureTransitionsPosixFormat) + { + // initialize the out parameters in case the TZifHead ctor throws + dts = null!; + typeOfLocalTime = null!; + transitionType = null!; + zoneAbbreviations = string.Empty; + StandardTime = null!; + GmtTime = null!; + futureTransitionsPosixFormat = null; + + // read in the 44-byte TZ header containing the count/length fields + // + int index = 0; + t = new TZifHead(data, index); + index += TZifHead.Length; + + int timeValuesLength = 4; // the first version uses 4-bytes to specify times + if (t.Version != TZVersion.V1) + { + // move index past the V1 information to read the V2 information + index += (int)((timeValuesLength * t.TimeCount) + t.TimeCount + (6 * t.TypeCount) + ((timeValuesLength + 4) * t.LeapCount) + t.IsStdCount + t.IsGmtCount + t.CharCount); + + // read the V2 header + t = new TZifHead(data, index); + index += TZifHead.Length; + timeValuesLength = 8; // the second version uses 8-bytes + } + + // initialize the containers for the rest of the TZ data + dts = new DateTime[t.TimeCount]; + typeOfLocalTime = new byte[t.TimeCount]; + transitionType = new TZifType[t.TypeCount]; + zoneAbbreviations = string.Empty; + StandardTime = new bool[t.TypeCount]; + GmtTime = new bool[t.TypeCount]; + + // read in the UTC transition points and convert them to Windows + // + for (int i = 0; i < t.TimeCount; i++) + { + long unixTime = TZif_ToUnixTime(data, index, t.Version); + dts[i] = TZif_UnixTimeToDateTime(unixTime); + index += timeValuesLength; + } + + // read in the Type Indices; there is a 1:1 mapping of UTC transition points to Type Indices + // these indices directly map to the array index in the transitionType array below + // + for (int i = 0; i < t.TimeCount; i++) + { + typeOfLocalTime[i] = data[index]; + index++; + } + + // read in the Type table. Each 6-byte entry represents + // {UtcOffset, IsDst, AbbreviationIndex} + // + // each AbbreviationIndex is a character index into the zoneAbbreviations string below + // + for (int i = 0; i < t.TypeCount; i++) + { + transitionType[i] = new TZifType(data, index); + index += 6; + } + + // read in the Abbreviation ASCII string. This string will be in the form: + // "PST\0PDT\0PWT\0\PPT" + // + Encoding enc = Encoding.UTF8; + zoneAbbreviations = enc.GetString(data, index, (int)t.CharCount); + index += (int)t.CharCount; + + // skip ahead of the Leap-Seconds Adjustment data. In a future release, consider adding + // support for Leap-Seconds + // + index += (int)(t.LeapCount * (timeValuesLength + 4)); // skip the leap second transition times + + // read in the Standard Time table. There should be a 1:1 mapping between Type-Index and Standard + // Time table entries. + // + // TRUE = transition time is standard time + // FALSE = transition time is wall clock time + // ABSENT = transition time is wall clock time + // + for (int i = 0; i < t.IsStdCount && i < t.TypeCount && index < data.Length; i++) + { + StandardTime[i] = (data[index++] != 0); + } + + // read in the GMT Time table. There should be a 1:1 mapping between Type-Index and GMT Time table + // entries. + // + // TRUE = transition time is UTC + // FALSE = transition time is local time + // ABSENT = transition time is local time + // + for (int i = 0; i < t.IsGmtCount && i < t.TypeCount && index < data.Length; i++) { - tzFilePath = tzVariable; + GmtTime[i] = (data[index++] != 0); + } + + if (t.Version != TZVersion.V1) + { + // read the POSIX-style format, which should be wrapped in newlines with the last newline at the end of the file + if (data[index++] == '\n' && data[data.Length - 1] == '\n') + { + futureTransitionsPosixFormat = enc.GetString(data, index, data.Length - index - 1); + } } - return TryLoadTzFile(tzFilePath, ref rawData, ref id); } /// - /// Helper function used by 'GetLocalTimeZone()' - this function wraps the call - /// for loading time zone data from computers without Registry support. - /// - /// The TryGetLocalTzFile() call returns a Byte[] containing the compiled tzfile. + /// Normalize adjustment rule offset so that it is within valid range + /// This method should not be called at all but is here in case something changes in the future + /// or if really old time zones are present on the OS (no combination is known at the moment) /// - private static TimeZoneInfo GetLocalTimeZoneFromTzFile() + private static void NormalizeAdjustmentRuleOffset(TimeSpan baseUtcOffset, [NotNull] ref AdjustmentRule adjustmentRule) + { + // Certain time zones such as: + // Time Zone start date end date offset + // ----------------------------------------------------- + // America/Yakutat 0001-01-01 1867-10-18 14:41:00 + // America/Yakutat 1867-10-18 1900-08-20 14:41:00 + // America/Sitka 0001-01-01 1867-10-18 14:58:00 + // America/Sitka 1867-10-18 1900-08-20 14:58:00 + // Asia/Manila 0001-01-01 1844-12-31 -15:56:00 + // Pacific/Guam 0001-01-01 1845-01-01 -14:21:00 + // Pacific/Saipan 0001-01-01 1845-01-01 -14:21:00 + // + // have larger offset than currently supported by framework. + // If for whatever reason we find that time zone exceeding max + // offset of 14h this function will truncate it to the max valid offset. + // Updating max offset may cause problems with interacting with SQL server + // which uses SQL DATETIMEOFFSET field type which was originally designed to be + // bit-for-bit compatible with DateTimeOffset. + + TimeSpan utcOffset = GetUtcOffset(baseUtcOffset, adjustmentRule); + + // utc base offset delta increment + TimeSpan adjustment = TimeSpan.Zero; + + if (utcOffset > MaxOffset) + { + adjustment = MaxOffset - utcOffset; + } + else if (utcOffset < MinOffset) + { + adjustment = MinOffset - utcOffset; + } + + if (adjustment != TimeSpan.Zero) + { + adjustmentRule = AdjustmentRule.CreateAdjustmentRule( + adjustmentRule.DateStart, + adjustmentRule.DateEnd, + adjustmentRule.DaylightDelta, + adjustmentRule.DaylightTransitionStart, + adjustmentRule.DaylightTransitionEnd, + adjustmentRule.BaseUtcOffsetDelta + adjustment, + adjustmentRule.NoDaylightTransitions); + } + } + + private struct TZifType { - byte[]? rawData; - string? id; - if (TryGetLocalTzFile(out rawData, out id)) + public const int Length = 6; + + public readonly TimeSpan UtcOffset; + public readonly bool IsDst; + public readonly byte AbbreviationIndex; + + public TZifType(byte[] data, int index) { - TimeZoneInfo? result = GetTimeZoneFromTzData(rawData, id); - if (result != null) + if (data == null || data.Length < index + Length) { - return result; + throw new ArgumentException(SR.Argument_TimeZoneInfoInvalidTZif, nameof(data)); } + UtcOffset = new TimeSpan(0, 0, TZif_ToInt32(data, index + 00)); + IsDst = (data[index + 4] != 0); + AbbreviationIndex = data[index + 5]; } - - // if we can't find a local time zone, return UTC - return Utc; } - private static string GetTimeZoneDirectory() + private struct TZifHead { - string? tzDirectory = Environment.GetEnvironmentVariable(TimeZoneDirectoryEnvironmentVariable); + public const int Length = 44; - if (tzDirectory == null) + public readonly uint Magic; // TZ_MAGIC "TZif" + public readonly TZVersion Version; // 1 byte for a \0 or 2 or 3 + // public byte[15] Reserved; // reserved for future use + public readonly uint IsGmtCount; // number of transition time flags + public readonly uint IsStdCount; // number of transition time flags + public readonly uint LeapCount; // number of leap seconds + public readonly uint TimeCount; // number of transition times + public readonly uint TypeCount; // number of local time types + public readonly uint CharCount; // number of abbreviated characters + + public TZifHead(byte[] data, int index) { - tzDirectory = DefaultTimeZoneDirectory; + if (data == null || data.Length < Length) + { + throw new ArgumentException("bad data", nameof(data)); + } + + Magic = (uint)TZif_ToInt32(data, index + 00); + + if (Magic != 0x545A6966) + { + // 0x545A6966 = {0x54, 0x5A, 0x69, 0x66} = "TZif" + throw new ArgumentException(SR.Argument_TimeZoneInfoBadTZif, nameof(data)); + } + + byte version = data[index + 04]; + Version = + version == '2' ? TZVersion.V2 : + version == '3' ? TZVersion.V3 : + TZVersion.V1; // default/fallback to V1 to guard against future, unsupported version numbers + + // skip the 15 byte reserved field + + // don't use the BitConverter class which parses data + // based on the Endianess of the machine architecture. + // this data is expected to always be in "standard byte order", + // regardless of the machine it is being processed on. + + IsGmtCount = (uint)TZif_ToInt32(data, index + 20); + IsStdCount = (uint)TZif_ToInt32(data, index + 24); + LeapCount = (uint)TZif_ToInt32(data, index + 28); + TimeCount = (uint)TZif_ToInt32(data, index + 32); + TypeCount = (uint)TZif_ToInt32(data, index + 36); + CharCount = (uint)TZif_ToInt32(data, index + 40); } - else if (!tzDirectory.EndsWith(Path.DirectorySeparatorChar)) + } + + private enum TZVersion : byte + { + V1 = 0, + V2, + V3, + // when adding more versions, ensure all the logic using TZVersion is still correct + } + + // Helper function for string array search. (LINQ is not available here.) + private static bool StringArrayContains(string value, string[] source, StringComparison comparison) + { + foreach (string s in source) { - tzDirectory += PathInternal.DirectorySeparatorCharAsString; + if (string.Equals(s, value, comparison)) + { + return true; + } } - return tzDirectory; + return false; } } } From 6ccad5d410b5b296d54b647ce963f1a80a3520d2 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 9 Jul 2021 13:56:26 -0400 Subject: [PATCH 62/81] Avoid unnecessary allocation --- .../src/System/TimeZoneInfo.Unix.Android.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs index 3d94ac44e7b739..c0866fc5bf1e90 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs @@ -271,14 +271,11 @@ private unsafe void ReadHeader(string tzFilePath, Stream fs) header.dataOffset = NetworkToHostOrder(header.dataOffset); // tzdata files are expected to start with the form of "tzdata2012f\0" depending on the year of the tzdata used which is 2012 in this example - sbyte* s = (sbyte*)header.signature; - string magic = new string(s, 0, 6, Encoding.ASCII); - - if (magic != "tzdata" || header.signature[11] != 0) + if (header.signature[0] != (byte)'t' || header.signature[1] != (byte)'z' || header.signature[2] != (byte)'d' || header.signature[3] != (byte)'a' || header.signature[4] != (byte)'t' || header.signature[5] != (byte)'a' || header.signature[11] != 0) { var b = new StringBuilder(); for (int i = 0; i < 12; ++i) { - b.Append(' ').Append(((byte)s[i]).ToString("x2")); + b.Append(' ').Append((header.signature[i]).ToString("x2")); } throw new InvalidOperationException(SR.Format(SR.InvalidOperation_BadTZHeader, tzFilePath, b.ToString())); From 0987ee76d23ea4b8f8a44971e057def4575b015a Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 9 Jul 2021 14:11:33 -0400 Subject: [PATCH 63/81] Assert buffer size over creating resource exception --- .../System.Private.CoreLib/src/Resources/Strings.resx | 3 --- .../src/System/TimeZoneInfo.Unix.Android.cs | 6 ++---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index 8569f8409c266f..aea4d80bd2a52a 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -3790,9 +3790,6 @@ Bad magic in '{0}': Header starts with '{1}' instead of 'tzdata' - - private error: buffer too small - Unable to fully read from file '{0}' at offset {1} length {2}; read {3} bytes expected {4}. diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs index c0866fc5bf1e90..9daec9f6cc1451 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.InteropServices; @@ -290,10 +291,7 @@ private unsafe T ReadAt(string tzFilePath, Stream fs, long position, Span= size); fs.Position = position; int numBytesRead; From 8d88a6d1b2df1fd9e244ee0fb724eb1844034415 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 13 Jul 2021 14:49:28 -0400 Subject: [PATCH 64/81] Use compare exchange atomic method --- .../src/System/TimeZoneInfo.Unix.Android.cs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs index 9daec9f6cc1451..3183dc8b8d7221 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs @@ -7,6 +7,7 @@ using System.IO; using System.Runtime.InteropServices; using System.Text; +using System.Threading; namespace System { @@ -15,22 +16,12 @@ public sealed partial class TimeZoneInfo private const string TimeZoneFileName = "tzdata"; private static AndroidTzData? s_tzData; - private static readonly object s_tzDataLock = new object(); private static AndroidTzData AndroidTzDataInstance { get { - if (s_tzData == null) - { - lock (s_tzDataLock) - { - if (s_tzData == null) - { - s_tzData = new AndroidTzData(); - } - } - } + Interlocked.CompareExchange(ref s_tzData, new AndroidTzData(), null); return s_tzData; } From 4433be63c162a0a925d82463d8b8b5d9664e3341 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 13 Jul 2021 16:43:03 -0400 Subject: [PATCH 65/81] Reduce redundant calls --- .../src/System/TimeZoneInfo.Unix.Android.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs index 3183dc8b8d7221..183849e3a32321 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs @@ -165,19 +165,22 @@ private static string GetApexRuntimeRoot() // Also follows the locations found at the bottom of https://github.com/aosp-mirror/platform_bionic/blob/master/libc/tzcode/bionic.cpp private static string GetTimeZoneDirectory() { + string apexTzDataFileDir = GetApexTimeDataRoot() + "/etc/tz/"; // Android 10+, TimeData module where the updates land - if (File.Exists(Path.Combine(GetApexTimeDataRoot() + "/etc/tz/", TimeZoneFileName))) + if (File.Exists(Path.Combine(apexTzDataFileDir, TimeZoneFileName))) { - return GetApexTimeDataRoot() + "/etc/tz/"; + return apexTzDataFileDir; } + string apexRuntimeFileDir = GetApexRuntimeRoot() + "/etc/tz/"; // Android 10+, Fallback location if the above isn't found or corrupted - if (File.Exists(Path.Combine(GetApexRuntimeRoot() + "/etc/tz/", TimeZoneFileName))) + if (File.Exists(Path.Combine(apexRuntimeFileDir, TimeZoneFileName))) { - return GetApexRuntimeRoot() + "/etc/tz/"; + return apexRuntimeFileDir; } - if (File.Exists(Path.Combine(Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/", TimeZoneFileName))) + string androidDataFileDir = Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/"; + if (File.Exists(Path.Combine(androidDataFileDir, TimeZoneFileName))) { - return Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/"; + return androidDataFileDir; } return Environment.GetEnvironmentVariable("ANDROID_ROOT") + DefaultTimeZoneDirectory; From 8330c2a5050b4ed16f749c9b4d8d6a91f8398bfa Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 13 Jul 2021 16:45:13 -0400 Subject: [PATCH 66/81] Cleanup output variables --- .../src/System/TimeZoneInfo.Unix.Android.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs index 183849e3a32321..9596a1f0f65c62 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs @@ -188,9 +188,6 @@ private static string GetTimeZoneDirectory() private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, out TimeZoneInfo? value, out Exception? e) { - value = null; - e = null; - value = id == LocalId ? GetLocalTimeZoneCore() : GetTimeZone(id, id); if (value == null) @@ -199,6 +196,7 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, return TimeZoneInfoResult.TimeZoneNotFoundException; } + e = null; return TimeZoneInfoResult.Success; } From b8bcce48bb38bd2e4a4c108a8b191dd36d78ed6c Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 13 Jul 2021 16:45:37 -0400 Subject: [PATCH 67/81] Remove comments --- .../src/System/TimeZoneInfo.Unix.Android.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs index 9596a1f0f65c62..37f7d00868a7a2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs @@ -294,7 +294,7 @@ private unsafe T ReadAt(string tzFilePath, Stream fs, long position, Span Date: Tue, 13 Jul 2021 16:47:55 -0400 Subject: [PATCH 68/81] Use sizeof over Marshal --- .../src/System/TimeZoneInfo.Unix.Android.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs index 37f7d00868a7a2..973118be1b38b9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs @@ -256,7 +256,7 @@ public AndroidTzData() [MemberNotNull(nameof(_lengths))] private unsafe void ReadHeader(string tzFilePath, Stream fs) { - int size = Math.Max(Marshal.SizeOf(typeof(AndroidTzDataHeader)), Marshal.SizeOf(typeof(AndroidTzDataEntry))); + int size = Math.Max(sizeof(AndroidTzDataHeader)), sizeof(AndroidTzDataEntry))); Span buffer = stackalloc byte[size]; AndroidTzDataHeader header = ReadAt(tzFilePath, fs, 0, buffer); @@ -282,7 +282,7 @@ private unsafe void ReadHeader(string tzFilePath, Stream fs) private unsafe T ReadAt(string tzFilePath, Stream fs, long position, Span buffer) where T : struct { - int size = Marshal.SizeOf(typeof(T)); + int size = sizeof(T)); Debug.Assert(buffer.Length >= size); fs.Position = position; @@ -325,7 +325,7 @@ private static unsafe int GetStringLength(sbyte* s, int maxLength) private unsafe void ReadIndex(string tzFilePath, Stream fs, int indexOffset, int dataOffset, Span buffer) { int indexSize = dataOffset - indexOffset; - int entrySize = Marshal.SizeOf(typeof(AndroidTzDataEntry)); + int entrySize = sizeof(AndroidTzDataEntry)); int entryCount = indexSize / entrySize; _byteOffsets = new int[entryCount]; @@ -341,7 +341,7 @@ private unsafe void ReadIndex(string tzFilePath, Stream fs, int indexOffset, int _ids![i] = new string(p, 0, GetStringLength(p, 40), Encoding.ASCII); _lengths![i] = NetworkToHostOrder(entry.length); - if (_lengths![i] < Marshal.SizeOf(typeof(AndroidTzDataHeader))) + if (_lengths![i] < sizeof(AndroidTzDataHeader))) { throw new InvalidOperationException(SR.InvalidOperation_BadIndexLength); } From 88917097f6fb0c603a7cfb1523124d45be445752 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 13 Jul 2021 16:49:49 -0400 Subject: [PATCH 69/81] Address feedback --- .../src/System/TimeZoneInfo.Unix.Android.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs index 973118be1b38b9..0db723a03211cc 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs @@ -266,9 +266,9 @@ private unsafe void ReadHeader(string tzFilePath, Stream fs) // tzdata files are expected to start with the form of "tzdata2012f\0" depending on the year of the tzdata used which is 2012 in this example if (header.signature[0] != (byte)'t' || header.signature[1] != (byte)'z' || header.signature[2] != (byte)'d' || header.signature[3] != (byte)'a' || header.signature[4] != (byte)'t' || header.signature[5] != (byte)'a' || header.signature[11] != 0) { - var b = new StringBuilder(); + var b = new StringBuilder(buffer.Length); for (int i = 0; i < 12; ++i) { - b.Append(' ').Append((header.signature[i]).ToString("x2")); + b.Append(' ').Append(HexConverter.ToCharLower(buffer[i])); } throw new InvalidOperationException(SR.Format(SR.InvalidOperation_BadTZHeader, tzFilePath, b.ToString())); From 15d4455176e69648c14123c4b8bac559df425e94 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 14 Jul 2021 12:26:42 -0400 Subject: [PATCH 70/81] Address Feedback Re add check for corrupted tzdata files Implement tzdata file parsing --- .../src/Resources/Strings.resx | 3 + .../src/System/TimeZoneInfo.Unix.Android.cs | 273 +++++++++++------- .../src/System/TimeZoneInfo.Unix.cs | 10 + 3 files changed, 174 insertions(+), 112 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index e885d4ef0ddedb..85df61e89b9f98 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -3805,4 +3805,7 @@ Length in index file less than AndroidTzDataHeader + + Unable to properly load any time zone data files. + diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs index 0db723a03211cc..a39e3c46606821 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.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.Binary; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -21,7 +22,10 @@ private static AndroidTzData AndroidTzDataInstance { get { - Interlocked.CompareExchange(ref s_tzData, new AndroidTzData(), null); + if (s_tzData == null) + { + Interlocked.CompareExchange(ref s_tzData, new AndroidTzData(), null); + } return s_tzData; } @@ -138,61 +142,14 @@ private static TimeZoneInfo GetLocalTimeZoneCore() return Utc; } - private static string GetApexTimeDataRoot() - { - string? ret = Environment.GetEnvironmentVariable("ANDROID_TZDATA_ROOT"); - if (!string.IsNullOrEmpty(ret)) - { - return ret; - } - - return "/apex/com.android.tzdata"; - } - - private static string GetApexRuntimeRoot() - { - string? ret = Environment.GetEnvironmentVariable("ANDROID_RUNTIME_ROOT"); - if (!string.IsNullOrEmpty(ret)) - { - return ret; - } - - return "/apex/com.android.runtime"; - } - - // On Android, time zone data is found in tzdata - // Based on https://github.com/mono/mono/blob/main/mcs/class/corlib/System/TimeZoneInfo.Android.cs - // Also follows the locations found at the bottom of https://github.com/aosp-mirror/platform_bionic/blob/master/libc/tzcode/bionic.cpp - private static string GetTimeZoneDirectory() - { - string apexTzDataFileDir = GetApexTimeDataRoot() + "/etc/tz/"; - // Android 10+, TimeData module where the updates land - if (File.Exists(Path.Combine(apexTzDataFileDir, TimeZoneFileName))) - { - return apexTzDataFileDir; - } - string apexRuntimeFileDir = GetApexRuntimeRoot() + "/etc/tz/"; - // Android 10+, Fallback location if the above isn't found or corrupted - if (File.Exists(Path.Combine(apexRuntimeFileDir, TimeZoneFileName))) - { - return apexRuntimeFileDir; - } - string androidDataFileDir = Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/"; - if (File.Exists(Path.Combine(androidDataFileDir, TimeZoneFileName))) - { - return androidDataFileDir; - } - - return Environment.GetEnvironmentVariable("ANDROID_ROOT") + DefaultTimeZoneDirectory; - } - private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, out TimeZoneInfo? value, out Exception? e) { + value = id == LocalId ? GetLocalTimeZoneCore() : GetTimeZone(id, id); if (value == null) { - e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, GetTimeZoneDirectory() + TimeZoneFileName)); + e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, AndroidTzDataInstance.GetTimeZoneDirectory() + TimeZoneFileName)); return TimeZoneInfoResult.TimeZoneNotFoundException; } @@ -238,94 +195,140 @@ private unsafe struct AndroidTzDataEntry public int unused; // Was raw GMT offset; always 0 since tzdata2014f (L). } - private string[] _ids; - private int[] _byteOffsets; - private int[] _lengths; + private string[]? _ids; + private int[]? _byteOffsets; + private int[]? _lengths; + private string _tzFileDir; + private string _tzFilePath; - public AndroidTzData() + private static string GetApexTimeDataRoot() { - string tzFilePath = GetTimeZoneDirectory() + TimeZoneFileName; - using (FileStream fs = File.OpenRead(tzFilePath)) + string? ret = Environment.GetEnvironmentVariable("ANDROID_TZDATA_ROOT"); + if (!string.IsNullOrEmpty(ret)) { - ReadHeader(tzFilePath, fs); + return ret; } + + return "/apex/com.android.tzdata"; } - [MemberNotNull(nameof(_ids))] - [MemberNotNull(nameof(_byteOffsets))] - [MemberNotNull(nameof(_lengths))] - private unsafe void ReadHeader(string tzFilePath, Stream fs) + private static string GetApexRuntimeRoot() { - int size = Math.Max(sizeof(AndroidTzDataHeader)), sizeof(AndroidTzDataEntry))); - Span buffer = stackalloc byte[size]; - AndroidTzDataHeader header = ReadAt(tzFilePath, fs, 0, buffer); - - header.indexOffset = NetworkToHostOrder(header.indexOffset); - header.dataOffset = NetworkToHostOrder(header.dataOffset); - - // tzdata files are expected to start with the form of "tzdata2012f\0" depending on the year of the tzdata used which is 2012 in this example - if (header.signature[0] != (byte)'t' || header.signature[1] != (byte)'z' || header.signature[2] != (byte)'d' || header.signature[3] != (byte)'a' || header.signature[4] != (byte)'t' || header.signature[5] != (byte)'a' || header.signature[11] != 0) + string? ret = Environment.GetEnvironmentVariable("ANDROID_RUNTIME_ROOT"); + if (!string.IsNullOrEmpty(ret)) { - var b = new StringBuilder(buffer.Length); - for (int i = 0; i < 12; ++i) { - b.Append(' ').Append(HexConverter.ToCharLower(buffer[i])); - } - - throw new InvalidOperationException(SR.Format(SR.InvalidOperation_BadTZHeader, tzFilePath, b.ToString())); + return ret; } - ReadIndex(tzFilePath, fs, header.indexOffset, header.dataOffset, buffer); + return "/apex/com.android.runtime"; } - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2087:UnrecognizedReflectionPattern", - Justification = "Implementation detail of Android TimeZone")] - private unsafe T ReadAt(string tzFilePath, Stream fs, long position, Span buffer) - where T : struct + public AndroidTzData() { - int size = sizeof(T)); - Debug.Assert(buffer.Length >= size); - fs.Position = position; - int numBytesRead; - if ((numBytesRead = fs.Read(buffer)) < size) + string[] tzFileDirList = new string[] {GetApexTimeDataRoot() + "/etc/tz/", // Android 10+, TimeData module where the updates land + GetApexRuntimeRoot() + "/etc/tz/", // Android 10+, Fallback location if the above isn't found or corrupted + Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/", + Environment.GetEnvironmentVariable("ANDROID_ROOT") + DefaultTimeZoneDirectory}; + foreach (var tzFileDir in tzFileDirList) { - throw new InvalidOperationException(SR.Format(SR.InvalidOperation_ReadTZError, tzFilePath, position, size, numBytesRead, size)); + string tzFilePath = Path.Combine(tzFileDir, TimeZoneFileName); + if (LoadData(tzFilePath)) + { + _tzFileDir = tzFileDir; + _tzFilePath = tzFilePath; + return; + } } - fixed (byte* b = buffer) + throw new TimeZoneNotFoundException(SR.TimeZoneNotFound_ValidTimeZoneFileMissing); + } + + private bool LoadData(string path) + { + if (!File.Exists(path)) { - return (T)Marshal.PtrToStructure((IntPtr)b, typeof(T))!; + return false; } + try { + using (FileStream fs = File.OpenRead(path)) + { + LoadTzFile(fs); + } + return true; + } + catch (IOException) {} + + return false; } - private static int NetworkToHostOrder(int value) + private unsafe void LoadTzFile(Stream fs) { - if (!BitConverter.IsLittleEndian) - return value; + int headerSize = 24; + Span buffer = stackalloc byte[headerSize]; + int bytesRead = 0; + int bytesLeft = headerSize; - return (((value >> 24) & 0xFF) | ((value >> 08) & 0xFF00) | ((value << 08) & 0xFF0000) | ((value << 24))); + while (bytesLeft > 0) + { + int b = fs.Read(buffer); + if (b == 0) + { + break; + } + + bytesRead += b; + bytesLeft -= b; + } + + AndroidTzDataHeader header = LoadHeader(buffer); + ReadIndex(fs, header.indexOffset, header.dataOffset); } - private static unsafe int GetStringLength(sbyte* s, int maxLength) + private unsafe AndroidTzDataHeader LoadHeader(Span buffer) { - int len; - for (len = 0; len < maxLength; len++, s++) + // tzdata files are expected to start with the form of "tzdata2012f\0" depending on the year of the tzdata used which is 2012 in this example + // since we're not differntiating on year, check for tzdata and the ending \0 + Span signature = buffer.Slice(0, 12); + Span tzDataSignature = stackalloc byte[8]; + tzDataSignature.Clear(); + + signature.Slice(0, 6).CopyTo(tzDataSignature); + ulong tzdata = (ulong)TZif_ToInt64(tzDataSignature); + + char[] cbits = Encoding.Default.GetChars(tzDataSignature.ToArray()); + + string sigBits = ""; + for (int i = 0; i < signature.Length; i++) { - if (*s == 0) + sigBits += signature[i].ToString() + " "; + } + + if (tzdata != 0x747A646174610000 || signature[11] != 0) + { + // 0x747A646174640000 = {0x74, 0x7A, 0x64, 0x61, 0x74, 0x64, 0x00, 0x00} = "tzdata[NUL][NUL]" + var b = new StringBuilder(signature.Length); + for (int i = 0; i < signature.Length; ++i) { - break; + b.Append(' ').Append(HexConverter.ToCharLower(signature[i])); } + + throw new InvalidOperationException(SR.Format(SR.InvalidOperation_BadTZHeader, TimeZoneFileName, b.ToString())); } - return len; + + AndroidTzDataHeader header; + + header.indexOffset = TZif_ToInt32(buffer.Slice(12, 4)); + header.dataOffset = TZif_ToInt32(buffer.Slice(16, 4)); + header.finalOffset = TZif_ToInt32(buffer.Slice(20, 4)); + + return header; } - [MemberNotNull(nameof(_ids))] - [MemberNotNull(nameof(_byteOffsets))] - [MemberNotNull(nameof(_lengths))] - private unsafe void ReadIndex(string tzFilePath, Stream fs, int indexOffset, int dataOffset, Span buffer) + private unsafe void ReadIndex(Stream fs, int indexOffset, int dataOffset) { int indexSize = dataOffset - indexOffset; - int entrySize = sizeof(AndroidTzDataEntry)); + int entrySize = sizeof(AndroidTzDataEntry); int entryCount = indexSize / entrySize; _byteOffsets = new int[entryCount]; @@ -334,25 +337,72 @@ private unsafe void ReadIndex(string tzFilePath, Stream fs, int indexOffset, int for (int i = 0; i < entryCount; ++i) { - AndroidTzDataEntry entry = ReadAt(tzFilePath, fs, indexOffset + (entrySize*i), buffer); + AndroidTzDataEntry entry = LoadEntryAt(fs, indexOffset + (entrySize*i)); var p = (sbyte*)entry.id; - _byteOffsets![i] = NetworkToHostOrder(entry.byteOffset) + dataOffset; - _ids![i] = new string(p, 0, GetStringLength(p, 40), Encoding.ASCII); - _lengths![i] = NetworkToHostOrder(entry.length); + _byteOffsets![i] = entry.byteOffset + dataOffset; + _ids![i] = new string(p); + _lengths![i] = entry.length; - if (_lengths![i] < sizeof(AndroidTzDataHeader))) + if (_lengths![i] < sizeof(AndroidTzDataHeader)) { throw new InvalidOperationException(SR.InvalidOperation_BadIndexLength); } } } + private unsafe AndroidTzDataEntry LoadEntryAt(Stream fs, long position) + { + int size = sizeof(AndroidTzDataEntry); + Span entryBuffer = stackalloc byte[size]; + + fs.Position = position; + + int bytesRead = 0; + int bytesLeft = size; + + while (bytesLeft > 0) + { + int b = fs.Read(entryBuffer); + if (b == 0) + { + break; + } + + bytesRead += b; + bytesLeft -= b; + } + + if (bytesLeft != 0) + { + throw new InvalidOperationException(SR.Format(SR.InvalidOperation_ReadTZError, _tzFilePath, position, size, bytesRead, size)); + } + + AndroidTzDataEntry entry; + for (int i = 0; i < 40; i++) + { + entry.id[i] = entryBuffer[i]; + } + entry.byteOffset = TZif_ToInt32(entryBuffer.Slice(40, 4)); + entry.length = TZif_ToInt32(entryBuffer.Slice(44, 4)); + entry.unused = TZif_ToInt32(entryBuffer.Slice(48)); + + return entry; + } + public string[] GetTimeZoneIds() { return _ids!; } + // On Android, time zone data is found in tzdata + // Based on https://github.com/mono/mono/blob/main/mcs/class/corlib/System/TimeZoneInfo.Android.cs + // Also follows the locations found at the bottom of https://github.com/aosp-mirror/platform_bionic/blob/master/libc/tzcode/bionic.cpp + public string GetTimeZoneDirectory() + { + return _tzFilePath!; + } + public byte[] GetTimeZoneData(string id) { int i = Array.BinarySearch(_ids!, id, StringComparer.Ordinal); @@ -364,14 +414,13 @@ public byte[] GetTimeZoneData(string id) int offset = _byteOffsets![i]; int length = _lengths![i]; var buffer = new byte[length]; - string tzFilePath = GetTimeZoneDirectory() + TimeZoneFileName; - using (FileStream fs = File.OpenRead(tzFilePath)) + using (FileStream fs = File.OpenRead(_tzFilePath)) { fs.Position = offset; int numBytesRead; if ((numBytesRead = fs.Read(buffer)) < buffer.Length) { - throw new InvalidOperationException(string.Format(SR.InvalidOperation_ReadTZError, tzFilePath, offset, length, numBytesRead, buffer.Length)); + throw new InvalidOperationException(string.Format(SR.InvalidOperation_ReadTZError, _tzFilePath, offset, length, numBytesRead, buffer.Length)); } } 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 3450764e8cb932..7ab8e0eaee575d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -1096,11 +1096,21 @@ private static string TZif_GetZoneAbbreviation(string zoneAbbreviations, int ind private static int TZif_ToInt32(byte[] value, int startIndex) => BinaryPrimitives.ReadInt32BigEndian(value.AsSpan(startIndex)); + // Converts a span of bytes into an int - always using standard byte order (Big Endian) + // per TZif file standard + private static int TZif_ToInt32(ReadOnlySpan value) + => BinaryPrimitives.ReadInt32BigEndian(value); + // Converts an array of bytes into a long - always using standard byte order (Big Endian) // per TZif file standard private static long TZif_ToInt64(byte[] value, int startIndex) => BinaryPrimitives.ReadInt64BigEndian(value.AsSpan(startIndex)); + // Converts a span of bytes into a long - always using standard byte order (Big Endian) + // per TZif file standard + private static long TZif_ToInt64(ReadOnlySpan value) + => BinaryPrimitives.ReadInt64BigEndian(value); + private static long TZif_ToUnixTime(byte[] value, int startIndex, TZVersion version) => version != TZVersion.V1 ? TZif_ToInt64(value, startIndex) : From 6501d7ee2229949b38c11e84fab8a47f6a3eeff8 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 16 Jul 2021 14:51:14 -0400 Subject: [PATCH 71/81] Address more feedback --- .../src/System/TimeZoneInfo.Unix.Android.cs | 81 +++++++++---------- .../src/System/TimeZoneInfo.Unix.cs | 9 ++- 2 files changed, 44 insertions(+), 46 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs index a39e3c46606821..fb3619681ac0ca 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs @@ -177,27 +177,30 @@ private static string[] GetTimeZoneIds() */ private sealed class AndroidTzData { - [StructLayout(LayoutKind.Sequential, Pack=1)] + // public fixed byte signature[12]; // "tzdata2012f\0" + // public int indexOffset; + // public int dataOffset; + // public int finalOffset; private unsafe struct AndroidTzDataHeader { - public fixed byte signature[12]; // "tzdata2012f\0" public int indexOffset; public int dataOffset; - public int finalOffset; } - [StructLayout(LayoutKind.Sequential, Pack=1)] + // public string id - 40 bytes + // public int byteOffset; - 4 bytes + // public int length - 4 bytes + // public int unused - 4 bytes Was raw GMT offset; always 0 since tzdata2014f (L). private unsafe struct AndroidTzDataEntry { public fixed byte id[40]; public int byteOffset; public int length; - public int unused; // Was raw GMT offset; always 0 since tzdata2014f (L). } - private string[]? _ids; - private int[]? _byteOffsets; - private int[]? _lengths; + private string[] _ids; + private int[] _byteOffsets; + private int[] _lengths; private string _tzFileDir; private string _tzFilePath; @@ -225,7 +228,6 @@ private static string GetApexRuntimeRoot() public AndroidTzData() { - string[] tzFileDirList = new string[] {GetApexTimeDataRoot() + "/etc/tz/", // Android 10+, TimeData module where the updates land GetApexRuntimeRoot() + "/etc/tz/", // Android 10+, Fallback location if the above isn't found or corrupted Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/", @@ -244,6 +246,9 @@ public AndroidTzData() throw new TimeZoneNotFoundException(SR.TimeZoneNotFound_ValidTimeZoneFileMissing); } + [MemberNotNullWhen(true, nameof(_ids))] + [MemberNotNullWhen(true, nameof(_byteOffsets))] + [MemberNotNullWhen(true, nameof(_lengths))] private bool LoadData(string path) { if (!File.Exists(path)) @@ -257,24 +262,27 @@ private bool LoadData(string path) } return true; } - catch (IOException) {} + catch {} return false; } - private unsafe void LoadTzFile(Stream fs) + [MemberNotNull(nameof(_ids))] + [MemberNotNull(nameof(_byteOffsets))] + [MemberNotNull(nameof(_lengths))] + private void LoadTzFile(Stream fs) { - int headerSize = 24; - Span buffer = stackalloc byte[headerSize]; + const int HeaderSize = 24; // AndroidTzDataHeader 12 bytes signature, 4 bytes index offset, 4 bytes data offset, 4 bytes final offset + Span buffer = stackalloc byte[HeaderSize]; int bytesRead = 0; - int bytesLeft = headerSize; + int bytesLeft = HeaderSize; while (bytesLeft > 0) { int b = fs.Read(buffer); if (b == 0) { - break; + throw new InvalidOperationException(SR.Format(SR.InvalidOperation_ReadTZError, _tzFilePath, 0, HeaderSize, bytesRead, HeaderSize)); } bytesRead += b; @@ -285,32 +293,20 @@ private unsafe void LoadTzFile(Stream fs) ReadIndex(fs, header.indexOffset, header.dataOffset); } - private unsafe AndroidTzDataHeader LoadHeader(Span buffer) + private AndroidTzDataHeader LoadHeader(Span buffer) { // tzdata files are expected to start with the form of "tzdata2012f\0" depending on the year of the tzdata used which is 2012 in this example - // since we're not differntiating on year, check for tzdata and the ending \0 - Span signature = buffer.Slice(0, 12); - Span tzDataSignature = stackalloc byte[8]; - tzDataSignature.Clear(); - - signature.Slice(0, 6).CopyTo(tzDataSignature); - ulong tzdata = (ulong)TZif_ToInt64(tzDataSignature); - - char[] cbits = Encoding.Default.GetChars(tzDataSignature.ToArray()); - - string sigBits = ""; - for (int i = 0; i < signature.Length; i++) - { - sigBits += signature[i].ToString() + " "; - } + // since we're not differentiating on year, check for tzdata and the ending \0 + var tz = (ushort)TZif_ToInt16(buffer.Slice(0, 2)); + var data = (uint)TZif_ToInt32(buffer.Slice(2, 4)); - if (tzdata != 0x747A646174610000 || signature[11] != 0) + if (tz != 0x747A || data != 0x64617461 || buffer[11] != 0) { - // 0x747A646174640000 = {0x74, 0x7A, 0x64, 0x61, 0x74, 0x64, 0x00, 0x00} = "tzdata[NUL][NUL]" - var b = new StringBuilder(signature.Length); - for (int i = 0; i < signature.Length; ++i) + // 0x747A 0x646174640000 = {0x74, 0x7A} {0x64, 0x61, 0x74, 0x64} = "tz" "data" + var b = new StringBuilder(buffer.Length); + for (int i = 0; i < 12; ++i) { - b.Append(' ').Append(HexConverter.ToCharLower(signature[i])); + b.Append(' ').Append(HexConverter.ToCharLower(buffer[i])); } throw new InvalidOperationException(SR.Format(SR.InvalidOperation_BadTZHeader, TimeZoneFileName, b.ToString())); @@ -320,15 +316,17 @@ private unsafe AndroidTzDataHeader LoadHeader(Span buffer) header.indexOffset = TZif_ToInt32(buffer.Slice(12, 4)); header.dataOffset = TZif_ToInt32(buffer.Slice(16, 4)); - header.finalOffset = TZif_ToInt32(buffer.Slice(20, 4)); return header; } + [MemberNotNull(nameof(_ids))] + [MemberNotNull(nameof(_byteOffsets))] + [MemberNotNull(nameof(_lengths))] private unsafe void ReadIndex(Stream fs, int indexOffset, int dataOffset) { int indexSize = dataOffset - indexOffset; - int entrySize = sizeof(AndroidTzDataEntry); + int entrySize = 52; // Size of AndroidTzDataEntry int entryCount = indexSize / entrySize; _byteOffsets = new int[entryCount]; @@ -344,7 +342,7 @@ private unsafe void ReadIndex(Stream fs, int indexOffset, int dataOffset) _ids![i] = new string(p); _lengths![i] = entry.length; - if (_lengths![i] < sizeof(AndroidTzDataHeader)) + if (_lengths![i] < 24) // AndroidTzDataHeader 12 bytes signature, 4 bytes index offset, 4 bytes data offset, 4 bytes final offset { throw new InvalidOperationException(SR.InvalidOperation_BadIndexLength); } @@ -353,7 +351,7 @@ private unsafe void ReadIndex(Stream fs, int indexOffset, int dataOffset) private unsafe AndroidTzDataEntry LoadEntryAt(Stream fs, long position) { - int size = sizeof(AndroidTzDataEntry); + int size = 52; // AndroidTzDataEntry Span entryBuffer = stackalloc byte[size]; fs.Position = position; @@ -385,7 +383,6 @@ private unsafe AndroidTzDataEntry LoadEntryAt(Stream fs, long position) } entry.byteOffset = TZif_ToInt32(entryBuffer.Slice(40, 4)); entry.length = TZif_ToInt32(entryBuffer.Slice(44, 4)); - entry.unused = TZif_ToInt32(entryBuffer.Slice(48)); return entry; } @@ -400,7 +397,7 @@ public string[] GetTimeZoneIds() // Also follows the locations found at the bottom of https://github.com/aosp-mirror/platform_bionic/blob/master/libc/tzcode/bionic.cpp public string GetTimeZoneDirectory() { - return _tzFilePath!; + return _tzFilePath; } public byte[] GetTimeZoneData(string id) 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 7ab8e0eaee575d..7b5131d053f92c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -1091,6 +1091,11 @@ private static string TZif_GetZoneAbbreviation(string zoneAbbreviations, int ind zoneAbbreviations.Substring(index); } + // Converts a span of bytes into a long - always using standard byte order (Big Endian) + // per TZif file standard + private static short TZif_ToInt16(ReadOnlySpan value) + => BinaryPrimitives.ReadInt16BigEndian(value); + // Converts an array of bytes into an int - always using standard byte order (Big Endian) // per TZif file standard private static int TZif_ToInt32(byte[] value, int startIndex) @@ -1106,10 +1111,6 @@ private static int TZif_ToInt32(ReadOnlySpan value) private static long TZif_ToInt64(byte[] value, int startIndex) => BinaryPrimitives.ReadInt64BigEndian(value.AsSpan(startIndex)); - // Converts a span of bytes into a long - always using standard byte order (Big Endian) - // per TZif file standard - private static long TZif_ToInt64(ReadOnlySpan value) - => BinaryPrimitives.ReadInt64BigEndian(value); private static long TZif_ToUnixTime(byte[] value, int startIndex, TZVersion version) => version != TZVersion.V1 ? From a63f8f9c41a2f999a4ea4f3d71b8aa89b32767cc Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 16 Jul 2021 15:21:28 -0400 Subject: [PATCH 72/81] Separate buffer loading into independent method --- .../src/System/TimeZoneInfo.Unix.Android.cs | 49 +++++++------------ 1 file changed, 18 insertions(+), 31 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs index fb3619681ac0ca..b6d9ae3f4cec38 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs @@ -274,20 +274,8 @@ private void LoadTzFile(Stream fs) { const int HeaderSize = 24; // AndroidTzDataHeader 12 bytes signature, 4 bytes index offset, 4 bytes data offset, 4 bytes final offset Span buffer = stackalloc byte[HeaderSize]; - int bytesRead = 0; - int bytesLeft = HeaderSize; - - while (bytesLeft > 0) - { - int b = fs.Read(buffer); - if (b == 0) - { - throw new InvalidOperationException(SR.Format(SR.InvalidOperation_ReadTZError, _tzFilePath, 0, HeaderSize, bytesRead, HeaderSize)); - } - bytesRead += b; - bytesLeft -= b; - } + ReadTzDataIntoBuffer(fs, 0, buffer); AndroidTzDataHeader header = LoadHeader(buffer); ReadIndex(fs, header.indexOffset, header.dataOffset); @@ -349,19 +337,16 @@ private unsafe void ReadIndex(Stream fs, int indexOffset, int dataOffset) } } - private unsafe AndroidTzDataEntry LoadEntryAt(Stream fs, long position) + private void ReadTzDataIntoBuffer(Stream fs, long position, Span buffer) { - int size = 52; // AndroidTzDataEntry - Span entryBuffer = stackalloc byte[size]; - fs.Position = position; int bytesRead = 0; - int bytesLeft = size; + int bytesLeft = buffer.Length; while (bytesLeft > 0) { - int b = fs.Read(entryBuffer); + int b = fs.Read(buffer); if (b == 0) { break; @@ -373,8 +358,16 @@ private unsafe AndroidTzDataEntry LoadEntryAt(Stream fs, long position) if (bytesLeft != 0) { - throw new InvalidOperationException(SR.Format(SR.InvalidOperation_ReadTZError, _tzFilePath, position, size, bytesRead, size)); + throw new InvalidOperationException(SR.Format(SR.InvalidOperation_ReadTZError, _tzFilePath, position, buffer.Length, bytesRead, buffer.Length)); } + } + + private unsafe AndroidTzDataEntry LoadEntryAt(Stream fs, long position) + { + int size = 52; // AndroidTzDataEntry + Span entryBuffer = stackalloc byte[size]; + + ReadTzDataIntoBuffer(fs, position, entryBuffer); AndroidTzDataEntry entry; for (int i = 0; i < 40; i++) @@ -410,18 +403,12 @@ public byte[] GetTimeZoneData(string id) int offset = _byteOffsets![i]; int length = _lengths![i]; - var buffer = new byte[length]; - using (FileStream fs = File.OpenRead(_tzFilePath)) - { - fs.Position = offset; - int numBytesRead; - if ((numBytesRead = fs.Read(buffer)) < buffer.Length) - { - throw new InvalidOperationException(string.Format(SR.InvalidOperation_ReadTZError, _tzFilePath, offset, length, numBytesRead, buffer.Length)); - } - } - return buffer; + Span buffer = stackalloc byte[length]; + ReadTzDataIntoBuffer(File.OpenRead(_tzFilePath), offset, buffer); + byte[] tzBuffer = buffer.ToArray(); + + return tzBuffer; } } } From 16289d2b5058052f9b858e4a1c6c7f063fe79958 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 16 Jul 2021 16:29:14 -0400 Subject: [PATCH 73/81] Properly set where to write into the buffer --- .../src/System/TimeZoneInfo.Unix.Android.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs index b6d9ae3f4cec38..3b493968587a23 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs @@ -346,7 +346,7 @@ private void ReadTzDataIntoBuffer(Stream fs, long position, Span buffer) while (bytesLeft > 0) { - int b = fs.Read(buffer); + int b = fs.Read(buffer.Slice(bytesRead)); if (b == 0) { break; From d5ffcef7a1fd9a71dffd1916c6c64bae44957bb4 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 16 Jul 2021 16:31:35 -0400 Subject: [PATCH 74/81] Update comments --- .../src/System/TimeZoneInfo.Unix.Android.cs | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs index 3b493968587a23..7a0f489622a883 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs @@ -165,9 +165,22 @@ private static string[] GetTimeZoneIds() /* * Android v4.3 Timezone support infrastructure. * - * This is a C# port of libcore.util.ZoneInfoDB: + * Android tzdata files are found in the format of + * Header Entry Entry Entry ... Entry * - * https://android.googlesource.com/platform/libcore/+/master/luni/src/main/java/libcore/util/ZoneInfoDB.java + * https://github.com/aosp-mirror/platform_bionic/blob/master/libc/tzcode/bionic.cpp + * + * The header (24 bytes) contains the following information + * signature - 12 bytes of the form "tzdata2012f\0" where 2012f is subject to change + * index offset - 4 bytes that denotes the offset at which the index of the tzdata file starts + * data offset - 4 bytes that denotes the offset at which the data of the tzdata file starts + * final offset - 4 bytes that used to denote the final offset, which we don't use but will note. + * + * Each Data Entry (52 bytes) can be used to generate a TimeZoneInfo and contain the following information + * id - 40 bytes that contain the id of the time zone data entry timezone + * byte offset - 4 bytes that denote the offset from the data offset timezone data can be found + * length - 4 bytes that denote the length of the data for timezone + * unused - 4 bytes that used to be raw GMT offset, but now is always 0 since tzdata2014f (L). * * This is needed in order to read Android v4.3 tzdata files. * @@ -177,20 +190,12 @@ private static string[] GetTimeZoneIds() */ private sealed class AndroidTzData { - // public fixed byte signature[12]; // "tzdata2012f\0" - // public int indexOffset; - // public int dataOffset; - // public int finalOffset; private unsafe struct AndroidTzDataHeader { public int indexOffset; public int dataOffset; } - // public string id - 40 bytes - // public int byteOffset; - 4 bytes - // public int length - 4 bytes - // public int unused - 4 bytes Was raw GMT offset; always 0 since tzdata2014f (L). private unsafe struct AndroidTzDataEntry { public fixed byte id[40]; @@ -228,6 +233,9 @@ private static string GetApexRuntimeRoot() public AndroidTzData() { + // On Android, time zone data is found in tzdata + // Based on https://github.com/mono/mono/blob/main/mcs/class/corlib/System/TimeZoneInfo.Android.cs + // Also follows the locations found at the bottom of https://github.com/aosp-mirror/platform_bionic/blob/master/libc/tzcode/bionic.cpp string[] tzFileDirList = new string[] {GetApexTimeDataRoot() + "/etc/tz/", // Android 10+, TimeData module where the updates land GetApexRuntimeRoot() + "/etc/tz/", // Android 10+, Fallback location if the above isn't found or corrupted Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/", @@ -272,7 +280,7 @@ private bool LoadData(string path) [MemberNotNull(nameof(_lengths))] private void LoadTzFile(Stream fs) { - const int HeaderSize = 24; // AndroidTzDataHeader 12 bytes signature, 4 bytes index offset, 4 bytes data offset, 4 bytes final offset + const int HeaderSize = 24; // AndroidTzDataHeader Span buffer = stackalloc byte[HeaderSize]; ReadTzDataIntoBuffer(fs, 0, buffer); @@ -314,7 +322,7 @@ private AndroidTzDataHeader LoadHeader(Span buffer) private unsafe void ReadIndex(Stream fs, int indexOffset, int dataOffset) { int indexSize = dataOffset - indexOffset; - int entrySize = 52; // Size of AndroidTzDataEntry + int entrySize = 52; // AndroidTzDataEntry int entryCount = indexSize / entrySize; _byteOffsets = new int[entryCount]; @@ -330,7 +338,7 @@ private unsafe void ReadIndex(Stream fs, int indexOffset, int dataOffset) _ids![i] = new string(p); _lengths![i] = entry.length; - if (_lengths![i] < 24) // AndroidTzDataHeader 12 bytes signature, 4 bytes index offset, 4 bytes data offset, 4 bytes final offset + if (_lengths![i] < 24) // AndroidTzDataHeader { throw new InvalidOperationException(SR.InvalidOperation_BadIndexLength); } @@ -385,9 +393,6 @@ public string[] GetTimeZoneIds() return _ids!; } - // On Android, time zone data is found in tzdata - // Based on https://github.com/mono/mono/blob/main/mcs/class/corlib/System/TimeZoneInfo.Android.cs - // Also follows the locations found at the bottom of https://github.com/aosp-mirror/platform_bionic/blob/master/libc/tzcode/bionic.cpp public string GetTimeZoneDirectory() { return _tzFilePath; From 311ecaa59a38c29f325755caba24e07cb0c8dbde Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 16 Jul 2021 19:39:07 -0400 Subject: [PATCH 75/81] Remove AndroidTzDataHeader and fix typos --- .../src/System/TimeZoneInfo.Unix.Android.cs | 50 ++++++++----------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs index 7a0f489622a883..e2996b94ef1835 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs @@ -190,12 +190,6 @@ private static string[] GetTimeZoneIds() */ private sealed class AndroidTzData { - private unsafe struct AndroidTzDataHeader - { - public int indexOffset; - public int dataOffset; - } - private unsafe struct AndroidTzDataEntry { public fixed byte id[40]; @@ -263,7 +257,8 @@ private bool LoadData(string path) { return false; } - try { + try + { using (FileStream fs = File.OpenRead(path)) { LoadTzFile(fs); @@ -285,11 +280,13 @@ private void LoadTzFile(Stream fs) ReadTzDataIntoBuffer(fs, 0, buffer); - AndroidTzDataHeader header = LoadHeader(buffer); - ReadIndex(fs, header.indexOffset, header.dataOffset); + int indexOffset; + int dataOffset; + LoadHeader(buffer, out indexOffset, out dataOffset); + ReadIndex(fs, indexOffset, dataOffset); } - private AndroidTzDataHeader LoadHeader(Span buffer) + private void LoadHeader(Span buffer, out int indexOffset, out int dataOffset) { // tzdata files are expected to start with the form of "tzdata2012f\0" depending on the year of the tzdata used which is 2012 in this example // since we're not differentiating on year, check for tzdata and the ending \0 @@ -298,7 +295,7 @@ private AndroidTzDataHeader LoadHeader(Span buffer) if (tz != 0x747A || data != 0x64617461 || buffer[11] != 0) { - // 0x747A 0x646174640000 = {0x74, 0x7A} {0x64, 0x61, 0x74, 0x64} = "tz" "data" + // 0x747A 0x64617461 = {0x74, 0x7A} {0x64, 0x61, 0x74, 0x61} = "tz" "data" var b = new StringBuilder(buffer.Length); for (int i = 0; i < 12; ++i) { @@ -308,12 +305,8 @@ private AndroidTzDataHeader LoadHeader(Span buffer) throw new InvalidOperationException(SR.Format(SR.InvalidOperation_BadTZHeader, TimeZoneFileName, b.ToString())); } - AndroidTzDataHeader header; - - header.indexOffset = TZif_ToInt32(buffer.Slice(12, 4)); - header.dataOffset = TZif_ToInt32(buffer.Slice(16, 4)); - - return header; + indexOffset = TZif_ToInt32(buffer.Slice(12, 4)); + dataOffset = TZif_ToInt32(buffer.Slice(16, 4)); } [MemberNotNull(nameof(_ids))] @@ -322,7 +315,7 @@ private AndroidTzDataHeader LoadHeader(Span buffer) private unsafe void ReadIndex(Stream fs, int indexOffset, int dataOffset) { int indexSize = dataOffset - indexOffset; - int entrySize = 52; // AndroidTzDataEntry + const int entrySize = 52; // AndroidTzDataEntry int entryCount = indexSize / entrySize; _byteOffsets = new int[entryCount]; @@ -334,11 +327,11 @@ private unsafe void ReadIndex(Stream fs, int indexOffset, int dataOffset) AndroidTzDataEntry entry = LoadEntryAt(fs, indexOffset + (entrySize*i)); var p = (sbyte*)entry.id; - _byteOffsets![i] = entry.byteOffset + dataOffset; - _ids![i] = new string(p); - _lengths![i] = entry.length; + _byteOffsets[i] = entry.byteOffset + dataOffset; + _ids[i] = new string(p); + _lengths[i] = entry.length; - if (_lengths![i] < 24) // AndroidTzDataHeader + if (_lengths[i] < 24) // AndroidTzDataHeader { throw new InvalidOperationException(SR.InvalidOperation_BadIndexLength); } @@ -390,7 +383,7 @@ private unsafe AndroidTzDataEntry LoadEntryAt(Stream fs, long position) public string[] GetTimeZoneIds() { - return _ids!; + return _ids; } public string GetTimeZoneDirectory() @@ -400,20 +393,19 @@ public string GetTimeZoneDirectory() public byte[] GetTimeZoneData(string id) { - int i = Array.BinarySearch(_ids!, id, StringComparer.Ordinal); + int i = Array.BinarySearch(_ids, id, StringComparer.Ordinal); if (i < 0) { throw new InvalidOperationException(SR.Format(SR.TimeZoneNotFound_MissingData, id)); } - int offset = _byteOffsets![i]; - int length = _lengths![i]; + int offset = _byteOffsets[i]; + int length = _lengths[i]; - Span buffer = stackalloc byte[length]; + byte[] buffer = new byte[length]; ReadTzDataIntoBuffer(File.OpenRead(_tzFilePath), offset, buffer); - byte[] tzBuffer = buffer.ToArray(); - return tzBuffer; + return buffer; } } } From a07da9623051be6239a912701cc3514def537179 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Mon, 19 Jul 2021 09:50:09 -0400 Subject: [PATCH 76/81] Add const keyword, move field declaration, use length comparison --- .../src/System/TimeZoneInfo.Unix.Android.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs index e2996b94ef1835..b2f685482e3407 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs @@ -280,9 +280,7 @@ private void LoadTzFile(Stream fs) ReadTzDataIntoBuffer(fs, 0, buffer); - int indexOffset; - int dataOffset; - LoadHeader(buffer, out indexOffset, out dataOffset); + LoadHeader(buffer, out int indexOffset, out int dataOffset); ReadIndex(fs, indexOffset, dataOffset); } @@ -331,7 +329,7 @@ private unsafe void ReadIndex(Stream fs, int indexOffset, int dataOffset) _ids[i] = new string(p); _lengths[i] = entry.length; - if (_lengths[i] < 24) // AndroidTzDataHeader + if (entry.length < 24) // AndroidTzDataHeader { throw new InvalidOperationException(SR.InvalidOperation_BadIndexLength); } @@ -365,7 +363,7 @@ private void ReadTzDataIntoBuffer(Stream fs, long position, Span buffer) private unsafe AndroidTzDataEntry LoadEntryAt(Stream fs, long position) { - int size = 52; // AndroidTzDataEntry + const int size = 52; // AndroidTzDataEntry Span entryBuffer = stackalloc byte[size]; ReadTzDataIntoBuffer(fs, position, entryBuffer); From a47ade25ef860a1ed559d9a1210d57fa3670576e Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Mon, 19 Jul 2021 10:25:26 -0400 Subject: [PATCH 77/81] Change data entry id to string --- .../src/System/TimeZoneInfo.Unix.Android.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs index b2f685482e3407..98161e22b3e11d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs @@ -192,7 +192,7 @@ private sealed class AndroidTzData { private unsafe struct AndroidTzDataEntry { - public fixed byte id[40]; + public string id; public int byteOffset; public int length; } @@ -323,10 +323,9 @@ private unsafe void ReadIndex(Stream fs, int indexOffset, int dataOffset) for (int i = 0; i < entryCount; ++i) { AndroidTzDataEntry entry = LoadEntryAt(fs, indexOffset + (entrySize*i)); - var p = (sbyte*)entry.id; _byteOffsets[i] = entry.byteOffset + dataOffset; - _ids[i] = new string(p); + _ids[i] = entry.id; _lengths[i] = entry.length; if (entry.length < 24) // AndroidTzDataHeader @@ -369,10 +368,7 @@ private unsafe AndroidTzDataEntry LoadEntryAt(Stream fs, long position) ReadTzDataIntoBuffer(fs, position, entryBuffer); AndroidTzDataEntry entry; - for (int i = 0; i < 40; i++) - { - entry.id[i] = entryBuffer[i]; - } + entry.id = Encoding.UTF8.GetString(entryBuffer.Slice(0, 40)).Split('\0')[0]; entry.byteOffset = TZif_ToInt32(entryBuffer.Slice(40, 4)); entry.length = TZif_ToInt32(entryBuffer.Slice(44, 4)); From cc06d9665d869a3e78432707afcb50da940f37ee Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Mon, 19 Jul 2021 10:49:39 -0400 Subject: [PATCH 78/81] Parse id account for null terminating character --- .../src/System/TimeZoneInfo.Unix.Android.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs index 98161e22b3e11d..a74bb527ad48bb 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs @@ -368,7 +368,12 @@ private unsafe AndroidTzDataEntry LoadEntryAt(Stream fs, long position) ReadTzDataIntoBuffer(fs, position, entryBuffer); AndroidTzDataEntry entry; - entry.id = Encoding.UTF8.GetString(entryBuffer.Slice(0, 40)).Split('\0')[0]; + int index = 0; + while (entryBuffer[index] != 0 && index < 40) + { + index += 1; + } + entry.id = Encoding.UTF8.GetString(entryBuffer.Slice(0, index)); entry.byteOffset = TZif_ToInt32(entryBuffer.Slice(40, 4)); entry.length = TZif_ToInt32(entryBuffer.Slice(44, 4)); From 00b0abe9bfed26fea7dba42b825fa0e010edac7b Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Mon, 19 Jul 2021 11:17:22 -0400 Subject: [PATCH 79/81] Remove AndroidTzDataEntry struct and use out parameters --- .../src/System/TimeZoneInfo.Unix.Android.cs | 34 +++++++------------ 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs index a74bb527ad48bb..11d8df6db4c936 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs @@ -190,13 +190,6 @@ private static string[] GetTimeZoneIds() */ private sealed class AndroidTzData { - private unsafe struct AndroidTzDataEntry - { - public string id; - public int byteOffset; - public int length; - } - private string[] _ids; private int[] _byteOffsets; private int[] _lengths; @@ -275,7 +268,7 @@ private bool LoadData(string path) [MemberNotNull(nameof(_lengths))] private void LoadTzFile(Stream fs) { - const int HeaderSize = 24; // AndroidTzDataHeader + const int HeaderSize = 24; Span buffer = stackalloc byte[HeaderSize]; ReadTzDataIntoBuffer(fs, 0, buffer); @@ -313,7 +306,7 @@ private void LoadHeader(Span buffer, out int indexOffset, out int dataOffs private unsafe void ReadIndex(Stream fs, int indexOffset, int dataOffset) { int indexSize = dataOffset - indexOffset; - const int entrySize = 52; // AndroidTzDataEntry + const int entrySize = 52; // Data entry size int entryCount = indexSize / entrySize; _byteOffsets = new int[entryCount]; @@ -322,13 +315,13 @@ private unsafe void ReadIndex(Stream fs, int indexOffset, int dataOffset) for (int i = 0; i < entryCount; ++i) { - AndroidTzDataEntry entry = LoadEntryAt(fs, indexOffset + (entrySize*i)); + LoadEntryAt(fs, indexOffset + (entrySize*i), out string id, out int byteOffset, out int length); - _byteOffsets[i] = entry.byteOffset + dataOffset; - _ids[i] = entry.id; - _lengths[i] = entry.length; + _byteOffsets[i] = byteOffset + dataOffset; + _ids[i] = id; + _lengths[i] = length; - if (entry.length < 24) // AndroidTzDataHeader + if (length < 24) // Header Size { throw new InvalidOperationException(SR.InvalidOperation_BadIndexLength); } @@ -360,24 +353,21 @@ private void ReadTzDataIntoBuffer(Stream fs, long position, Span buffer) } } - private unsafe AndroidTzDataEntry LoadEntryAt(Stream fs, long position) + private unsafe void LoadEntryAt(Stream fs, long position, out string id, out int byteOffset, out int length) { - const int size = 52; // AndroidTzDataEntry + const int size = 52; // data entry size Span entryBuffer = stackalloc byte[size]; ReadTzDataIntoBuffer(fs, position, entryBuffer); - AndroidTzDataEntry entry; int index = 0; while (entryBuffer[index] != 0 && index < 40) { index += 1; } - entry.id = Encoding.UTF8.GetString(entryBuffer.Slice(0, index)); - entry.byteOffset = TZif_ToInt32(entryBuffer.Slice(40, 4)); - entry.length = TZif_ToInt32(entryBuffer.Slice(44, 4)); - - return entry; + id = Encoding.UTF8.GetString(entryBuffer.Slice(0, index)); + byteOffset = TZif_ToInt32(entryBuffer.Slice(40, 4)); + length = TZif_ToInt32(entryBuffer.Slice(44, 4)); } public string[] GetTimeZoneIds() From 5290490dfd7f4c752909b425daff241d31ac0e0d Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Mon, 19 Jul 2021 11:47:57 -0400 Subject: [PATCH 80/81] Remove unsafe keywords --- .../src/System/TimeZoneInfo.Unix.Android.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs index 11d8df6db4c936..7ec2dc3e255564 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs @@ -303,7 +303,7 @@ private void LoadHeader(Span buffer, out int indexOffset, out int dataOffs [MemberNotNull(nameof(_ids))] [MemberNotNull(nameof(_byteOffsets))] [MemberNotNull(nameof(_lengths))] - private unsafe void ReadIndex(Stream fs, int indexOffset, int dataOffset) + private void ReadIndex(Stream fs, int indexOffset, int dataOffset) { int indexSize = dataOffset - indexOffset; const int entrySize = 52; // Data entry size @@ -353,7 +353,7 @@ private void ReadTzDataIntoBuffer(Stream fs, long position, Span buffer) } } - private unsafe void LoadEntryAt(Stream fs, long position, out string id, out int byteOffset, out int length) + private void LoadEntryAt(Stream fs, long position, out string id, out int byteOffset, out int length) { const int size = 52; // data entry size Span entryBuffer = stackalloc byte[size]; From 95f30f6052e8dd69de155382d5d165ed0a192dac Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Mon, 19 Jul 2021 14:38:34 -0400 Subject: [PATCH 81/81] Close file handle in GetTimeZoneData --- .../src/System/TimeZoneInfo.Unix.Android.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs index 7ec2dc3e255564..53baf4eb65111f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs @@ -390,9 +390,12 @@ public byte[] GetTimeZoneData(string id) int offset = _byteOffsets[i]; int length = _lengths[i]; - byte[] buffer = new byte[length]; - ReadTzDataIntoBuffer(File.OpenRead(_tzFilePath), offset, buffer); + + using (FileStream fs = File.OpenRead(_tzFilePath)) + { + ReadTzDataIntoBuffer(fs, offset, buffer); + } return buffer; }