Fix pragma warning restore (dotnet/coreclr#26389)
[mono-project.git] / netcore / System.Private.CoreLib / shared / System / TimeZoneInfo.Unix.cs
blob1321fe768ece9920bb7509f89c7c57363cd180fb
1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
5 using System.Buffers;
6 using System.Collections.Generic;
7 using System.Diagnostics;
8 using System.Diagnostics.CodeAnalysis;
9 using System.Globalization;
10 using System.IO;
11 using System.Text;
12 using System.Threading;
13 using System.Security;
14 using System.Runtime.CompilerServices;
15 using System.Runtime.InteropServices;
17 using Internal.IO;
19 namespace System
21 public sealed partial class TimeZoneInfo
23 private const string DefaultTimeZoneDirectory = "/usr/share/zoneinfo/";
24 private const string ZoneTabFileName = "zone.tab";
25 private const string TimeZoneEnvironmentVariable = "TZ";
26 private const string TimeZoneDirectoryEnvironmentVariable = "TZDIR";
28 private TimeZoneInfo(byte[] data, string id, bool dstDisabled)
30 TZifHead t;
31 DateTime[] dts;
32 byte[] typeOfLocalTime;
33 TZifType[] transitionType;
34 string zoneAbbreviations;
35 bool[] StandardTime;
36 bool[] GmtTime;
37 string? futureTransitionsPosixFormat;
39 // parse the raw TZif bytes; this method can throw ArgumentException when the data is malformed.
40 TZif_ParseRaw(data, out t, out dts, out typeOfLocalTime, out transitionType, out zoneAbbreviations, out StandardTime, out GmtTime, out futureTransitionsPosixFormat);
42 _id = id;
43 _displayName = LocalId;
44 _baseUtcOffset = TimeSpan.Zero;
46 // find the best matching baseUtcOffset and display strings based on the current utcNow value.
47 // NOTE: read the display strings from the tzfile now in case they can't be loaded later
48 // from the globalization data.
49 DateTime utcNow = DateTime.UtcNow;
50 for (int i = 0; i < dts.Length && dts[i] <= utcNow; i++)
52 int type = typeOfLocalTime[i];
53 if (!transitionType[type].IsDst)
55 _baseUtcOffset = transitionType[type].UtcOffset;
56 _standardDisplayName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[type].AbbreviationIndex);
58 else
60 _daylightDisplayName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[type].AbbreviationIndex);
64 if (dts.Length == 0)
66 // time zones like Africa/Bujumbura and Etc/GMT* have no transition times but still contain
67 // TZifType entries that may contain a baseUtcOffset and display strings
68 for (int i = 0; i < transitionType.Length; i++)
70 if (!transitionType[i].IsDst)
72 _baseUtcOffset = transitionType[i].UtcOffset;
73 _standardDisplayName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[i].AbbreviationIndex);
75 else
77 _daylightDisplayName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[i].AbbreviationIndex);
81 _displayName = _standardDisplayName;
83 GetDisplayName(Interop.Globalization.TimeZoneDisplayNameType.Generic, ref _displayName);
84 GetDisplayName(Interop.Globalization.TimeZoneDisplayNameType.Standard, ref _standardDisplayName);
85 GetDisplayName(Interop.Globalization.TimeZoneDisplayNameType.DaylightSavings, ref _daylightDisplayName);
87 if (_standardDisplayName == _displayName)
89 if (_baseUtcOffset >= TimeSpan.Zero)
90 _displayName = $"(UTC+{_baseUtcOffset:hh\\:mm}) {_standardDisplayName}";
91 else
92 _displayName = $"(UTC-{_baseUtcOffset:hh\\:mm}) {_standardDisplayName}";
95 // TZif supports seconds-level granularity with offsets but TimeZoneInfo only supports minutes since it aligns
96 // with DateTimeOffset, SQL Server, and the W3C XML Specification
97 if (_baseUtcOffset.Ticks % TimeSpan.TicksPerMinute != 0)
99 _baseUtcOffset = new TimeSpan(_baseUtcOffset.Hours, _baseUtcOffset.Minutes, 0);
102 if (!dstDisabled)
104 // only create the adjustment rule if DST is enabled
105 TZif_GenerateAdjustmentRules(out _adjustmentRules, _baseUtcOffset, dts, typeOfLocalTime, transitionType, StandardTime, GmtTime, futureTransitionsPosixFormat);
108 ValidateTimeZoneInfo(_id, _baseUtcOffset, _adjustmentRules, out _supportsDaylightSavingTime);
111 private unsafe void GetDisplayName(Interop.Globalization.TimeZoneDisplayNameType nameType, ref string? displayName)
113 if (GlobalizationMode.Invariant)
115 displayName = _standardDisplayName;
116 return;
119 string? timeZoneDisplayName;
120 bool result = Interop.CallStringMethod(
121 (buffer, locale, id, type) =>
123 fixed (char* bufferPtr = buffer)
125 return Interop.Globalization.GetTimeZoneDisplayName(locale, id, type, bufferPtr, buffer.Length);
128 CultureInfo.CurrentUICulture.Name,
129 _id,
130 nameType,
131 out timeZoneDisplayName);
133 // If there is an unknown error, don't set the displayName field.
134 // It will be set to the abbreviation that was read out of the tzfile.
135 if (result)
137 displayName = timeZoneDisplayName;
141 /// <summary>
142 /// Returns a cloned array of AdjustmentRule objects
143 /// </summary>
144 public AdjustmentRule[] GetAdjustmentRules()
146 if (_adjustmentRules == null)
148 return Array.Empty<AdjustmentRule>();
151 // The rules we use in Unix care mostly about the start and end dates but don't fill the transition start and end info.
152 // as the rules now is public, we should fill it properly so the caller doesn't have to know how we use it internally
153 // and can use it as it is used in Windows
155 AdjustmentRule[] rules = new AdjustmentRule[_adjustmentRules.Length];
157 for (int i = 0; i < _adjustmentRules.Length; i++)
159 var rule = _adjustmentRules[i];
160 var start = rule.DateStart.Kind == DateTimeKind.Utc ?
161 // At the daylight start we didn't start the daylight saving yet then we convert to Local time
162 // by adding the _baseUtcOffset to the UTC time
163 new DateTime(rule.DateStart.Ticks + _baseUtcOffset.Ticks, DateTimeKind.Unspecified) :
164 rule.DateStart;
165 var end = rule.DateEnd.Kind == DateTimeKind.Utc ?
166 // At the daylight saving end, the UTC time is mapped to local time which is already shifted by the daylight delta
167 // we calculate the local time by adding _baseUtcOffset + DaylightDelta to the UTC time
168 new DateTime(rule.DateEnd.Ticks + _baseUtcOffset.Ticks + rule.DaylightDelta.Ticks, DateTimeKind.Unspecified) :
169 rule.DateEnd;
171 var startTransition = TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, start.Hour, start.Minute, start.Second), start.Month, start.Day);
172 var endTransition = TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, end.Hour, end.Minute, end.Second), end.Month, end.Day);
174 rules[i] = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(start.Date, end.Date, rule.DaylightDelta, startTransition, endTransition);
177 return rules;
180 private static void PopulateAllSystemTimeZones(CachedData cachedData)
182 Debug.Assert(Monitor.IsEntered(cachedData));
184 string timeZoneDirectory = GetTimeZoneDirectory();
185 foreach (string timeZoneId in GetTimeZoneIds(timeZoneDirectory))
187 TryGetTimeZone(timeZoneId, false, out _, out _, cachedData, alwaysFallbackToLocalMachine: true); // populate the cache
191 /// <summary>
192 /// Helper function for retrieving the local system time zone.
193 /// May throw COMException, TimeZoneNotFoundException, InvalidTimeZoneException.
194 /// Assumes cachedData lock is taken.
195 /// </summary>
196 /// <returns>A new TimeZoneInfo instance.</returns>
197 private static TimeZoneInfo GetLocalTimeZone(CachedData cachedData)
199 Debug.Assert(Monitor.IsEntered(cachedData));
201 // Without Registry support, create the TimeZoneInfo from a TZ file
202 return GetLocalTimeZoneFromTzFile();
205 private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out TimeZoneInfo? value, out Exception? e)
207 value = null;
208 e = null;
210 string timeZoneDirectory = GetTimeZoneDirectory();
211 string timeZoneFilePath = Path.Combine(timeZoneDirectory, id);
212 byte[] rawData;
215 rawData = File.ReadAllBytes(timeZoneFilePath);
217 catch (UnauthorizedAccessException ex)
219 e = ex;
220 return TimeZoneInfoResult.SecurityException;
222 catch (FileNotFoundException ex)
224 e = ex;
225 return TimeZoneInfoResult.TimeZoneNotFoundException;
227 catch (DirectoryNotFoundException ex)
229 e = ex;
230 return TimeZoneInfoResult.TimeZoneNotFoundException;
232 catch (IOException ex)
234 e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, timeZoneFilePath), ex);
235 return TimeZoneInfoResult.InvalidTimeZoneException;
238 value = GetTimeZoneFromTzData(rawData, id);
240 if (value == null)
242 e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, timeZoneFilePath));
243 return TimeZoneInfoResult.InvalidTimeZoneException;
246 return TimeZoneInfoResult.Success;
249 /// <summary>
250 /// Returns a collection of TimeZone Id values from the zone.tab file in the timeZoneDirectory.
251 /// </summary>
252 /// <remarks>
253 /// Lines that start with # are comments and are skipped.
254 /// </remarks>
255 private static List<string> GetTimeZoneIds(string timeZoneDirectory)
257 List<string> timeZoneIds = new List<string>();
261 using (StreamReader sr = new StreamReader(Path.Combine(timeZoneDirectory, ZoneTabFileName), Encoding.UTF8))
263 string? zoneTabFileLine;
264 while ((zoneTabFileLine = sr.ReadLine()) != null)
266 if (!string.IsNullOrEmpty(zoneTabFileLine) && zoneTabFileLine[0] != '#')
268 // the format of the line is "country-code \t coordinates \t TimeZone Id \t comments"
270 int firstTabIndex = zoneTabFileLine.IndexOf('\t');
271 if (firstTabIndex != -1)
273 int secondTabIndex = zoneTabFileLine.IndexOf('\t', firstTabIndex + 1);
274 if (secondTabIndex != -1)
276 string timeZoneId;
277 int startIndex = secondTabIndex + 1;
278 int thirdTabIndex = zoneTabFileLine.IndexOf('\t', startIndex);
279 if (thirdTabIndex != -1)
281 int length = thirdTabIndex - startIndex;
282 timeZoneId = zoneTabFileLine.Substring(startIndex, length);
284 else
286 timeZoneId = zoneTabFileLine.Substring(startIndex);
289 if (!string.IsNullOrEmpty(timeZoneId))
291 timeZoneIds.Add(timeZoneId);
299 catch (IOException) { }
300 catch (UnauthorizedAccessException) { }
302 return timeZoneIds;
305 /// <summary>
306 /// Gets the tzfile raw data for the current 'local' time zone using the following rules.
307 /// 1. Read the TZ environment variable. If it is set, use it.
308 /// 2. Look for the data in /etc/localtime.
309 /// 3. Look for the data in GetTimeZoneDirectory()/localtime.
310 /// 4. Use UTC if all else fails.
311 /// </summary>
312 private static bool TryGetLocalTzFile([NotNullWhen(true)] out byte[]? rawData, [NotNullWhen(true)] out string? id)
314 rawData = null;
315 id = null;
316 string? tzVariable = GetTzEnvironmentVariable();
318 // If the env var is null, use the localtime file
319 if (tzVariable == null)
321 return
322 TryLoadTzFile("/etc/localtime", ref rawData, ref id) ||
323 TryLoadTzFile(Path.Combine(GetTimeZoneDirectory(), "localtime"), ref rawData, ref id);
326 // If it's empty, use UTC (TryGetLocalTzFile() should return false).
327 if (tzVariable.Length == 0)
329 return false;
332 // Otherwise, use the path from the env var. If it's not absolute, make it relative
333 // to the system timezone directory
334 string tzFilePath;
335 if (tzVariable[0] != '/')
337 id = tzVariable;
338 tzFilePath = Path.Combine(GetTimeZoneDirectory(), tzVariable);
340 else
342 tzFilePath = tzVariable;
344 return TryLoadTzFile(tzFilePath, ref rawData, ref id);
347 private static string? GetTzEnvironmentVariable()
349 string? result = Environment.GetEnvironmentVariable(TimeZoneEnvironmentVariable);
350 if (!string.IsNullOrEmpty(result))
352 if (result[0] == ':')
354 // strip off the ':' prefix
355 result = result.Substring(1);
359 return result;
362 private static bool TryLoadTzFile(string tzFilePath, [NotNullWhen(true)] ref byte[]? rawData, [NotNullWhen(true)] ref string? id)
364 if (File.Exists(tzFilePath))
368 rawData = File.ReadAllBytes(tzFilePath);
369 if (string.IsNullOrEmpty(id))
371 id = FindTimeZoneIdUsingReadLink(tzFilePath);
373 if (string.IsNullOrEmpty(id))
375 id = FindTimeZoneId(rawData);
378 return true;
380 catch (IOException) { }
381 catch (SecurityException) { }
382 catch (UnauthorizedAccessException) { }
384 return false;
387 /// <summary>
388 /// Finds the time zone id by using 'readlink' on the path to see if tzFilePath is
389 /// a symlink to a file.
390 /// </summary>
391 private static string? FindTimeZoneIdUsingReadLink(string tzFilePath)
393 string? id = null;
395 string? symlinkPath = Interop.Sys.ReadLink(tzFilePath);
396 if (symlinkPath != null)
398 // symlinkPath can be relative path, use Path to get the full absolute path.
399 symlinkPath = Path.GetFullPath(symlinkPath, Path.GetDirectoryName(tzFilePath)!);
401 string timeZoneDirectory = GetTimeZoneDirectory();
402 if (symlinkPath.StartsWith(timeZoneDirectory, StringComparison.Ordinal))
404 id = symlinkPath.Substring(timeZoneDirectory.Length);
408 return id;
411 private static string? GetDirectoryEntryFullPath(ref Interop.Sys.DirectoryEntry dirent, string currentPath)
413 Span<char> nameBuffer = stackalloc char[Interop.Sys.DirectoryEntry.NameBufferSize];
414 ReadOnlySpan<char> direntName = dirent.GetName(nameBuffer);
416 if ((direntName.Length == 1 && direntName[0] == '.') ||
417 (direntName.Length == 2 && direntName[0] == '.' && direntName[1] == '.'))
418 return null;
420 return Path.Join(currentPath.AsSpan(), direntName);
423 /// <summary>
424 /// Enumerate files
425 /// </summary>
426 private static unsafe void EnumerateFilesRecursively(string path, Predicate<string> condition)
428 List<string>? toExplore = null; // List used as a stack
430 int bufferSize = Interop.Sys.GetReadDirRBufferSize();
431 byte[]? dirBuffer = null;
434 dirBuffer = ArrayPool<byte>.Shared.Rent(bufferSize);
435 string currentPath = path;
437 fixed (byte* dirBufferPtr = dirBuffer)
439 while (true)
441 IntPtr dirHandle = Interop.Sys.OpenDir(currentPath);
442 if (dirHandle == IntPtr.Zero)
444 throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo(), currentPath, isDirectory: true);
449 // Read each entry from the enumerator
450 Interop.Sys.DirectoryEntry dirent;
451 while (Interop.Sys.ReadDirR(dirHandle, dirBufferPtr, bufferSize, out dirent) == 0)
453 string? fullPath = GetDirectoryEntryFullPath(ref dirent, currentPath);
454 if (fullPath == null)
455 continue;
457 // Get from the dir entry whether the entry is a file or directory.
458 // We classify everything as a file unless we know it to be a directory.
459 bool isDir;
460 if (dirent.InodeType == Interop.Sys.NodeType.DT_DIR)
462 // We know it's a directory.
463 isDir = true;
465 else if (dirent.InodeType == Interop.Sys.NodeType.DT_LNK || dirent.InodeType == Interop.Sys.NodeType.DT_UNKNOWN)
467 // It's a symlink or unknown: stat to it to see if we can resolve it to a directory.
468 // If we can't (e.g. symlink to a file, broken symlink, etc.), we'll just treat it as a file.
470 Interop.Sys.FileStatus fileinfo;
471 if (Interop.Sys.Stat(fullPath, out fileinfo) >= 0)
473 isDir = (fileinfo.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR;
475 else
477 isDir = false;
480 else
482 // Otherwise, treat it as a file. This includes regular files, FIFOs, etc.
483 isDir = false;
486 // Yield the result if the user has asked for it. In the case of directories,
487 // always explore it by pushing it onto the stack, regardless of whether
488 // we're returning directories.
489 if (isDir)
491 if (toExplore == null)
493 toExplore = new List<string>();
495 toExplore.Add(fullPath);
497 else if (condition(fullPath))
499 return;
503 finally
505 if (dirHandle != IntPtr.Zero)
506 Interop.Sys.CloseDir(dirHandle);
509 if (toExplore == null || toExplore.Count == 0)
510 break;
512 currentPath = toExplore[toExplore.Count - 1];
513 toExplore.RemoveAt(toExplore.Count - 1);
517 finally
519 if (dirBuffer != null)
520 ArrayPool<byte>.Shared.Return(dirBuffer);
524 /// <summary>
525 /// Find the time zone id by searching all the tzfiles for the one that matches rawData
526 /// and return its file name.
527 /// </summary>
528 private static string FindTimeZoneId(byte[] rawData)
530 // default to "Local" if we can't find the right tzfile
531 string id = LocalId;
532 string timeZoneDirectory = GetTimeZoneDirectory();
533 string localtimeFilePath = Path.Combine(timeZoneDirectory, "localtime");
534 string posixrulesFilePath = Path.Combine(timeZoneDirectory, "posixrules");
535 byte[] buffer = new byte[rawData.Length];
539 EnumerateFilesRecursively(timeZoneDirectory, (string filePath) =>
541 // skip the localtime and posixrules file, since they won't give us the correct id
542 if (!string.Equals(filePath, localtimeFilePath, StringComparison.OrdinalIgnoreCase)
543 && !string.Equals(filePath, posixrulesFilePath, StringComparison.OrdinalIgnoreCase))
545 if (CompareTimeZoneFile(filePath, buffer, rawData))
547 // if all bytes are the same, this must be the right tz file
548 id = filePath;
550 // strip off the root time zone directory
551 if (id.StartsWith(timeZoneDirectory, StringComparison.Ordinal))
553 id = id.Substring(timeZoneDirectory.Length);
555 return true;
558 return false;
561 catch (IOException) { }
562 catch (SecurityException) { }
563 catch (UnauthorizedAccessException) { }
565 return id;
568 private static bool CompareTimeZoneFile(string filePath, byte[] buffer, byte[] rawData)
572 // bufferSize == 1 used to avoid unnecessary buffer in FileStream
573 using (FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1))
575 if (stream.Length == rawData.Length)
577 int index = 0;
578 int count = rawData.Length;
580 while (count > 0)
582 int n = stream.Read(buffer, index, count);
583 if (n == 0)
584 throw Error.GetEndOfFile();
586 int end = index + n;
587 for (; index < end; index++)
589 if (buffer[index] != rawData[index])
591 return false;
595 count -= n;
598 return true;
602 catch (IOException) { }
603 catch (SecurityException) { }
604 catch (UnauthorizedAccessException) { }
606 return false;
609 /// <summary>
610 /// Helper function used by 'GetLocalTimeZone()' - this function wraps the call
611 /// for loading time zone data from computers without Registry support.
613 /// The TryGetLocalTzFile() call returns a Byte[] containing the compiled tzfile.
614 /// </summary>
615 private static TimeZoneInfo GetLocalTimeZoneFromTzFile()
617 byte[]? rawData;
618 string? id;
619 if (TryGetLocalTzFile(out rawData, out id))
621 TimeZoneInfo? result = GetTimeZoneFromTzData(rawData, id);
622 if (result != null)
624 return result;
628 // if we can't find a local time zone, return UTC
629 return Utc;
632 private static TimeZoneInfo? GetTimeZoneFromTzData(byte[]? rawData, string id)
634 if (rawData != null)
638 return new TimeZoneInfo(rawData, id, dstDisabled: false); // create a TimeZoneInfo instance from the TZif data w/ DST support
640 catch (ArgumentException) { }
641 catch (InvalidTimeZoneException) { }
645 return new TimeZoneInfo(rawData, id, dstDisabled: true); // create a TimeZoneInfo instance from the TZif data w/o DST support
647 catch (ArgumentException) { }
648 catch (InvalidTimeZoneException) { }
650 return null;
653 private static string GetTimeZoneDirectory()
655 string? tzDirectory = Environment.GetEnvironmentVariable(TimeZoneDirectoryEnvironmentVariable);
657 if (tzDirectory == null)
659 tzDirectory = DefaultTimeZoneDirectory;
661 else if (!tzDirectory.EndsWith(Path.DirectorySeparatorChar))
663 tzDirectory += PathInternal.DirectorySeparatorCharAsString;
666 return tzDirectory;
669 /// <summary>
670 /// Helper function for retrieving a TimeZoneInfo object by time_zone_name.
671 /// This function wraps the logic necessary to keep the private
672 /// SystemTimeZones cache in working order
674 /// This function will either return a valid TimeZoneInfo instance or
675 /// it will throw 'InvalidTimeZoneException' / 'TimeZoneNotFoundException'.
676 /// </summary>
677 public static TimeZoneInfo FindSystemTimeZoneById(string id)
679 // Special case for Utc as it will not exist in the dictionary with the rest
680 // of the system time zones. There is no need to do this check for Local.Id
681 // since Local is a real time zone that exists in the dictionary cache
682 if (string.Equals(id, UtcId, StringComparison.OrdinalIgnoreCase))
684 return Utc;
687 if (id == null)
689 throw new ArgumentNullException(nameof(id));
691 else if (id.Length == 0 || id.Contains('\0'))
693 throw new TimeZoneNotFoundException(SR.Format(SR.TimeZoneNotFound_MissingData, id));
696 TimeZoneInfo? value;
697 Exception? e;
699 TimeZoneInfoResult result;
701 CachedData cachedData = s_cachedData;
703 lock (cachedData)
705 result = TryGetTimeZone(id, false, out value, out e, cachedData, alwaysFallbackToLocalMachine: true);
708 if (result == TimeZoneInfoResult.Success)
710 return value!;
712 else if (result == TimeZoneInfoResult.InvalidTimeZoneException)
714 Debug.Assert(e is InvalidTimeZoneException,
715 "TryGetTimeZone must create an InvalidTimeZoneException when it returns TimeZoneInfoResult.InvalidTimeZoneException");
716 throw e;
718 else if (result == TimeZoneInfoResult.SecurityException)
720 throw new SecurityException(SR.Format(SR.Security_CannotReadFileData, id), e);
722 else
724 throw new TimeZoneNotFoundException(SR.Format(SR.TimeZoneNotFound_MissingData, id), e);
728 // DateTime.Now fast path that avoids allocating an historically accurate TimeZoneInfo.Local and just creates a 1-year (current year) accurate time zone
729 internal static TimeSpan GetDateTimeNowUtcOffsetFromUtc(DateTime time, out bool isAmbiguousLocalDst)
731 bool isDaylightSavings;
732 // Use the standard code path for Unix since there isn't a faster way of handling current-year-only time zones
733 return GetUtcOffsetFromUtc(time, Local, out isDaylightSavings, out isAmbiguousLocalDst);
736 // TZFILE(5) BSD File Formats Manual TZFILE(5)
738 // NAME
739 // tzfile -- timezone information
741 // SYNOPSIS
742 // #include "/usr/src/lib/libc/stdtime/tzfile.h"
744 // DESCRIPTION
745 // The time zone information files used by tzset(3) begin with the magic
746 // characters ``TZif'' to identify them as time zone information files, fol-
747 // lowed by sixteen bytes reserved for future use, followed by four four-
748 // byte values written in a ``standard'' byte order (the high-order byte of
749 // the value is written first). These values are, in order:
751 // tzh_ttisgmtcnt The number of UTC/local indicators stored in the file.
752 // tzh_ttisstdcnt The number of standard/wall indicators stored in the
753 // file.
754 // tzh_leapcnt The number of leap seconds for which data is stored in
755 // the file.
756 // tzh_timecnt The number of ``transition times'' for which data is
757 // stored in the file.
758 // tzh_typecnt The number of ``local time types'' for which data is
759 // stored in the file (must not be zero).
760 // tzh_charcnt The number of characters of ``time zone abbreviation
761 // strings'' stored in the file.
763 // The above header is followed by tzh_timecnt four-byte values of type
764 // long, sorted in ascending order. These values are written in ``stan-
765 // dard'' byte order. Each is used as a transition time (as returned by
766 // time(3)) at which the rules for computing local time change. Next come
767 // tzh_timecnt one-byte values of type unsigned char; each one tells which
768 // of the different types of ``local time'' types described in the file is
769 // associated with the same-indexed transition time. These values serve as
770 // indices into an array of ttinfo structures that appears next in the file;
771 // these structures are defined as follows:
773 // struct ttinfo {
774 // long tt_gmtoff;
775 // int tt_isdst;
776 // unsigned int tt_abbrind;
777 // };
779 // Each structure is written as a four-byte value for tt_gmtoff of type
780 // long, in a standard byte order, followed by a one-byte value for tt_isdst
781 // and a one-byte value for tt_abbrind. In each structure, tt_gmtoff gives
782 // the number of seconds to be added to UTC, tt_isdst tells whether tm_isdst
783 // should be set by localtime(3) and tt_abbrind serves as an index into the
784 // array of time zone abbreviation characters that follow the ttinfo struc-
785 // ture(s) in the file.
787 // Then there are tzh_leapcnt pairs of four-byte values, written in standard
788 // byte order; the first value of each pair gives the time (as returned by
789 // time(3)) at which a leap second occurs; the second gives the total number
790 // of leap seconds to be applied after the given time. The pairs of values
791 // are sorted in ascending order by time.b
793 // Then there are tzh_ttisstdcnt standard/wall indicators, each stored as a
794 // one-byte value; they tell whether the transition times associated with
795 // local time types were specified as standard time or wall clock time, and
796 // are used when a time zone file is used in handling POSIX-style time zone
797 // environment variables.
799 // Finally there are tzh_ttisgmtcnt UTC/local indicators, each stored as a
800 // one-byte value; they tell whether the transition times associated with
801 // local time types were specified as UTC or local time, and are used when a
802 // time zone file is used in handling POSIX-style time zone environment
803 // variables.
805 // localtime uses the first standard-time ttinfo structure in the file (or
806 // simply the first ttinfo structure in the absence of a standard-time
807 // structure) if either tzh_timecnt is zero or the time argument is less
808 // than the first transition time recorded in the file.
810 // SEE ALSO
811 // ctime(3), time2posix(3), zic(8)
813 // BSD September 13, 1994 BSD
817 // TIME(3) BSD Library Functions Manual TIME(3)
819 // NAME
820 // time -- get time of day
822 // LIBRARY
823 // Standard C Library (libc, -lc)
825 // SYNOPSIS
826 // #include <time.h>
828 // time_t
829 // time(time_t *tloc);
831 // DESCRIPTION
832 // The time() function returns the value of time in seconds since 0 hours, 0
833 // minutes, 0 seconds, January 1, 1970, Coordinated Universal Time, without
834 // including leap seconds. If an error occurs, time() returns the value
835 // (time_t)-1.
837 // The return value is also stored in *tloc, provided that tloc is non-null.
839 // ERRORS
840 // The time() function may fail for any of the reasons described in
841 // gettimeofday(2).
843 // SEE ALSO
844 // gettimeofday(2), ctime(3)
846 // STANDARDS
847 // The time function conforms to IEEE Std 1003.1-2001 (``POSIX.1'').
849 // BUGS
850 // Neither ISO/IEC 9899:1999 (``ISO C99'') nor IEEE Std 1003.1-2001
851 // (``POSIX.1'') requires time() to set errno on failure; thus, it is impos-
852 // sible for an application to distinguish the valid time value -1 (repre-
853 // senting the last UTC second of 1969) from the error return value.
855 // Systems conforming to earlier versions of the C and POSIX standards
856 // (including older versions of FreeBSD) did not set *tloc in the error
857 // case.
859 // HISTORY
860 // A time() function appeared in Version 6 AT&T UNIX.
862 // BSD July 18, 2003 BSD
865 private static void TZif_GenerateAdjustmentRules(out AdjustmentRule[]? rules, TimeSpan baseUtcOffset, DateTime[] dts, byte[] typeOfLocalTime,
866 TZifType[] transitionType, bool[] StandardTime, bool[] GmtTime, string? futureTransitionsPosixFormat)
868 rules = null;
870 if (dts.Length > 0)
872 int index = 0;
873 List<AdjustmentRule> rulesList = new List<AdjustmentRule>();
875 while (index <= dts.Length)
877 TZif_GenerateAdjustmentRule(ref index, baseUtcOffset, rulesList, dts, typeOfLocalTime, transitionType, StandardTime, GmtTime, futureTransitionsPosixFormat);
880 rules = rulesList.ToArray();
881 if (rules != null && rules.Length == 0)
883 rules = null;
888 private static void TZif_GenerateAdjustmentRule(ref int index, TimeSpan timeZoneBaseUtcOffset, List<AdjustmentRule> rulesList, DateTime[] dts,
889 byte[] typeOfLocalTime, TZifType[] transitionTypes, bool[] StandardTime, bool[] GmtTime, string? futureTransitionsPosixFormat)
891 // To generate AdjustmentRules, use the following approach:
892 // The first AdjustmentRule will go from DateTime.MinValue to the first transition time greater than DateTime.MinValue.
893 // Each middle AdjustmentRule wil go from dts[index-1] to dts[index].
894 // The last AdjustmentRule will go from dts[dts.Length-1] to Datetime.MaxValue.
896 // 0. Skip any DateTime.MinValue transition times. In newer versions of the tzfile, there
897 // is a "big bang" transition time, which is before the year 0001. Since any times before year 0001
898 // cannot be represented by DateTime, there is no reason to make AdjustmentRules for these unrepresentable time periods.
899 // 1. If there are no DateTime.MinValue times, the first AdjustmentRule goes from DateTime.MinValue
900 // to the first transition and uses the first standard transitionType (or the first transitionType if none of them are standard)
901 // 2. Create an AdjustmentRule for each transition, i.e. from dts[index - 1] to dts[index].
902 // This rule uses the transitionType[index - 1] and the whole AdjustmentRule only describes a single offset - either
903 // all daylight savings, or all stanard time.
904 // 3. After all the transitions are filled out, the last AdjustmentRule is created from either:
905 // a. a POSIX-style timezone description ("futureTransitionsPosixFormat"), if there is one or
906 // b. continue the last transition offset until DateTime.Max
908 while (index < dts.Length && dts[index] == DateTime.MinValue)
910 index++;
913 if (rulesList.Count == 0 && index < dts.Length)
915 TZifType transitionType = TZif_GetEarlyDateTransitionType(transitionTypes);
916 DateTime endTransitionDate = dts[index];
918 TimeSpan transitionOffset = TZif_CalculateTransitionOffsetFromBase(transitionType.UtcOffset, timeZoneBaseUtcOffset);
919 TimeSpan daylightDelta = transitionType.IsDst ? transitionOffset : TimeSpan.Zero;
920 TimeSpan baseUtcDelta = transitionType.IsDst ? TimeSpan.Zero : transitionOffset;
922 AdjustmentRule r = AdjustmentRule.CreateAdjustmentRule(
923 DateTime.MinValue,
924 endTransitionDate.AddTicks(-1),
925 daylightDelta,
926 default(TransitionTime),
927 default(TransitionTime),
928 baseUtcDelta,
929 noDaylightTransitions: true);
931 if (!IsValidAdjustmentRuleOffest(timeZoneBaseUtcOffset, r))
933 NormalizeAdjustmentRuleOffset(timeZoneBaseUtcOffset, ref r);
936 rulesList.Add(r);
938 else if (index < dts.Length)
940 DateTime startTransitionDate = dts[index - 1];
941 TZifType startTransitionType = transitionTypes[typeOfLocalTime[index - 1]];
943 DateTime endTransitionDate = dts[index];
945 TimeSpan transitionOffset = TZif_CalculateTransitionOffsetFromBase(startTransitionType.UtcOffset, timeZoneBaseUtcOffset);
946 TimeSpan daylightDelta = startTransitionType.IsDst ? transitionOffset : TimeSpan.Zero;
947 TimeSpan baseUtcDelta = startTransitionType.IsDst ? TimeSpan.Zero : transitionOffset;
949 TransitionTime dstStart;
950 if (startTransitionType.IsDst)
952 // the TransitionTime fields are not used when AdjustmentRule.NoDaylightTransitions == true.
953 // However, there are some cases in the past where DST = true, and the daylight savings offset
954 // now equals what the current BaseUtcOffset is. In that case, the AdjustmentRule.DaylightOffset
955 // is going to be TimeSpan.Zero. But we still need to return 'true' from AdjustmentRule.HasDaylightSaving.
956 // To ensure we always return true from HasDaylightSaving, make a "special" dstStart that will make the logic
957 // in HasDaylightSaving return true.
958 dstStart = TransitionTime.CreateFixedDateRule(DateTime.MinValue.AddMilliseconds(2), 1, 1);
960 else
962 dstStart = default(TransitionTime);
965 AdjustmentRule r = AdjustmentRule.CreateAdjustmentRule(
966 startTransitionDate,
967 endTransitionDate.AddTicks(-1),
968 daylightDelta,
969 dstStart,
970 default(TransitionTime),
971 baseUtcDelta,
972 noDaylightTransitions: true);
974 if (!IsValidAdjustmentRuleOffest(timeZoneBaseUtcOffset, r))
976 NormalizeAdjustmentRuleOffset(timeZoneBaseUtcOffset, ref r);
979 rulesList.Add(r);
981 else
983 // create the AdjustmentRule that will be used for all DateTimes after the last transition
985 // NOTE: index == dts.Length
986 DateTime startTransitionDate = dts[index - 1];
988 if (!string.IsNullOrEmpty(futureTransitionsPosixFormat))
990 AdjustmentRule? r = TZif_CreateAdjustmentRuleForPosixFormat(futureTransitionsPosixFormat, startTransitionDate, timeZoneBaseUtcOffset);
992 if (r != null)
994 if (!IsValidAdjustmentRuleOffest(timeZoneBaseUtcOffset, r))
996 NormalizeAdjustmentRuleOffset(timeZoneBaseUtcOffset, ref r);
999 rulesList.Add(r);
1002 else
1004 // just use the last transition as the rule which will be used until the end of time
1006 TZifType transitionType = transitionTypes[typeOfLocalTime[index - 1]];
1007 TimeSpan transitionOffset = TZif_CalculateTransitionOffsetFromBase(transitionType.UtcOffset, timeZoneBaseUtcOffset);
1008 TimeSpan daylightDelta = transitionType.IsDst ? transitionOffset : TimeSpan.Zero;
1009 TimeSpan baseUtcDelta = transitionType.IsDst ? TimeSpan.Zero : transitionOffset;
1011 AdjustmentRule r = AdjustmentRule.CreateAdjustmentRule(
1012 startTransitionDate,
1013 DateTime.MaxValue,
1014 daylightDelta,
1015 default(TransitionTime),
1016 default(TransitionTime),
1017 baseUtcDelta,
1018 noDaylightTransitions: true);
1020 if (!IsValidAdjustmentRuleOffest(timeZoneBaseUtcOffset, r))
1022 NormalizeAdjustmentRuleOffset(timeZoneBaseUtcOffset, ref r);
1025 rulesList.Add(r);
1029 index++;
1032 private static TimeSpan TZif_CalculateTransitionOffsetFromBase(TimeSpan transitionOffset, TimeSpan timeZoneBaseUtcOffset)
1034 TimeSpan result = transitionOffset - timeZoneBaseUtcOffset;
1036 // TZif supports seconds-level granularity with offsets but TimeZoneInfo only supports minutes since it aligns
1037 // with DateTimeOffset, SQL Server, and the W3C XML Specification
1038 if (result.Ticks % TimeSpan.TicksPerMinute != 0)
1040 result = new TimeSpan(result.Hours, result.Minutes, 0);
1043 return result;
1046 /// <summary>
1047 /// Gets the first standard-time transition type, or simply the first transition type
1048 /// if there are no standard transition types.
1049 /// </summary>>
1050 /// <remarks>
1051 /// from 'man tzfile':
1052 /// localtime(3) uses the first standard-time ttinfo structure in the file
1053 /// (or simply the first ttinfo structure in the absence of a standard-time
1054 /// structure) if either tzh_timecnt is zero or the time argument is less
1055 /// than the first transition time recorded in the file.
1056 /// </remarks>
1057 private static TZifType TZif_GetEarlyDateTransitionType(TZifType[] transitionTypes)
1059 foreach (TZifType transitionType in transitionTypes)
1061 if (!transitionType.IsDst)
1063 return transitionType;
1067 if (transitionTypes.Length > 0)
1069 return transitionTypes[0];
1072 throw new InvalidTimeZoneException(SR.InvalidTimeZone_NoTTInfoStructures);
1075 /// <summary>
1076 /// Creates an AdjustmentRule given the POSIX TZ environment variable string.
1077 /// </summary>
1078 /// <remarks>
1079 /// See http://man7.org/linux/man-pages/man3/tzset.3.html for the format and semantics of this POSX string.
1080 /// </remarks>
1081 private static AdjustmentRule? TZif_CreateAdjustmentRuleForPosixFormat(string posixFormat, DateTime startTransitionDate, TimeSpan timeZoneBaseUtcOffset)
1083 if (TZif_ParsePosixFormat(posixFormat,
1084 out ReadOnlySpan<char> standardName,
1085 out ReadOnlySpan<char> standardOffset,
1086 out ReadOnlySpan<char> daylightSavingsName,
1087 out ReadOnlySpan<char> daylightSavingsOffset,
1088 out ReadOnlySpan<char> start,
1089 out ReadOnlySpan<char> startTime,
1090 out ReadOnlySpan<char> end,
1091 out ReadOnlySpan<char> endTime))
1093 // a valid posixFormat has at least standardName and standardOffset
1095 TimeSpan? parsedBaseOffset = TZif_ParseOffsetString(standardOffset);
1096 if (parsedBaseOffset.HasValue)
1098 TimeSpan baseOffset = parsedBaseOffset.GetValueOrDefault().Negate(); // offsets are backwards in POSIX notation
1099 baseOffset = TZif_CalculateTransitionOffsetFromBase(baseOffset, timeZoneBaseUtcOffset);
1101 // having a daylightSavingsName means there is a DST rule
1102 if (!daylightSavingsName.IsEmpty)
1104 TimeSpan? parsedDaylightSavings = TZif_ParseOffsetString(daylightSavingsOffset);
1105 TimeSpan daylightSavingsTimeSpan;
1106 if (!parsedDaylightSavings.HasValue)
1108 // default DST to 1 hour if it isn't specified
1109 daylightSavingsTimeSpan = new TimeSpan(1, 0, 0);
1111 else
1113 daylightSavingsTimeSpan = parsedDaylightSavings.GetValueOrDefault().Negate(); // offsets are backwards in POSIX notation
1114 daylightSavingsTimeSpan = TZif_CalculateTransitionOffsetFromBase(daylightSavingsTimeSpan, timeZoneBaseUtcOffset);
1115 daylightSavingsTimeSpan = TZif_CalculateTransitionOffsetFromBase(daylightSavingsTimeSpan, baseOffset);
1118 TransitionTime dstStart = TZif_CreateTransitionTimeFromPosixRule(start, startTime);
1119 TransitionTime dstEnd = TZif_CreateTransitionTimeFromPosixRule(end, endTime);
1121 return AdjustmentRule.CreateAdjustmentRule(
1122 startTransitionDate,
1123 DateTime.MaxValue,
1124 daylightSavingsTimeSpan,
1125 dstStart,
1126 dstEnd,
1127 baseOffset,
1128 noDaylightTransitions: false);
1130 else
1132 // if there is no daylightSavingsName, the whole AdjustmentRule should be with no transitions - just the baseOffset
1133 return AdjustmentRule.CreateAdjustmentRule(
1134 startTransitionDate,
1135 DateTime.MaxValue,
1136 TimeSpan.Zero,
1137 default(TransitionTime),
1138 default(TransitionTime),
1139 baseOffset,
1140 noDaylightTransitions: true);
1145 return null;
1148 private static TimeSpan? TZif_ParseOffsetString(ReadOnlySpan<char> offset)
1150 TimeSpan? result = null;
1152 if (offset.Length > 0)
1154 bool negative = offset[0] == '-';
1155 if (negative || offset[0] == '+')
1157 offset = offset.Slice(1);
1160 // Try parsing just hours first.
1161 // Note, TimeSpan.TryParseExact "%h" can't be used here because some time zones using values
1162 // like "26" or "144" and TimeSpan parsing would turn that into 26 or 144 *days* instead of hours.
1163 int hours;
1164 if (int.TryParse(offset, out hours))
1166 result = new TimeSpan(hours, 0, 0);
1168 else
1170 TimeSpan parsedTimeSpan;
1171 if (TimeSpan.TryParseExact(offset, "g", CultureInfo.InvariantCulture, out parsedTimeSpan))
1173 result = parsedTimeSpan;
1177 if (result.HasValue && negative)
1179 result = result.GetValueOrDefault().Negate();
1183 return result;
1186 private static DateTime ParseTimeOfDay(ReadOnlySpan<char> time)
1188 DateTime timeOfDay;
1189 TimeSpan? timeOffset = TZif_ParseOffsetString(time);
1190 if (timeOffset.HasValue)
1192 // This logic isn't correct and can't be corrected until https://github.com/dotnet/corefx/issues/2618 is fixed.
1193 // Some time zones use time values like, "26", "144", or "-2".
1194 // This allows the week to sometimes be week 4 and sometimes week 5 in the month.
1195 // For now, strip off any 'days' in the offset, and just get the time of day correct
1196 timeOffset = new TimeSpan(timeOffset.GetValueOrDefault().Hours, timeOffset.GetValueOrDefault().Minutes, timeOffset.GetValueOrDefault().Seconds);
1197 if (timeOffset.GetValueOrDefault() < TimeSpan.Zero)
1199 timeOfDay = new DateTime(1, 1, 2, 0, 0, 0);
1201 else
1203 timeOfDay = new DateTime(1, 1, 1, 0, 0, 0);
1206 timeOfDay += timeOffset.GetValueOrDefault();
1208 else
1210 // default to 2AM.
1211 timeOfDay = new DateTime(1, 1, 1, 2, 0, 0);
1214 return timeOfDay;
1217 private static TransitionTime TZif_CreateTransitionTimeFromPosixRule(ReadOnlySpan<char> date, ReadOnlySpan<char> time)
1219 if (date.IsEmpty)
1221 return default;
1224 if (date[0] == 'M')
1226 // Mm.w.d
1227 // This specifies day d of week w of month m. The day d must be between 0(Sunday) and 6.The week w must be between 1 and 5;
1228 // week 1 is the first week in which day d occurs, and week 5 specifies the last d day in the month. The month m should be between 1 and 12.
1230 int month;
1231 int week;
1232 DayOfWeek day;
1233 if (!TZif_ParseMDateRule(date, out month, out week, out day))
1235 throw new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_UnparseablePosixMDateString, date.ToString()));
1238 return TransitionTime.CreateFloatingDateRule(ParseTimeOfDay(time), month, week, day);
1240 else
1242 if (date[0] != 'J')
1244 // should be n Julian day format which we don't support.
1246 // This specifies the Julian day, with n between 0 and 365. February 29 is counted in leap years.
1248 // n would be a relative number from the begining of the year. which should handle if the
1249 // the year is a leap year or not.
1251 // In leap year, n would be counted as:
1253 // 0 30 31 59 60 90 335 365
1254 // |-------Jan--------|-------Feb--------|-------Mar--------|....|-------Dec--------|
1256 // while in non leap year we'll have
1258 // 0 30 31 58 59 89 334 364
1259 // |-------Jan--------|-------Feb--------|-------Mar--------|....|-------Dec--------|
1262 // For example if n is specified as 60, this means in leap year the rule will start at Mar 1,
1263 // while in non leap year the rule will start at Mar 2.
1265 // If we need to support n format, we'll have to have a floating adjustment rule support this case.
1267 throw new InvalidTimeZoneException(SR.InvalidTimeZone_NJulianDayNotSupported);
1270 // Julian day
1271 TZif_ParseJulianDay(date, out int month, out int day);
1272 return TransitionTime.CreateFixedDateRule(ParseTimeOfDay(time), month, day);
1276 /// <summary>
1277 /// Parses a string like Jn or n into month and day values.
1278 /// </summary>
1279 private static void TZif_ParseJulianDay(ReadOnlySpan<char> date, out int month, out int day)
1281 // Jn
1282 // This specifies the Julian day, with n between 1 and 365.February 29 is never counted, even in leap years.
1283 Debug.Assert(!date.IsEmpty);
1284 Debug.Assert(date[0] == 'J');
1285 month = day = 0;
1287 int index = 1;
1289 if (index >= date.Length || ((uint)(date[index] - '0') > '9'-'0'))
1291 throw new InvalidTimeZoneException(SR.InvalidTimeZone_InvalidJulianDay);
1294 int julianDay = 0;
1298 julianDay = julianDay * 10 + (int) (date[index] - '0');
1299 index++;
1300 } while (index < date.Length && ((uint)(date[index] - '0') <= '9'-'0'));
1302 int[] days = GregorianCalendarHelper.DaysToMonth365;
1304 if (julianDay == 0 || julianDay > days[days.Length - 1])
1306 throw new InvalidTimeZoneException(SR.InvalidTimeZone_InvalidJulianDay);
1309 int i = 1;
1310 while (i < days.Length && julianDay > days[i])
1312 i++;
1315 Debug.Assert(i > 0 && i < days.Length);
1317 month = i;
1318 day = julianDay - days[i - 1];
1321 /// <summary>
1322 /// Parses a string like Mm.w.d into month, week and DayOfWeek values.
1323 /// </summary>
1324 /// <returns>
1325 /// true if the parsing succeeded; otherwise, false.
1326 /// </returns>
1327 private static bool TZif_ParseMDateRule(ReadOnlySpan<char> dateRule, out int month, out int week, out DayOfWeek dayOfWeek)
1329 if (dateRule[0] == 'M')
1331 int monthWeekDotIndex = dateRule.IndexOf('.');
1332 if (monthWeekDotIndex > 0)
1334 ReadOnlySpan<char> weekDaySpan = dateRule.Slice(monthWeekDotIndex + 1);
1335 int weekDayDotIndex = weekDaySpan.IndexOf('.');
1336 if (weekDayDotIndex > 0)
1338 if (int.TryParse(dateRule.Slice(1, monthWeekDotIndex - 1), out month) &&
1339 int.TryParse(weekDaySpan.Slice(0, weekDayDotIndex), out week) &&
1340 int.TryParse(weekDaySpan.Slice(weekDayDotIndex + 1), out int day))
1342 dayOfWeek = (DayOfWeek)day;
1343 return true;
1349 month = 0;
1350 week = 0;
1351 dayOfWeek = default(DayOfWeek);
1352 return false;
1355 private static bool TZif_ParsePosixFormat(
1356 ReadOnlySpan<char> posixFormat,
1357 out ReadOnlySpan<char> standardName,
1358 out ReadOnlySpan<char> standardOffset,
1359 out ReadOnlySpan<char> daylightSavingsName,
1360 out ReadOnlySpan<char> daylightSavingsOffset,
1361 out ReadOnlySpan<char> start,
1362 out ReadOnlySpan<char> startTime,
1363 out ReadOnlySpan<char> end,
1364 out ReadOnlySpan<char> endTime)
1366 standardName = null;
1367 standardOffset = null;
1368 daylightSavingsName = null;
1369 daylightSavingsOffset = null;
1370 start = null;
1371 startTime = null;
1372 end = null;
1373 endTime = null;
1375 int index = 0;
1376 standardName = TZif_ParsePosixName(posixFormat, ref index);
1377 standardOffset = TZif_ParsePosixOffset(posixFormat, ref index);
1379 daylightSavingsName = TZif_ParsePosixName(posixFormat, ref index);
1380 if (!daylightSavingsName.IsEmpty)
1382 daylightSavingsOffset = TZif_ParsePosixOffset(posixFormat, ref index);
1384 if (index < posixFormat.Length && posixFormat[index] == ',')
1386 index++;
1387 TZif_ParsePosixDateTime(posixFormat, ref index, out start, out startTime);
1389 if (index < posixFormat.Length && posixFormat[index] == ',')
1391 index++;
1392 TZif_ParsePosixDateTime(posixFormat, ref index, out end, out endTime);
1397 return !standardName.IsEmpty && !standardOffset.IsEmpty;
1400 private static ReadOnlySpan<char> TZif_ParsePosixName(ReadOnlySpan<char> posixFormat, ref int index)
1402 bool isBracketEnclosed = index < posixFormat.Length && posixFormat[index] == '<';
1403 if (isBracketEnclosed)
1405 // move past the opening bracket
1406 index++;
1408 ReadOnlySpan<char> result = TZif_ParsePosixString(posixFormat, ref index, c => c == '>');
1410 // move past the closing bracket
1411 if (index < posixFormat.Length && posixFormat[index] == '>')
1413 index++;
1416 return result;
1418 else
1420 return TZif_ParsePosixString(
1421 posixFormat,
1422 ref index,
1423 c => char.IsDigit(c) || c == '+' || c == '-' || c == ',');
1427 private static ReadOnlySpan<char> TZif_ParsePosixOffset(ReadOnlySpan<char> posixFormat, ref int index) =>
1428 TZif_ParsePosixString(posixFormat, ref index, c => !char.IsDigit(c) && c != '+' && c != '-' && c != ':');
1430 private static void TZif_ParsePosixDateTime(ReadOnlySpan<char> posixFormat, ref int index, out ReadOnlySpan<char> date, out ReadOnlySpan<char> time)
1432 time = null;
1434 date = TZif_ParsePosixDate(posixFormat, ref index);
1435 if (index < posixFormat.Length && posixFormat[index] == '/')
1437 index++;
1438 time = TZif_ParsePosixTime(posixFormat, ref index);
1442 private static ReadOnlySpan<char> TZif_ParsePosixDate(ReadOnlySpan<char> posixFormat, ref int index) =>
1443 TZif_ParsePosixString(posixFormat, ref index, c => c == '/' || c == ',');
1445 private static ReadOnlySpan<char> TZif_ParsePosixTime(ReadOnlySpan<char> posixFormat, ref int index) =>
1446 TZif_ParsePosixString(posixFormat, ref index, c => c == ',');
1448 private static ReadOnlySpan<char> TZif_ParsePosixString(ReadOnlySpan<char> posixFormat, ref int index, Func<char, bool> breakCondition)
1450 int startIndex = index;
1451 for (; index < posixFormat.Length; index++)
1453 char current = posixFormat[index];
1454 if (breakCondition(current))
1456 break;
1460 return posixFormat.Slice(startIndex, index - startIndex);
1463 // Returns the Substring from zoneAbbreviations starting at index and ending at '\0'
1464 // zoneAbbreviations is expected to be in the form: "PST\0PDT\0PWT\0\PPT"
1465 private static string TZif_GetZoneAbbreviation(string zoneAbbreviations, int index)
1467 int lastIndex = zoneAbbreviations.IndexOf('\0', index);
1468 return lastIndex > 0 ?
1469 zoneAbbreviations.Substring(index, lastIndex - index) :
1470 zoneAbbreviations.Substring(index);
1473 // Converts an array of bytes into an int - always using standard byte order (Big Endian)
1474 // per TZif file standard
1475 private static unsafe int TZif_ToInt32(byte[] value, int startIndex)
1477 fixed (byte* pbyte = &value[startIndex])
1479 return (*pbyte << 24) | (*(pbyte + 1) << 16) | (*(pbyte + 2) << 8) | (*(pbyte + 3));
1483 // Converts an array of bytes into a long - always using standard byte order (Big Endian)
1484 // per TZif file standard
1485 private static unsafe long TZif_ToInt64(byte[] value, int startIndex)
1487 fixed (byte* pbyte = &value[startIndex])
1489 int i1 = (*pbyte << 24) | (*(pbyte + 1) << 16) | (*(pbyte + 2) << 8) | (*(pbyte + 3));
1490 int i2 = (*(pbyte + 4) << 24) | (*(pbyte + 5) << 16) | (*(pbyte + 6) << 8) | (*(pbyte + 7));
1491 return (uint)i2 | ((long)i1 << 32);
1495 private static long TZif_ToUnixTime(byte[] value, int startIndex, TZVersion version) =>
1496 version != TZVersion.V1 ?
1497 TZif_ToInt64(value, startIndex) :
1498 TZif_ToInt32(value, startIndex);
1500 private static DateTime TZif_UnixTimeToDateTime(long unixTime) =>
1501 unixTime < DateTimeOffset.UnixMinSeconds ? DateTime.MinValue :
1502 unixTime > DateTimeOffset.UnixMaxSeconds ? DateTime.MaxValue :
1503 DateTimeOffset.FromUnixTimeSeconds(unixTime).UtcDateTime;
1505 private static void TZif_ParseRaw(byte[] data, out TZifHead t, out DateTime[] dts, out byte[] typeOfLocalTime, out TZifType[] transitionType,
1506 out string zoneAbbreviations, out bool[] StandardTime, out bool[] GmtTime, out string? futureTransitionsPosixFormat)
1508 // initialize the out parameters in case the TZifHead ctor throws
1509 dts = null!;
1510 typeOfLocalTime = null!;
1511 transitionType = null!;
1512 zoneAbbreviations = string.Empty;
1513 StandardTime = null!;
1514 GmtTime = null!;
1515 futureTransitionsPosixFormat = null;
1517 // read in the 44-byte TZ header containing the count/length fields
1519 int index = 0;
1520 t = new TZifHead(data, index);
1521 index += TZifHead.Length;
1523 int timeValuesLength = 4; // the first version uses 4-bytes to specify times
1524 if (t.Version != TZVersion.V1)
1526 // move index past the V1 information to read the V2 information
1527 index += (int)((timeValuesLength * t.TimeCount) + t.TimeCount + (6 * t.TypeCount) + ((timeValuesLength + 4) * t.LeapCount) + t.IsStdCount + t.IsGmtCount + t.CharCount);
1529 // read the V2 header
1530 t = new TZifHead(data, index);
1531 index += TZifHead.Length;
1532 timeValuesLength = 8; // the second version uses 8-bytes
1535 // initialize the containers for the rest of the TZ data
1536 dts = new DateTime[t.TimeCount];
1537 typeOfLocalTime = new byte[t.TimeCount];
1538 transitionType = new TZifType[t.TypeCount];
1539 zoneAbbreviations = string.Empty;
1540 StandardTime = new bool[t.TypeCount];
1541 GmtTime = new bool[t.TypeCount];
1543 // read in the UTC transition points and convert them to Windows
1545 for (int i = 0; i < t.TimeCount; i++)
1547 long unixTime = TZif_ToUnixTime(data, index, t.Version);
1548 dts[i] = TZif_UnixTimeToDateTime(unixTime);
1549 index += timeValuesLength;
1552 // read in the Type Indices; there is a 1:1 mapping of UTC transition points to Type Indices
1553 // these indices directly map to the array index in the transitionType array below
1555 for (int i = 0; i < t.TimeCount; i++)
1557 typeOfLocalTime[i] = data[index];
1558 index += 1;
1561 // read in the Type table. Each 6-byte entry represents
1562 // {UtcOffset, IsDst, AbbreviationIndex}
1564 // each AbbreviationIndex is a character index into the zoneAbbreviations string below
1566 for (int i = 0; i < t.TypeCount; i++)
1568 transitionType[i] = new TZifType(data, index);
1569 index += 6;
1572 // read in the Abbreviation ASCII string. This string will be in the form:
1573 // "PST\0PDT\0PWT\0\PPT"
1575 Encoding enc = Encoding.UTF8;
1576 zoneAbbreviations = enc.GetString(data, index, (int)t.CharCount);
1577 index += (int)t.CharCount;
1579 // skip ahead of the Leap-Seconds Adjustment data. In a future release, consider adding
1580 // support for Leap-Seconds
1582 index += (int)(t.LeapCount * (timeValuesLength + 4)); // skip the leap second transition times
1584 // read in the Standard Time table. There should be a 1:1 mapping between Type-Index and Standard
1585 // Time table entries.
1587 // TRUE = transition time is standard time
1588 // FALSE = transition time is wall clock time
1589 // ABSENT = transition time is wall clock time
1591 for (int i = 0; i < t.IsStdCount && i < t.TypeCount && index < data.Length; i++)
1593 StandardTime[i] = (data[index++] != 0);
1596 // read in the GMT Time table. There should be a 1:1 mapping between Type-Index and GMT Time table
1597 // entries.
1599 // TRUE = transition time is UTC
1600 // FALSE = transition time is local time
1601 // ABSENT = transition time is local time
1603 for (int i = 0; i < t.IsGmtCount && i < t.TypeCount && index < data.Length; i++)
1605 GmtTime[i] = (data[index++] != 0);
1608 if (t.Version != TZVersion.V1)
1610 // read the POSIX-style format, which should be wrapped in newlines with the last newline at the end of the file
1611 if (data[index++] == '\n' && data[data.Length - 1] == '\n')
1613 futureTransitionsPosixFormat = enc.GetString(data, index, data.Length - index - 1);
1618 private struct TZifType
1620 public const int Length = 6;
1622 public readonly TimeSpan UtcOffset;
1623 public readonly bool IsDst;
1624 public readonly byte AbbreviationIndex;
1626 public TZifType(byte[] data, int index)
1628 if (data == null || data.Length < index + Length)
1630 throw new ArgumentException(SR.Argument_TimeZoneInfoInvalidTZif, nameof(data));
1632 UtcOffset = new TimeSpan(0, 0, TZif_ToInt32(data, index + 00));
1633 IsDst = (data[index + 4] != 0);
1634 AbbreviationIndex = data[index + 5];
1638 private struct TZifHead
1640 public const int Length = 44;
1642 public readonly uint Magic; // TZ_MAGIC "TZif"
1643 public readonly TZVersion Version; // 1 byte for a \0 or 2 or 3
1644 // public byte[15] Reserved; // reserved for future use
1645 public readonly uint IsGmtCount; // number of transition time flags
1646 public readonly uint IsStdCount; // number of transition time flags
1647 public readonly uint LeapCount; // number of leap seconds
1648 public readonly uint TimeCount; // number of transition times
1649 public readonly uint TypeCount; // number of local time types
1650 public readonly uint CharCount; // number of abbreviated characters
1652 public TZifHead(byte[] data, int index)
1654 if (data == null || data.Length < Length)
1656 throw new ArgumentException("bad data", nameof(data));
1659 Magic = (uint)TZif_ToInt32(data, index + 00);
1661 if (Magic != 0x545A6966)
1663 // 0x545A6966 = {0x54, 0x5A, 0x69, 0x66} = "TZif"
1664 throw new ArgumentException(SR.Argument_TimeZoneInfoBadTZif, nameof(data));
1667 byte version = data[index + 04];
1668 Version =
1669 version == '2' ? TZVersion.V2 :
1670 version == '3' ? TZVersion.V3 :
1671 TZVersion.V1; // default/fallback to V1 to guard against future, unsupported version numbers
1673 // skip the 15 byte reserved field
1675 // don't use the BitConverter class which parses data
1676 // based on the Endianess of the machine architecture.
1677 // this data is expected to always be in "standard byte order",
1678 // regardless of the machine it is being processed on.
1680 IsGmtCount = (uint)TZif_ToInt32(data, index + 20);
1681 IsStdCount = (uint)TZif_ToInt32(data, index + 24);
1682 LeapCount = (uint)TZif_ToInt32(data, index + 28);
1683 TimeCount = (uint)TZif_ToInt32(data, index + 32);
1684 TypeCount = (uint)TZif_ToInt32(data, index + 36);
1685 CharCount = (uint)TZif_ToInt32(data, index + 40);
1689 private enum TZVersion : byte
1691 V1 = 0,
1694 // when adding more versions, ensure all the logic using TZVersion is still correct