diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetDefaultTimeZone.Android.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetDefaultTimeZone.Android.cs new file mode 100644 index 00000000000000..b9a9cd50409897 --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetDefaultTimeZone.Android.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Sys + { + [DllImport(Interop.Libraries.SystemNative, EntryPoint = "SystemNative_GetDefaultTimeZone", CharSet = CharSet.Ansi, SetLastError = true)] + internal static extern string? GetDefaultTimeZone(); + } +} diff --git a/src/libraries/Native/Unix/System.Native/pal_datetime.c b/src/libraries/Native/Unix/System.Native/pal_datetime.c index 30f93ad05a1d3c..3832114b4eb2e6 100644 --- a/src/libraries/Native/Unix/System.Native/pal_datetime.c +++ b/src/libraries/Native/Unix/System.Native/pal_datetime.c @@ -2,13 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. #include "pal_config.h" - -#include +#include "pal_datetime.h" #include -#include +#include +#include #include - -#include "pal_datetime.h" +#if defined(TARGET_ANDROID) +#include +#endif +#include 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 diff --git a/src/libraries/Native/Unix/System.Native/pal_datetime.h b/src/libraries/Native/Unix/System.Native/pal_datetime.h index 564a69a4857ebc..1b88d472780fd1 100644 --- a/src/libraries/Native/Unix/System.Native/pal_datetime.h +++ b/src/libraries/Native/Unix/System.Native/pal_datetime.h @@ -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 diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index 23a96351e049c2..85df61e89b9f98 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -3796,4 +3796,16 @@ A MemberInfo that matches '{0}' could not be found. + + Bad magic in '{0}': Header starts with '{1}' instead of 'tzdata' + + + Unable to fully read from file '{0}' at offset {1} length {2}; read {3} bytes expected {4}. + + + Length in index file less than AndroidTzDataHeader + + + Unable to properly load any time zone data files. + diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 3dcfc2c7313a57..b31752869f1b8f 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -1953,6 +1953,9 @@ Common\Interop\Unix\System.Native\Interop.GetCwd.cs + + Common\Interop\Unix\System.Native\Interop.GetDefaultTimeZone.Android.cs + Common\Interop\Unix\System.Native\Interop.GetHostName.cs @@ -2126,6 +2129,8 @@ + + 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 index 00000000000000..53baf4eb65111f --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs @@ -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 Entry Entry Entry ... Entry + * + * https://github.com/aosp-mirror/platform_bionic/blob/master/libc/tzcode/bionic.cpp + * + * The header (24 bytes) contains the following information + * signature - 12 bytes of the form "tzdata2012f\0" where 2012f is subject to change + * index offset - 4 bytes that denotes the offset at which the index of the tzdata file starts + * data offset - 4 bytes that denotes the offset at which the data of the tzdata file starts + * final offset - 4 bytes that used to denote the final offset, which we don't use but will note. + * + * Each Data Entry (52 bytes) can be used to generate a TimeZoneInfo and contain the following information + * id - 40 bytes that contain the id of the time zone data entry timezone + * byte offset - 4 bytes that denote the offset from the data offset timezone data can be found + * length - 4 bytes that denote the length of the data for timezone + * unused - 4 bytes that used to be raw GMT offset, but now is always 0 since tzdata2014f (L). + * + * This is needed in order to read Android v4.3 tzdata files. + * + * 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 buffer = stackalloc byte[HeaderSize]; + + ReadTzDataIntoBuffer(fs, 0, buffer); + + LoadHeader(buffer, out int indexOffset, out int dataOffset); + ReadIndex(fs, indexOffset, dataOffset); + } + + private void LoadHeader(Span buffer, out int indexOffset, out int dataOffset) + { + // tzdata files are expected to start with the form of "tzdata2012f\0" depending on the year of the tzdata used which is 2012 in this example + // since we're not differentiating on year, check for tzdata and the ending \0 + 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 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 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 index 00000000000000..c5ad4b0f42660a --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.NonAndroid.cs @@ -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; + } + + /// + /// Returns a collection of TimeZone Id values from the time zone file in the timeZoneDirectory. + /// + /// + /// Lines that start with # are comments and are skipped. + /// + private static List GetTimeZoneIds() + { + List timeZoneIds = new List(); + + try + { + using (StreamReader sr = new StreamReader(Path.Combine(GetTimeZoneDirectory(), TimeZoneFileName), Encoding.UTF8)) + { + string? zoneTabFileLine; + while ((zoneTabFileLine = sr.ReadLine()) != null) + { + if (!string.IsNullOrEmpty(zoneTabFileLine) && zoneTabFileLine[0] != '#') + { + // the format of the line is "country-code \t coordinates \t TimeZone Id \t comments" + + int firstTabIndex = zoneTabFileLine.IndexOf('\t'); + if (firstTabIndex != -1) + { + int secondTabIndex = zoneTabFileLine.IndexOf('\t', firstTabIndex + 1); + if (secondTabIndex != -1) + { + string timeZoneId; + int startIndex = secondTabIndex + 1; + int thirdTabIndex = zoneTabFileLine.IndexOf('\t', startIndex); + if (thirdTabIndex != -1) + { + int length = thirdTabIndex - startIndex; + timeZoneId = zoneTabFileLine.Substring(startIndex, length); + } + else + { + timeZoneId = zoneTabFileLine.Substring(startIndex); + } + + if (!string.IsNullOrEmpty(timeZoneId)) + { + timeZoneIds.Add(timeZoneId); + } + } + } + } + } + } + } + catch (IOException) { } + catch (UnauthorizedAccessException) { } + + return timeZoneIds; + } + + private static string? GetTzEnvironmentVariable() + { + string? result = Environment.GetEnvironmentVariable(TimeZoneEnvironmentVariable); + if (!string.IsNullOrEmpty(result)) + { + if (result[0] == ':') + { + // strip off the ':' prefix + result = result.Substring(1); + } + } + + return result; + } + + /// + /// Finds the time zone id by using 'readlink' on the path to see if tzFilePath is + /// a symlink to a file. + /// + private static string? FindTimeZoneIdUsingReadLink(string tzFilePath) + { + string? id = null; + + string? symlinkPath = Interop.Sys.ReadLink(tzFilePath); + if (symlinkPath != null) + { + // symlinkPath can be relative path, use Path to get the full absolute path. + symlinkPath = Path.GetFullPath(symlinkPath, Path.GetDirectoryName(tzFilePath)!); + + string timeZoneDirectory = GetTimeZoneDirectory(); + if (symlinkPath.StartsWith(timeZoneDirectory, StringComparison.Ordinal)) + { + id = symlinkPath.Substring(timeZoneDirectory.Length); + } + } + + return id; + } + + private static string? GetDirectoryEntryFullPath(ref Interop.Sys.DirectoryEntry dirent, string currentPath) + { + ReadOnlySpan direntName = dirent.GetName(stackalloc char[Interop.Sys.DirectoryEntry.NameBufferSize]); + + if ((direntName.Length == 1 && direntName[0] == '.') || + (direntName.Length == 2 && direntName[0] == '.' && direntName[1] == '.')) + return null; + + return Path.Join(currentPath.AsSpan(), direntName); + } + + /// + /// Enumerate files + /// + private static unsafe void EnumerateFilesRecursively(string path, Predicate condition) + { + List? toExplore = null; // List used as a stack + + int bufferSize = Interop.Sys.GetReadDirRBufferSize(); + byte[]? dirBuffer = null; + try + { + dirBuffer = ArrayPool.Shared.Rent(bufferSize); + string currentPath = path; + + fixed (byte* dirBufferPtr = dirBuffer) + { + while (true) + { + IntPtr dirHandle = Interop.Sys.OpenDir(currentPath); + if (dirHandle == IntPtr.Zero) + { + throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo(), currentPath, isDirectory: true); + } + + try + { + // Read each entry from the enumerator + Interop.Sys.DirectoryEntry dirent; + while (Interop.Sys.ReadDirR(dirHandle, dirBufferPtr, bufferSize, out dirent) == 0) + { + string? fullPath = GetDirectoryEntryFullPath(ref dirent, currentPath); + if (fullPath == null) + continue; + + // Get from the dir entry whether the entry is a file or directory. + // We classify everything as a file unless we know it to be a directory. + bool isDir; + if (dirent.InodeType == Interop.Sys.NodeType.DT_DIR) + { + // We know it's a directory. + isDir = true; + } + else if (dirent.InodeType == Interop.Sys.NodeType.DT_LNK || dirent.InodeType == Interop.Sys.NodeType.DT_UNKNOWN) + { + // It's a symlink or unknown: stat to it to see if we can resolve it to a directory. + // If we can't (e.g. symlink to a file, broken symlink, etc.), we'll just treat it as a file. + + Interop.Sys.FileStatus fileinfo; + if (Interop.Sys.Stat(fullPath, out fileinfo) >= 0) + { + isDir = (fileinfo.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR; + } + else + { + isDir = false; + } + } + else + { + // Otherwise, treat it as a file. This includes regular files, FIFOs, etc. + isDir = false; + } + + // Yield the result if the user has asked for it. In the case of directories, + // always explore it by pushing it onto the stack, regardless of whether + // we're returning directories. + if (isDir) + { + toExplore ??= new List(); + toExplore.Add(fullPath); + } + else if (condition(fullPath)) + { + return; + } + } + } + finally + { + if (dirHandle != IntPtr.Zero) + Interop.Sys.CloseDir(dirHandle); + } + + if (toExplore == null || toExplore.Count == 0) + break; + + currentPath = toExplore[toExplore.Count - 1]; + toExplore.RemoveAt(toExplore.Count - 1); + } + } + } + finally + { + if (dirBuffer != null) + ArrayPool.Shared.Return(dirBuffer); + } + } + + private static bool CompareTimeZoneFile(string filePath, byte[] buffer, byte[] rawData) + { + try + { + // bufferSize == 1 used to avoid unnecessary buffer in FileStream + using (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; + } + + /// + /// Find the time zone id by searching all the tzfiles for the one that matches rawData + /// and return its file name. + /// + private static string FindTimeZoneId(byte[] rawData) + { + // default to "Local" if we can't find the right tzfile + string id = LocalId; + string timeZoneDirectory = GetTimeZoneDirectory(); + string localtimeFilePath = Path.Combine(timeZoneDirectory, "localtime"); + string posixrulesFilePath = Path.Combine(timeZoneDirectory, "posixrules"); + byte[] buffer = new byte[rawData.Length]; + + try + { + EnumerateFilesRecursively(timeZoneDirectory, (string filePath) => + { + // skip the localtime and posixrules file, since they won't give us the correct id + if (!string.Equals(filePath, localtimeFilePath, StringComparison.OrdinalIgnoreCase) + && !string.Equals(filePath, posixrulesFilePath, StringComparison.OrdinalIgnoreCase)) + { + if (CompareTimeZoneFile(filePath, buffer, rawData)) + { + // if all bytes are the same, this must be the right tz file + id = filePath; + + // strip off the root time zone directory + if (id.StartsWith(timeZoneDirectory, StringComparison.Ordinal)) + { + id = id.Substring(timeZoneDirectory.Length); + } + return true; + } + } + return false; + }); + } + catch (IOException) { } + catch (SecurityException) { } + catch (UnauthorizedAccessException) { } + + return id; + } + + private static bool TryLoadTzFile(string tzFilePath, [NotNullWhen(true)] ref byte[]? rawData, [NotNullWhen(true)] ref string? id) + { + if (File.Exists(tzFilePath)) + { + try + { + rawData = File.ReadAllBytes(tzFilePath); + if (string.IsNullOrEmpty(id)) + { + id = FindTimeZoneIdUsingReadLink(tzFilePath); + + if (string.IsNullOrEmpty(id)) + { + id = FindTimeZoneId(rawData); + } + } + return true; + } + catch (IOException) { } + catch (SecurityException) { } + catch (UnauthorizedAccessException) { } + } + return false; + } + + /// + /// Gets the tzfile raw data for the current 'local' time zone using the following rules. + /// 1. Read the TZ environment variable. If it is set, use it. + /// 2. Look for the data in /etc/localtime. + /// 3. Look for the data in GetTimeZoneDirectory()/localtime. + /// 4. Use UTC if all else fails. + /// + private static bool TryGetLocalTzFile([NotNullWhen(true)] out byte[]? rawData, [NotNullWhen(true)] out string? id) + { + rawData = null; + id = null; + string? tzVariable = GetTzEnvironmentVariable(); + + // If the env var is null, use the localtime file + if (tzVariable == null) + { + return + TryLoadTzFile("/etc/localtime", ref rawData, ref id) || + TryLoadTzFile(Path.Combine(GetTimeZoneDirectory(), "localtime"), ref rawData, ref id); + } + + // If it's empty, use UTC (TryGetLocalTzFile() should return false). + if (tzVariable.Length == 0) + { + return false; + } + + // Otherwise, use the path from the env var. If it's not absolute, make it relative + // to the system timezone directory + string tzFilePath; + if (tzVariable[0] != '/') + { + id = tzVariable; + tzFilePath = Path.Combine(GetTimeZoneDirectory(), tzVariable); + } + else + { + tzFilePath = tzVariable; + } + return TryLoadTzFile(tzFilePath, ref rawData, ref id); + } + + /// + /// Helper function used by 'GetLocalTimeZone()' - this function wraps the call + /// for loading time zone data from computers without Registry support. + /// + /// The TryGetLocalTzFile() call returns a Byte[] containing the compiled tzfile. + /// + private static TimeZoneInfo GetLocalTimeZoneFromTzFile() + { + byte[]? rawData; + string? id; + if (TryGetLocalTzFile(out rawData, out id)) + { + TimeZoneInfo? result = GetTimeZoneFromTzData(rawData, id); + if (result != null) + { + return result; + } + } + + // if we can't find a local time zone, return UTC + return Utc; + } + + private static string GetTimeZoneDirectory() + { + string? tzDirectory = Environment.GetEnvironmentVariable(TimeZoneDirectoryEnvironmentVariable); + + if (tzDirectory == null) + { + tzDirectory = DefaultTimeZoneDirectory; + } + else if (!tzDirectory.EndsWith(Path.DirectorySeparatorChar)) + { + tzDirectory += PathInternal.DirectorySeparatorCharAsString; + } + + return tzDirectory; + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs index 1356e65758c5c3..7b5131d053f92c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Buffers; using System.Buffers.Binary; using System.Collections.Generic; using System.Diagnostics; @@ -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 @@ private static void PopulateAllSystemTimeZones(CachedData cachedData) { 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 @@ private static TimeZoneInfo GetLocalTimeZone(CachedData cachedData) { Debug.Assert(Monitor.IsEntered(cachedData)); - // Without Registry support, create the TimeZoneInfo from a TZ file - return GetLocalTimeZoneFromTzFile(); + return GetLocalTimeZoneCore(); } private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out TimeZoneInfo? value, out Exception? e) { - value = null; - e = null; - - string timeZoneDirectory = GetTimeZoneDirectory(); - string timeZoneFilePath = Path.Combine(timeZoneDirectory, id); - byte[] rawData; - try - { - rawData = File.ReadAllBytes(timeZoneFilePath); - } - catch (UnauthorizedAccessException ex) - { - e = ex; - return TimeZoneInfoResult.SecurityException; - } - catch (FileNotFoundException ex) - { - e = ex; - return TimeZoneInfoResult.TimeZoneNotFoundException; - } - catch (DirectoryNotFoundException ex) - { - e = ex; - return TimeZoneInfoResult.TimeZoneNotFoundException; - } - catch (IOException ex) - { - e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, timeZoneFilePath), ex); - return TimeZoneInfoResult.InvalidTimeZoneException; - } - - value = GetTimeZoneFromTzData(rawData, id); - - if (value == null) - { - e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, timeZoneFilePath)); - return TimeZoneInfoResult.InvalidTimeZoneException; - } - - return TimeZoneInfoResult.Success; - } - - /// - /// Returns a collection of TimeZone Id values from the zone.tab file in the timeZoneDirectory. - /// - /// - /// Lines that start with # are comments and are skipped. - /// - private static List GetTimeZoneIds(string timeZoneDirectory) - { - List timeZoneIds = new List(); - - try - { - using (StreamReader sr = new StreamReader(Path.Combine(timeZoneDirectory, 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; - } - - /// - /// Gets the tzfile raw data for the current 'local' time zone using the following rules. - /// 1. Read the TZ environment variable. If it is set, use it. - /// 2. Look for the data in /etc/localtime. - /// 3. Look for the data in GetTimeZoneDirectory()/localtime. - /// 4. Use UTC if all else fails. - /// - private static bool TryGetLocalTzFile([NotNullWhen(true)] out byte[]? rawData, [NotNullWhen(true)] out string? id) - { - rawData = null; - id = null; - string? tzVariable = GetTzEnvironmentVariable(); - - // If the env var is null, use the localtime file - if (tzVariable == null) - { - return - TryLoadTzFile("/etc/localtime", ref rawData, ref id) || - TryLoadTzFile(Path.Combine(GetTimeZoneDirectory(), "localtime"), ref rawData, ref id); - } - - // If it's empty, use UTC (TryGetLocalTzFile() should return false). - if (tzVariable.Length == 0) - { - return false; - } - - // Otherwise, use the path from the env var. If it's not absolute, make it relative - // to the system timezone directory - string tzFilePath; - if (tzVariable[0] != '/') - { - id = tzVariable; - tzFilePath = Path.Combine(GetTimeZoneDirectory(), tzVariable); - } - else - { - tzFilePath = tzVariable; - } - return TryLoadTzFile(tzFilePath, ref rawData, ref id); - } - - private static string? GetTzEnvironmentVariable() - { - string? result = Environment.GetEnvironmentVariable(TimeZoneEnvironmentVariable); - if (!string.IsNullOrEmpty(result)) - { - if (result[0] == ':') - { - // strip off the ':' prefix - result = result.Substring(1); - } - } - - return result; - } - - private static bool TryLoadTzFile(string tzFilePath, [NotNullWhen(true)] ref byte[]? rawData, [NotNullWhen(true)] ref string? id) - { - if (File.Exists(tzFilePath)) - { - try - { - rawData = File.ReadAllBytes(tzFilePath); - if (string.IsNullOrEmpty(id)) - { - id = FindTimeZoneIdUsingReadLink(tzFilePath); - - if (string.IsNullOrEmpty(id)) - { - id = FindTimeZoneId(rawData); - } - } - return true; - } - catch (IOException) { } - catch (SecurityException) { } - catch (UnauthorizedAccessException) { } - } - return false; - } - - /// - /// Finds the time zone id by using 'readlink' on the path to see if tzFilePath is - /// a symlink to a file. - /// - private static string? FindTimeZoneIdUsingReadLink(string tzFilePath) - { - string? id = null; - - string? symlinkPath = Interop.Sys.ReadLink(tzFilePath); - if (symlinkPath != null) - { - // symlinkPath can be relative path, use Path to get the full absolute path. - symlinkPath = Path.GetFullPath(symlinkPath, Path.GetDirectoryName(tzFilePath)!); - - string timeZoneDirectory = GetTimeZoneDirectory(); - if (symlinkPath.StartsWith(timeZoneDirectory, StringComparison.Ordinal)) - { - id = symlinkPath.Substring(timeZoneDirectory.Length); - } - } - - return id; - } - - private static string? GetDirectoryEntryFullPath(ref Interop.Sys.DirectoryEntry dirent, string currentPath) - { - ReadOnlySpan direntName = dirent.GetName(stackalloc char[Interop.Sys.DirectoryEntry.NameBufferSize]); - - if ((direntName.Length == 1 && direntName[0] == '.') || - (direntName.Length == 2 && direntName[0] == '.' && direntName[1] == '.')) - return null; - - return Path.Join(currentPath.AsSpan(), direntName); - } - - /// - /// Enumerate files - /// - private static unsafe void EnumerateFilesRecursively(string path, Predicate condition) - { - List? toExplore = null; // List used as a stack - - int bufferSize = Interop.Sys.GetReadDirRBufferSize(); - byte[]? dirBuffer = null; - try - { - dirBuffer = ArrayPool.Shared.Rent(bufferSize); - string currentPath = path; - - fixed (byte* dirBufferPtr = dirBuffer) - { - while (true) - { - IntPtr dirHandle = Interop.Sys.OpenDir(currentPath); - if (dirHandle == IntPtr.Zero) - { - throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo(), currentPath, isDirectory: true); - } - - try - { - // Read each entry from the enumerator - Interop.Sys.DirectoryEntry dirent; - while (Interop.Sys.ReadDirR(dirHandle, dirBufferPtr, bufferSize, out dirent) == 0) - { - string? fullPath = GetDirectoryEntryFullPath(ref dirent, currentPath); - if (fullPath == null) - continue; - - // Get from the dir entry whether the entry is a file or directory. - // We classify everything as a file unless we know it to be a directory. - bool isDir; - if (dirent.InodeType == Interop.Sys.NodeType.DT_DIR) - { - // We know it's a directory. - isDir = true; - } - else if (dirent.InodeType == Interop.Sys.NodeType.DT_LNK || dirent.InodeType == Interop.Sys.NodeType.DT_UNKNOWN) - { - // It's a symlink or unknown: stat to it to see if we can resolve it to a directory. - // If we can't (e.g. symlink to a file, broken symlink, etc.), we'll just treat it as a file. - - Interop.Sys.FileStatus fileinfo; - if (Interop.Sys.Stat(fullPath, out fileinfo) >= 0) - { - isDir = (fileinfo.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR; - } - else - { - isDir = false; - } - } - else - { - // Otherwise, treat it as a file. This includes regular files, FIFOs, etc. - isDir = false; - } - - // Yield the result if the user has asked for it. In the case of directories, - // always explore it by pushing it onto the stack, regardless of whether - // we're returning directories. - if (isDir) - { - toExplore ??= new List(); - toExplore.Add(fullPath); - } - else if (condition(fullPath)) - { - return; - } - } - } - finally - { - if (dirHandle != IntPtr.Zero) - Interop.Sys.CloseDir(dirHandle); - } - - if (toExplore == null || toExplore.Count == 0) - break; - - currentPath = toExplore[toExplore.Count - 1]; - toExplore.RemoveAt(toExplore.Count - 1); - } - } - } - finally - { - if (dirBuffer != null) - ArrayPool.Shared.Return(dirBuffer); - } - } - - /// - /// Find the time zone id by searching all the tzfiles for the one that matches rawData - /// and return its file name. - /// - private static string FindTimeZoneId(byte[] rawData) - { - // default to "Local" if we can't find the right tzfile - string id = LocalId; - string timeZoneDirectory = GetTimeZoneDirectory(); - string localtimeFilePath = Path.Combine(timeZoneDirectory, "localtime"); - string posixrulesFilePath = Path.Combine(timeZoneDirectory, "posixrules"); - byte[] buffer = new byte[rawData.Length]; - - try - { - EnumerateFilesRecursively(timeZoneDirectory, (string filePath) => - { - // skip the localtime and posixrules file, since they won't give us the correct id - if (!string.Equals(filePath, localtimeFilePath, StringComparison.OrdinalIgnoreCase) - && !string.Equals(filePath, posixrulesFilePath, StringComparison.OrdinalIgnoreCase)) - { - if (CompareTimeZoneFile(filePath, buffer, rawData)) - { - // if all bytes are the same, this must be the right tz file - id = filePath; - - // strip off the root time zone directory - if (id.StartsWith(timeZoneDirectory, StringComparison.Ordinal)) - { - id = id.Substring(timeZoneDirectory.Length); - } - return true; - } - } - return false; - }); - } - catch (IOException) { } - catch (SecurityException) { } - catch (UnauthorizedAccessException) { } - - return id; - } - - private static bool CompareTimeZoneFile(string filePath, byte[] buffer, byte[] rawData) - { - try - { - // bufferSize == 1 used to avoid unnecessary buffer in FileStream - using (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; - } - - /// - /// Helper function used by 'GetLocalTimeZone()' - this function wraps the call - /// for loading time zone data from computers without Registry support. - /// - /// The TryGetLocalTzFile() call returns a Byte[] containing the compiled tzfile. - /// - private static TimeZoneInfo GetLocalTimeZoneFromTzFile() - { - byte[]? rawData; - string? id; - if (TryGetLocalTzFile(out rawData, out id)) - { - TimeZoneInfo? result = GetTimeZoneFromTzData(rawData, id); - if (result != null) - { - return result; - } - } - - // if we can't find a local time zone, return UTC - return Utc; + return TryGetTimeZoneFromLocalMachineCore(id, out value, out e); } private static TimeZoneInfo? GetTimeZoneFromTzData(byte[]? rawData, string id) @@ -697,21 +272,6 @@ private static TimeZoneInfo GetLocalTimeZoneFromTzFile() return null; } - private static string GetTimeZoneDirectory() - { - string? tzDirectory = Environment.GetEnvironmentVariable(TimeZoneDirectoryEnvironmentVariable); - - if (tzDirectory == null) - { - tzDirectory = DefaultTimeZoneDirectory; - } - else if (!tzDirectory.EndsWith(Path.DirectorySeparatorChar)) - { - tzDirectory += PathInternal.DirectorySeparatorCharAsString; - } - - return tzDirectory; - } /// /// Helper function for retrieving a TimeZoneInfo object by time_zone_name. @@ -1531,16 +1091,27 @@ private static string TZif_GetZoneAbbreviation(string zoneAbbreviations, int ind zoneAbbreviations.Substring(index); } + // Converts a span of bytes into a long - always using standard byte order (Big Endian) + // per TZif file standard + private static short TZif_ToInt16(ReadOnlySpan value) + => BinaryPrimitives.ReadInt16BigEndian(value); + // Converts an array of bytes into an int - always using standard byte order (Big Endian) // per TZif file standard private static int TZif_ToInt32(byte[] value, int startIndex) => BinaryPrimitives.ReadInt32BigEndian(value.AsSpan(startIndex)); + // Converts a span of bytes into an int - always using standard byte order (Big Endian) + // per TZif file standard + private static int TZif_ToInt32(ReadOnlySpan value) + => BinaryPrimitives.ReadInt32BigEndian(value); + // Converts an array of bytes into a long - always using standard byte order (Big Endian) // per TZif file standard private static long TZif_ToInt64(byte[] value, int startIndex) => BinaryPrimitives.ReadInt64BigEndian(value.AsSpan(startIndex)); + private static long TZif_ToUnixTime(byte[] value, int startIndex, TZVersion version) => version != TZVersion.V1 ? TZif_ToInt64(value, startIndex) :