[Android][libraries] TimeZoneInfo Android imp (#54845)
authorMitchell Hwang <16830051+mdh1418@users.noreply.github.com>
Wed, 21 Jul 2021 16:39:13 +0000 (12:39 -0400)
committerGitHub <noreply@github.com>
Wed, 21 Jul 2021 16:39:13 +0000 (12:39 -0400)
Fixes #41867

Android has removed zone.tab, so TimeZoneInfo.Unix.cs will no longer work properly on Android as GetTimeZoneIds directly depends on the zone.tab file. The chain of dependency is as follows

GetSystemTimeZones -> PopulateAllSystemTimeZones -> GetTimeZoneIds -> zone.tab
                TZI.cs                                  TZI.Unix.cs                        TZI.Unix.cs         TZI.Unix.cs
 Where TZI is TimeZoneInfo

zone.tab is a file that is found on the unix system under /usr/share/zoneinfo/
GetTimeZoneIds reads zone.tab to obtain the TimeZoneId in that file
PopulateAllSystemTimeZones caches all the TimeZone Ids in cachedData
GetSystemTimeZones returns a ReadOnlyCollection containing all valid TimeZone’s from the local machine, and the entries are sorted by their DisplayName. It relies on cachedData._systemTimeZones being populated.

The problem is that the time zone data for Android can be found in the file tzdata at the possible locations

/apex/com.android.tzdata/etc/tz/tzdata
/apex/com.android.runtime/etc/tz/tzdata
/data/misc/zoneinfo/tzdata
/system/usr/share/zoneinfo/tzdata

The rest of unix the time zone data can be found in the file zone.tab at

 /usr/share/zoneinfo/zone.tab
Android's TimeZoneInfo implementation should read time zone data from its locations instead of the general /usr/share/zoneinfo/zone.tab path. Moreover, tzdata contains all timezones byte data.

This PR achieves the following:

1. Splits TimeZoneInfo.Unix.cs into TimeZoneInfo.Unix.cs, TimeZoneInfo.Unix.NonAndroid.cs (non-Android), and
    TimeZoneInfo.Unix.Android.cs (Android specific)
2. Adds an interop to obtain the default time zone on Android based on persist.sys.timezone
3. Implements GetLocalTimeZoneCore TryGetTimeZoneFromLocalMachineCore and GetTimeZoneIds for Android
    based on mono/mono implementation
    https://github.com/mono/mono/blob/main/mcs/class/corlib/System/TimeZoneInfo.Android.cs
4. Adds new string resources to throw exceptions
5. Refactors the mono/mono implementation of parsing tzdata

Android tzdata files are found in the format of
Header <Beginning of Entry Index> Entry Entry Entry ... Entry <Beginning of Data Index> <TZDATA>

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<id>
byte offset - 4 bytes that denote the offset from the data offset timezone<id> data can be found
length - 4 bytes that denote the length of the data for timezone<id>
unused - 4 bytes that used to be raw GMT offset, but now is always 0 since tzdata2014f (L).

When GetLocalTimeZoneCore TryGetTimeZoneFromLocalMachineCore or GetTimeZoneIds are called, an android timezone data instance is instantiated and loaded by attempting to load a tzdata file that can be found at four locations mentioned earlier. The file is parsed by first loading the header which contains information about where the data index and data begin. The data index is then parsed to obtain the timezone and the corresponding bytes location in the file to fill the three arrays _ids _byteOffsets _lengths. These arrays are referenced to obtain the corresponding byte data for a timezone, and functions from TimeZoneInfo.Unix.cs are leveraged to create a TimeZoneInfo from there.

src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetDefaultTimeZone.Android.cs [new file with mode: 0644]
src/libraries/Native/Unix/System.Native/pal_datetime.c
src/libraries/Native/Unix/System.Native/pal_datetime.h
src/libraries/System.Private.CoreLib/src/Resources/Strings.resx
src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs [new file with mode: 0644]
src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.NonAndroid.cs [new file with mode: 0644]
src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.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 (file)
index 0000000..b9a9cd5
--- /dev/null
@@ -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", CharSet = CharSet.Ansi, SetLastError = true)]
+        internal static extern string? GetDefaultTimeZone();
+    }
+}
index 30f93ad05a1d3c3dc4fa97cc6ef6e4e04a1ef846..3832114b4eb2e65b33276c53a46832f74cb44763 100644 (file)
@@ -2,13 +2,15 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 #include "pal_config.h"
-
-#include <stdlib.h>
+#include "pal_datetime.h"
 #include <stdint.h>
