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.
6 using System
.Collections
.Generic
;
7 using System
.Diagnostics
;
8 using System
.Diagnostics
.CodeAnalysis
;
9 using System
.Globalization
;
12 using System
.Threading
;
13 using System
.Security
;
14 using System
.Runtime
.CompilerServices
;
15 using System
.Runtime
.InteropServices
;
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
)
32 byte[] typeOfLocalTime
;
33 TZifType
[] transitionType
;
34 string zoneAbbreviations
;
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
);
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
);
60 _daylightDisplayName
= TZif_GetZoneAbbreviation(zoneAbbreviations
, transitionType
[type
].AbbreviationIndex
);
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
);
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}";
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);
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
;
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
,
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.
137 displayName
= timeZoneDisplayName
;
142 /// Returns a cloned array of AdjustmentRule objects
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
) :
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
) :
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
);
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
192 /// Helper function for retrieving the local system time zone.
193 /// May throw COMException, TimeZoneNotFoundException, InvalidTimeZoneException.
194 /// Assumes cachedData lock is taken.
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
)
210 string timeZoneDirectory
= GetTimeZoneDirectory();
211 string timeZoneFilePath
= Path
.Combine(timeZoneDirectory
, id
);
215 rawData
= File
.ReadAllBytes(timeZoneFilePath
);
217 catch (UnauthorizedAccessException ex
)
220 return TimeZoneInfoResult
.SecurityException
;
222 catch (FileNotFoundException ex
)
225 return TimeZoneInfoResult
.TimeZoneNotFoundException
;
227 catch (DirectoryNotFoundException 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
);
242 e
= new InvalidTimeZoneException(SR
.Format(SR
.InvalidTimeZone_InvalidFileData
, id
, timeZoneFilePath
));
243 return TimeZoneInfoResult
.InvalidTimeZoneException
;
246 return TimeZoneInfoResult
.Success
;
250 /// Returns a collection of TimeZone Id values from the zone.tab file in the timeZoneDirectory.
253 /// Lines that start with # are comments and are skipped.
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)
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
);
286 timeZoneId
= zoneTabFileLine
.Substring(startIndex
);
289 if (!string.IsNullOrEmpty(timeZoneId
))
291 timeZoneIds
.Add(timeZoneId
);
299 catch (IOException
) { }
300 catch (UnauthorizedAccessException
) { }
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.
312 private static bool TryGetLocalTzFile([NotNullWhen(true)] out byte[]? rawData
, [NotNullWhen(true)] out string? id
)
316 string? tzVariable
= GetTzEnvironmentVariable();
318 // If the env var is null, use the localtime file
319 if (tzVariable
== null)
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)
332 // Otherwise, use the path from the env var. If it's not absolute, make it relative
333 // to the system timezone directory
335 if (tzVariable
[0] != '/')
338 tzFilePath
= Path
.Combine(GetTimeZoneDirectory(), tzVariable
);
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);
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
);
380 catch (IOException
) { }
381 catch (SecurityException
) { }
382 catch (UnauthorizedAccessException
) { }
388 /// Finds the time zone id by using 'readlink' on the path to see if tzFilePath is
389 /// a symlink to a file.
391 private static string? FindTimeZoneIdUsingReadLink(string tzFilePath
)
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
);
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] == '.'))
420 return Path
.Join(currentPath
.AsSpan(), direntName
);
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
)
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)
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.
460 if (dirent
.InodeType
== Interop
.Sys
.NodeType
.DT_DIR
)
462 // We know it's a directory.
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
;
482 // Otherwise, treat it as a file. This includes regular files, FIFOs, etc.
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.
491 if (toExplore
== null)
493 toExplore
= new List
<string>();
495 toExplore
.Add(fullPath
);
497 else if (condition(fullPath
))
505 if (dirHandle
!= IntPtr
.Zero
)
506 Interop
.Sys
.CloseDir(dirHandle
);
509 if (toExplore
== null || toExplore
.Count
== 0)
512 currentPath
= toExplore
[toExplore
.Count
- 1];
513 toExplore
.RemoveAt(toExplore
.Count
- 1);
519 if (dirBuffer
!= null)
520 ArrayPool
<byte>.Shared
.Return(dirBuffer
);
525 /// Find the time zone id by searching all the tzfiles for the one that matches rawData
526 /// and return its file name.
528 private static string FindTimeZoneId(byte[] rawData
)
530 // default to "Local" if we can't find the right tzfile
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
550 // strip off the root time zone directory
551 if (id
.StartsWith(timeZoneDirectory
, StringComparison
.Ordinal
))
553 id
= id
.Substring(timeZoneDirectory
.Length
);
561 catch (IOException
) { }
562 catch (SecurityException
) { }
563 catch (UnauthorizedAccessException
) { }
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
)
578 int count
= rawData
.Length
;
582 int n
= stream
.Read(buffer
, index
, count
);
584 throw Error
.GetEndOfFile();
587 for (; index
< end
; index
++)
589 if (buffer
[index
] != rawData
[index
])
602 catch (IOException
) { }
603 catch (SecurityException
) { }
604 catch (UnauthorizedAccessException
) { }
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.
615 private static TimeZoneInfo
GetLocalTimeZoneFromTzFile()
619 if (TryGetLocalTzFile(out rawData
, out id
))
621 TimeZoneInfo
? result
= GetTimeZoneFromTzData(rawData
, id
);
628 // if we can't find a local time zone, return UTC
632 private static TimeZoneInfo
? GetTimeZoneFromTzData(byte[]? rawData
, string id
)
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
) { }
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
;
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'.
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
))
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
));
699 TimeZoneInfoResult result
;
701 CachedData cachedData
= s_cachedData
;
705 result
= TryGetTimeZone(id
, false, out value, out e
, cachedData
, alwaysFallbackToLocalMachine
: true);
708 if (result
== TimeZoneInfoResult
.Success
)
712 else if (result
== TimeZoneInfoResult
.InvalidTimeZoneException
)
714 Debug
.Assert(e
is InvalidTimeZoneException
,
715 "TryGetTimeZone must create an InvalidTimeZoneException when it returns TimeZoneInfoResult.InvalidTimeZoneException");
718 else if (result
== TimeZoneInfoResult
.SecurityException
)
720 throw new SecurityException(SR
.Format(SR
.Security_CannotReadFileData
, id
), e
);
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)
739 // tzfile -- timezone information
742 // #include "/usr/src/lib/libc/stdtime/tzfile.h"
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
754 // tzh_leapcnt The number of leap seconds for which data is stored in
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:
776 // unsigned int tt_abbrind;
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
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.
811 // ctime(3), time2posix(3), zic(8)
813 // BSD September 13, 1994 BSD
817 // TIME(3) BSD Library Functions Manual TIME(3)
820 // time -- get time of day
823 // Standard C Library (libc, -lc)
829 // time(time_t *tloc);
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
837 // The return value is also stored in *tloc, provided that tloc is non-null.
840 // The time() function may fail for any of the reasons described in
844 // gettimeofday(2), ctime(3)
847 // The time function conforms to IEEE Std 1003.1-2001 (``POSIX.1'').
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
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
)
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)
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
)
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(
924 endTransitionDate
.AddTicks(-1),
926 default(TransitionTime
),
927 default(TransitionTime
),
929 noDaylightTransitions: true);
931 if (!IsValidAdjustmentRuleOffest(timeZoneBaseUtcOffset
, r
))
933 NormalizeAdjustmentRuleOffset(timeZoneBaseUtcOffset
, ref 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);
962 dstStart
= default(TransitionTime
);
965 AdjustmentRule r
= AdjustmentRule
.CreateAdjustmentRule(
967 endTransitionDate
.AddTicks(-1),
970 default(TransitionTime
),
972 noDaylightTransitions: true);
974 if (!IsValidAdjustmentRuleOffest(timeZoneBaseUtcOffset
, r
))
976 NormalizeAdjustmentRuleOffset(timeZoneBaseUtcOffset
, ref r
);
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
);
994 if (!IsValidAdjustmentRuleOffest(timeZoneBaseUtcOffset
, r
))
996 NormalizeAdjustmentRuleOffset(timeZoneBaseUtcOffset
, ref r
);
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
,
1015 default(TransitionTime
),
1016 default(TransitionTime
),
1018 noDaylightTransitions: true);
1020 if (!IsValidAdjustmentRuleOffest(timeZoneBaseUtcOffset
, r
))
1022 NormalizeAdjustmentRuleOffset(timeZoneBaseUtcOffset
, ref r
);
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);
1047 /// Gets the first standard-time transition type, or simply the first transition type
1048 /// if there are no standard transition types.
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.
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
);
1076 /// Creates an AdjustmentRule given the POSIX TZ environment variable string.
1079 /// See http://man7.org/linux/man-pages/man3/tzset.3.html for the format and semantics of this POSX string.
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);
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
,
1124 daylightSavingsTimeSpan
,
1128 noDaylightTransitions: false);
1132 // if there is no daylightSavingsName, the whole AdjustmentRule should be with no transitions - just the baseOffset
1133 return AdjustmentRule
.CreateAdjustmentRule(
1134 startTransitionDate
,
1137 default(TransitionTime
),
1138 default(TransitionTime
),
1140 noDaylightTransitions: true);
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.
1164 if (int.TryParse(offset
, out hours
))
1166 result
= new TimeSpan(hours
, 0, 0);
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();
1186 private static DateTime
ParseTimeOfDay(ReadOnlySpan
<char> time
)
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);
1203 timeOfDay
= new DateTime(1, 1, 1, 0, 0, 0);
1206 timeOfDay
+= timeOffset
.GetValueOrDefault();
1211 timeOfDay
= new DateTime(1, 1, 1, 2, 0, 0);
1217 private static TransitionTime
TZif_CreateTransitionTimeFromPosixRule(ReadOnlySpan
<char> date
, ReadOnlySpan
<char> time
)
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.
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
);
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
);
1271 TZif_ParseJulianDay(date
, out int month
, out int day
);
1272 return TransitionTime
.CreateFixedDateRule(ParseTimeOfDay(time
), month
, day
);
1277 /// Parses a string like Jn or n into month and day values.
1279 private static void TZif_ParseJulianDay(ReadOnlySpan
<char> date
, out int month
, out int day
)
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');
1289 if (index
>= date
.Length
|| ((uint)(date
[index
] - '0') > '9'-'0'))
1291 throw new InvalidTimeZoneException(SR
.InvalidTimeZone_InvalidJulianDay
);
1298 julianDay
= julianDay
* 10 + (int) (date
[index
] - '0');
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
);
1310 while (i
< days
.Length
&& julianDay
> days
[i
])
1315 Debug
.Assert(i
> 0 && i
< days
.Length
);
1318 day
= julianDay
- days
[i
- 1];
1322 /// Parses a string like Mm.w.d into month, week and DayOfWeek values.
1325 /// true if the parsing succeeded; otherwise, false.
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
;
1351 dayOfWeek
= default(DayOfWeek
);
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;
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
] == ',')
1387 TZif_ParsePosixDateTime(posixFormat
, ref index
, out start
, out startTime
);
1389 if (index
< posixFormat
.Length
&& posixFormat
[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
1408 ReadOnlySpan
<char> result
= TZif_ParsePosixString(posixFormat
, ref index
, c
=> c
== '>');
1410 // move past the closing bracket
1411 if (index
< posixFormat
.Length
&& posixFormat
[index
] == '>')
1420 return TZif_ParsePosixString(
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
)
1434 date
= TZif_ParsePosixDate(posixFormat
, ref index
);
1435 if (index
< posixFormat
.Length
&& posixFormat
[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
))
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
1510 typeOfLocalTime
= null!;
1511 transitionType
= null!;
1512 zoneAbbreviations
= string.Empty
;
1513 StandardTime
= null!;
1515 futureTransitionsPosixFormat
= null;
1517 // read in the 44-byte TZ header containing the count/length fields
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
];
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
);
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
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];
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
1694 // when adding more versions, ensure all the logic using TZVersion is still correct