--- /dev/null
+// 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();
+ }
+}
// 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
// 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
#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
<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>
<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">
--- /dev/null
+// 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
--- /dev/null
+// 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;
+ }
+ }
+}
// 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;
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.
{
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
}
{
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)
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.
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) :