-#include <time.h>
+#include <stdlib.h>
+#include <string.h>
 #include <sys/time.h>
-
-#include "pal_datetime.h"
+#if defined(TARGET_ANDROID)
+#include <sys/system_properties.h>
+#endif
+#include <time.h>
 
 static const int64_t TICKS_PER_SECOND = 10000000; /* 10^7 */
 #if HAVE_CLOCK_REALTIME
@@ -39,3 +41,18 @@ int64_t SystemNative_GetSystemTimeAsTicks()
     // in failure we return 00:00 01 January 1970 UTC (Unix epoch)
     return 0;
 }
+
+#if defined(TARGET_ANDROID)
+char* SystemNative_GetDefaultTimeZone()
+{
+    char defaulttimezone[PROP_VALUE_MAX];
+    if (__system_property_get("persist.sys.timezone", defaulttimezone))
+    {
+        return strdup(defaulttimezone);
+    }
+    else
+    {
+        return NULL;
+    }
+}
+#endif
index 564a69a4857ebc72f97a365b2ba5f5c37fbb534f..1b88d472780fd1214e6ef438587cf7bc2b30a521 100644 (file)
@@ -4,5 +4,10 @@
 #pragma once
 
 #include "pal_compiler.h"
+#include "pal_types.h"
 
 PALEXPORT int64_t SystemNative_GetSystemTimeAsTicks(void);
+
+#if defined(TARGET_ANDROID)
+PALEXPORT char* SystemNative_GetDefaultTimeZone(void);
+#endif
index a9910865f00655e42774355a61401d4f359c2b3a..82195aeb457ecce5b5dadbb39c1e4dfa6dbd3e2c 100644 (file)
   <data name="Arg_MemberInfoNotFound" xml:space="preserve">
     <value>A MemberInfo that matches '{0}' could not be found.</value>
   </data>
+  <data name="InvalidOperation_BadTZHeader" xml:space="preserve">
+    <value>Bad magic in '{0}': Header starts with '{1}' instead of 'tzdata'</value>
+  </data>
+  <data name="InvalidOperation_ReadTZError" xml:space="preserve">
+    <value>Unable to fully read from file '{0}' at offset {1} length {2}; read {3} bytes expected {4}.</value>
+  </data>
+  <data name="InvalidOperation_BadIndexLength" xml:space="preserve">
+    <value>Length in index file less than AndroidTzDataHeader</value>
+  </data>
+  <data name="TimeZoneNotFound_ValidTimeZoneFileMissing" xml:space="preserve">
+    <value>Unable to properly load any time zone data files.</value>
+  </data>
 </root>
index b3c6bd1b6e7f26e797e1333efcbf33bdaa921cb5..2c6783465d5ae726b1a0f1c8571aeb96aff477fa 100644 (file)
     <Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.GetCwd.cs">
       <Link>Common\Interop\Unix\System.Native\Interop.GetCwd.cs</Link>
     </Compile>
+    <Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.GetDefaultTimeZone.Android.cs" Condition="'$(TargetsAndroid)' == 'true'">
+      <Link>Common\Interop\Unix\System.Native\Interop.GetDefaultTimeZone.Android.cs</Link>
+    </Compile>
     <Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.GetEGid.cs">
       <Link>Common\Interop\Unix\System.Native\Interop.GetEGid.cs</Link>
     </Compile>
     <Compile Include="$(MSBuildThisFileDirectory)System\Threading\LowLevelMonitor.Unix.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Threading\TimerQueue.Unix.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\TimeZoneInfo.Unix.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)System\TimeZoneInfo.Unix.Android.cs" Condition="'$(TargetsAndroid)' == 'true'" />
+    <Compile Include="$(MSBuildThisFileDirectory)System\TimeZoneInfo.Unix.NonAndroid.cs" Condition="'$(TargetsAndroid)' != 'true'" />
   </ItemGroup>
   <ItemGroup Condition="'$(TargetsUnix)' == 'true'">
     <Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.GetEUid.cs">
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
new file mode 100644 (file)
index 0000000..53baf4e
--- /dev/null
@@ -0,0 +1,404 @@
+// 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.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+
+namespace System
+{
+    public sealed partial class TimeZoneInfo
+    {
+        private const string TimeZoneFileName = "tzdata";
+
+        private static AndroidTzData? s_tzData;
+
+        private static AndroidTzData AndroidTzDataInstance
+        {
+            get
+            {
+                if (s_tzData == null)
+                {
+                    Interlocked.CompareExchange(ref s_tzData, new AndroidTzData(), null);
+                }
+
+                return s_tzData;
+            }
+        }
+
+        // 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] == '-')
+            {
+                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;
+            }
+        }
+
+        private 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", StringComparison.Ordinal))
+            {
+                return new TimeZoneInfo(id, TimeSpan.FromSeconds(ParseGMTNumericZone(name)), id, name, name, null, disableDaylightSavingTime:true);
+            }
+
+            try
+            {
+                byte[] buffer = AndroidTzDataInstance.GetTimeZoneData(name);
+                return GetTimeZoneFromTzData(buffer, id);
+            }
+            catch
+            {
+                return null;
+            }
+        }
+
+        // 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();
+            if (!string.IsNullOrEmpty(id))
+            {
+                TimeZoneInfo? defaultTimeZone = GetTimeZone(id, id);
+
+                if (defaultTimeZone != null)
+                {
+                    return defaultTimeZone;
+                }
+            }
+
+            return Utc;
+        }
+
+        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, AndroidTzDataInstance.GetTimeZoneDirectory() + TimeZoneFileName));
+                return TimeZoneInfoResult.TimeZoneNotFoundException;
+            }
+
+            e = null;
+            return TimeZoneInfoResult.Success;
+        }
+
+        private static string[] GetTimeZoneIds()
+        {
+            return AndroidTzDataInstance.GetTimeZoneIds();
+        }
+
+        /*
+        * Android v4.3 Timezone support infrastructure.
+        *
+        * Android tzdata files are found in the format of
+        * Header <Beginning of Entry Index> Entry Entry Entry ... Entry <Beginning of Data Index> <TZDATA>
+        *
+        * 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<id>
+        * byte offset - 4 bytes that denote the offset from the data offset timezone<id> data can be found
+        * length - 4 bytes that denote the length of the data for timezone<id>
+        * 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.
+        *
+        * 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
+        {
+            private string[] _ids;
+            private int[] _byteOffsets;
+            private int[] _lengths;
+            private string _tzFileDir;
+            private string _tzFilePath;
+
+            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";
+            }
+
+            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/",
+                                                       Environment.GetEnvironmentVariable("ANDROID_ROOT") + DefaultTimeZoneDirectory};
+                foreach (var tzFileDir in tzFileDirList)
+                {
+                    string tzFilePath = Path.Combine(tzFileDir, TimeZoneFileName);
+                    if (LoadData(tzFilePath))
+                    {
+                        _tzFileDir = tzFileDir;
+                        _tzFilePath = tzFilePath;
+                        return;
+                    }
+                }
+
+                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))
+                {
+                    return false;
+                }
+                try
+                {
+                    using (FileStream fs = File.OpenRead(path))
+                    {
+                        LoadTzFile(fs);
+                    }
+                    return true;
+                }
+                catch {}
+
+                return false;
+            }
+
+            [MemberNotNull(nameof(_ids))]
+            [MemberNotNull(nameof(_byteOffsets))]
+            [MemberNotNull(nameof(_lengths))]
+            private void LoadTzFile(Stream fs)
+            {
+                const int HeaderSize = 24;
+                Span<byte> buffer = stackalloc byte[HeaderSize];
+
+                ReadTzDataIntoBuffer(fs, 0, buffer);
+
+                LoadHeader(buffer, out int indexOffset, out int dataOffset);
+                ReadIndex(fs, indexOffset, dataOffset);
+            }
+
+            private void LoadHeader(Span<byte> 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
+                var tz = (ushort)TZif_ToInt16(buffer.Slice(0, 2));
+                var data = (uint)TZif_ToInt32(buffer.Slice(2, 4));
+
+                if (tz != 0x747A || data != 0x64617461 || buffer[11] != 0)
+                {
+                    // 0x747A  0x64617461 = {0x74, 0x7A} {0x64, 0x61, 0x74, 0x61} = "tz" "data"
+                    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, TimeZoneFileName, b.ToString()));
+                }
+
+                indexOffset = TZif_ToInt32(buffer.Slice(12, 4));
+                dataOffset = TZif_ToInt32(buffer.Slice(16, 4));
+            }
+
+            [MemberNotNull(nameof(_ids))]
+            [MemberNotNull(nameof(_byteOffsets))]
+            [MemberNotNull(nameof(_lengths))]
+            private void ReadIndex(Stream fs, int indexOffset, int dataOffset)
+            {
+                int indexSize = dataOffset - indexOffset;
+                const int entrySize = 52; // Data entry size
+                int entryCount = indexSize / entrySize;
+
+                _byteOffsets = new int[entryCount];
+                _ids = new string[entryCount];
+                _lengths = new int[entryCount];
+
+                for (int i = 0; i < entryCount; ++i)
+                {
+                    LoadEntryAt(fs, indexOffset + (entrySize*i), out string id, out int byteOffset, out int length);
+
+                    _byteOffsets[i] = byteOffset + dataOffset;
+                    _ids[i] = id;
+                    _lengths[i] = length;
+
+                    if (length < 24) // Header Size
+                    {
+                        throw new InvalidOperationException(SR.InvalidOperation_BadIndexLength);
+                    }
+                }
+            }
+
+            private void ReadTzDataIntoBuffer(Stream fs, long position, Span<byte> buffer)
+            {
+                fs.Position = position;
+
+                int bytesRead = 0;
+                int bytesLeft = buffer.Length;
+
+                while (bytesLeft > 0)
+                {
+                    int b = fs.Read(buffer.Slice(bytesRead));
+                    if (b == 0)
+                    {
+                        break;
+                    }
+
+                    bytesRead += b;
+                    bytesLeft -= b;
+                }
+
+                if (bytesLeft != 0)
+                {
+                    throw new InvalidOperationException(SR.Format(SR.InvalidOperation_ReadTZError, _tzFilePath, position, buffer.Length, bytesRead, buffer.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<byte> entryBuffer = stackalloc byte[size];
+
+                ReadTzDataIntoBuffer(fs, position, entryBuffer);
+
+                int index = 0;
+                while (entryBuffer[index] != 0 && index < 40)
+                {
+                    index += 1;
+                }
+                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()
+            {
+                return _ids;
+            }
+
+            public string GetTimeZoneDirectory()
+            {
+                return _tzFilePath;
+            }
+
+            public byte[] GetTimeZoneData(string id)
+            {
+                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];
+                byte[] buffer = new byte[length];
+
+                using (FileStream fs = File.OpenRead(_tzFilePath))
+                {
+                    ReadTzDataIntoBuffer(fs, offset, buffer);
+                }
+
+                return buffer;
+            }
+        }
+    }
+}
\ No newline at end of file
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 (file)
index 0000000..c5ad4b0
--- /dev/null
@@ -0,0 +1,466 @@
+// 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;
+using Microsoft.Win32.SafeHandles;
+
+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;
+        }
+
+        /// <summary>
+        /// Returns a collection of TimeZone Id values from the time zone file in the timeZoneDirectory.
+        /// </summary>
+        /// <remarks>
+        /// Lines that start with # are comments and are skipped.
+        /// </remarks>
+        private static List<string> GetTimeZoneIds()
+        {
+            List<string> timeZoneIds = new List<string>();
+
+            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;
+        }
+
+        /// <summary>
+        /// Finds the time zone id by using 'readlink' on the path to see if tzFilePath is
+        /// a symlink to a file.
+        /// </summary>
+        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<char> 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);
+        }
+
+        /// <summary>
+        /// Enumerate files
+        /// </summary>
+        private static unsafe void EnumerateFilesRecursively(string path, Predicate<string> condition)
+        {
+            List<string>? toExplore = null; // List used as a stack
+
+            int bufferSize = Interop.Sys.GetReadDirRBufferSize();
+            byte[]? dirBuffer = null;
+            try
+            {
+                dirBuffer = ArrayPool<byte>.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<string>();
+                                    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<byte>.Shared.Return(dirBuffer);
+            }
+        }
+
+        private static bool CompareTimeZoneFile(string filePath, byte[] buffer, byte[] rawData)
+        {
+            try
+            {
+                // bufferSize == 1 used to avoid unnecessary buffer in FileStream
+                using (SafeFileHandle sfh = File.OpenHandle(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
+                {
+                    long fileLength = RandomAccess.GetLength(sfh);
+                    if (fileLength == rawData.Length)
+                    {
+                        int index = 0;
+                        int count = rawData.Length;
+
+                        while (count > 0)
+                        {
+                            int n = RandomAccess.Read(sfh, buffer.AsSpan(index, count), index);
+                            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;
+        }
+
+        /// <summary>
+        /// Find the time zone id by searching all the tzfiles for the one that matches rawData
+        /// and return its file name.
+        /// </summary>
+        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;
+        }
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        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);
+        }
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        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;
+        }
+    }
+}
index 4756a93271593718580afc5ae3d52aa45c04674b..622195782cb5054fb69ac723fd7defd41e539e9e 100644 (file)
@@ -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;
@@ -18,9 +17,6 @@ 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.
@@ -231,8 +227,7 @@ namespace System
         {
             Debug.Assert(Monitor.IsEntered(cachedData));
 
-            string timeZoneDirectory = GetTimeZoneDirectory();
-            foreach (string timeZoneId in GetTimeZoneIds(timeZoneDirectory))
+            foreach (string timeZoneId in GetTimeZoneIds())
             {
                 TryGetTimeZone(timeZoneId, false, out _, out _, cachedData, alwaysFallbackToLocalMachine: true);  // populate the cache
             }
@@ -248,432 +243,12 @@ namespace System
         {
             Debug.Assert(Monitor.IsEntered(cachedData));
 
-            // Without Registry support, create the TimeZoneInfo from a TZ file
-            return GetLocalTimeZoneFromTzFile();
+            return GetLocalTimeZoneCore();
         }
 
         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;
-        }
-
-        /// <summary>
-        /// Returns a collection of TimeZone Id values from the zone.tab file in the timeZoneDirectory.
-        /// </summary>
-        /// <remarks>
-        /// Lines that start with # are comments and are skipped.
-        /// </remarks>
-        private static List<string> GetTimeZoneIds(string timeZoneDirectory)
-        {
-            List<string> timeZoneIds = new List<string>();
-
-            try
-            {
-                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);
-                                    }
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-            catch (IOException) { }
-            catch (UnauthorizedAccessException) { }
-
-            return timeZoneIds;
-        }
-
-        /// <summary>
-        /// 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.
-        /// </summary>
-        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;
-        }
-
-        /// <summary>
-        /// Finds the time zone id by using 'readlink' on the path to see if tzFilePath is
-        /// a symlink to a file.
-        /// </summary>
-        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<char> 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);
-        }
-
-        /// <summary>
-        /// Enumerate files
-        /// </summary>
-        private static unsafe void EnumerateFilesRecursively(string path, Predicate<string> condition)
-        {
-            List<string>? toExplore = null; // List used as a stack
-
-            int bufferSize = Interop.Sys.GetReadDirRBufferSize();
-            byte[]? dirBuffer = null;
-            try
-            {
-                dirBuffer = ArrayPool<byte>.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<string>();
-                                    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<byte>.Shared.Return(dirBuffer);
-            }
-        }
-
-        /// <summary>
-        /// Find the time zone id by searching all the tzfiles for the one that matches rawData
-        /// and return its file name.
-        /// </summary>
-        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 (SafeFileHandle sfh = File.OpenHandle(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
-                {
-                    long fileLength = RandomAccess.GetLength(sfh);
-                    if (fileLength == rawData.Length)
-                    {
-                        int index = 0;
-                        int count = rawData.Length;
-
-                        while (count > 0)
-                        {
-                            int n = RandomAccess.Read(sfh, buffer.AsSpan(index, count), index);
-                            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;
-        }
-
-        /// <summary>
-        /// 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.
-        /// </summary>
-        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;
+            return TryGetTimeZoneFromLocalMachineCore(id, out value, out e);
         }
 
         private static TimeZoneInfo? GetTimeZoneFromTzData(byte[]? rawData, string id)
@@ -697,21 +272,6 @@ namespace System
             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;
-        }
 
         /// <summary>
         /// Helper function for retrieving a TimeZoneInfo object by time_zone_name.
@@ -1531,16 +1091,27 @@ namespace System
                 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<byte> 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)
             => 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<byte> 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));
 
+
         private static long TZif_ToUnixTime(byte[] value, int startIndex, TZVersion version) =>
             version != TZVersion.V1 ?
                 TZif_ToInt64(value, startIndex) :