// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;
using System.Security;
namespace System
{
public sealed partial class TimeZoneInfo
{
private const string DefaultTimeZoneDirectory = "/usr/share/zoneinfo/";
private const string ZoneTabFileName = "zone.tab";
private const string TimeZoneEnvironmentVariable = "TZ";
private const string TimeZoneDirectoryEnvironmentVariable = "TZDIR";
private TimeZoneInfo(byte[] data, string id, bool dstDisabled)
{
TZifHead t;
DateTime[] dts;
byte[] typeOfLocalTime;
TZifType[] transitionType;
string zoneAbbreviations;
bool[] StandardTime;
bool[] GmtTime;
string futureTransitionsPosixFormat;
// parse the raw TZif bytes; this method can throw ArgumentException when the data is malformed.
TZif_ParseRaw(data, out t, out dts, out typeOfLocalTime, out transitionType, out zoneAbbreviations, out StandardTime, out GmtTime, out futureTransitionsPosixFormat);
_id = id;
_displayName = LocalId;
_baseUtcOffset = TimeSpan.Zero;
// find the best matching baseUtcOffset and display strings based on the current utcNow value.
// NOTE: read the display strings from the the tzfile now in case they can't be loaded later
// from the globalization data.
DateTime utcNow = DateTime.UtcNow;
for (int i = 0; i < dts.Length && dts[i] <= utcNow; i++)
{
int type = typeOfLocalTime[i];
if (!transitionType[type].IsDst)
{
_baseUtcOffset = transitionType[type].UtcOffset;
_standardDisplayName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[type].AbbreviationIndex);
}
else
{
_daylightDisplayName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[type].AbbreviationIndex);
}
}
if (dts.Length == 0)
{
// time zones like Africa/Bujumbura and Etc/GMT* have no transition times but still contain
// TZifType entries that may contain a baseUtcOffset and display strings
for (int i = 0; i < transitionType.Length; i++)
{
if (!transitionType[i].IsDst)
{
_baseUtcOffset = transitionType[i].UtcOffset;
_standardDisplayName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[i].AbbreviationIndex);
}
else
{
_daylightDisplayName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[i].AbbreviationIndex);
}
}
}
_displayName = _standardDisplayName;
GetDisplayName(Interop.GlobalizationInterop.TimeZoneDisplayNameType.Generic, ref _displayName);
GetDisplayName(Interop.GlobalizationInterop.TimeZoneDisplayNameType.Standard, ref _standardDisplayName);
GetDisplayName(Interop.GlobalizationInterop.TimeZoneDisplayNameType.DaylightSavings, ref _daylightDisplayName);
// TZif supports seconds-level granularity with offsets but TimeZoneInfo only supports minutes since it aligns
// with DateTimeOffset, SQL Server, and the W3C XML Specification
if (_baseUtcOffset.Ticks % TimeSpan.TicksPerMinute != 0)
{
_baseUtcOffset = new TimeSpan(_baseUtcOffset.Hours, _baseUtcOffset.Minutes, 0);
}
if (!dstDisabled)
{
// only create the adjustment rule if DST is enabled
TZif_GenerateAdjustmentRules(out _adjustmentRules, _baseUtcOffset, dts, typeOfLocalTime, transitionType, StandardTime, GmtTime, futureTransitionsPosixFormat);
}
ValidateTimeZoneInfo(_id, _baseUtcOffset, _adjustmentRules, out _supportsDaylightSavingTime);
}
private void GetDisplayName(Interop.GlobalizationInterop.TimeZoneDisplayNameType nameType, ref string displayName)
{
if (GlobalizationMode.Invariant)
{
displayName = _standardDisplayName;
return;
}
string timeZoneDisplayName;
bool result = Interop.CallStringMethod(
(locale, id, type, stringBuilder) => Interop.GlobalizationInterop.GetTimeZoneDisplayName(
locale,
id,
type,
stringBuilder,
stringBuilder.Capacity),
CultureInfo.CurrentUICulture.Name,
_id,
nameType,
out timeZoneDisplayName);
// If there is an unknown error, don't set the displayName field.
// It will be set to the abbreviation that was read out of the tzfile.
if (result)
{
displayName = timeZoneDisplayName;
}
}
///
/// Returns a cloned array of AdjustmentRule objects
///
public AdjustmentRule[] GetAdjustmentRules()
{
if (_adjustmentRules == null)
{
return Array.Empty();
}
// The rules we use in Unix care mostly about the start and end dates but don't fill the transition start and end info.
// as the rules now is public, we should fill it properly so the caller doesn't have to know how we use it internally
// and can use it as it is used in Windows
AdjustmentRule[] rules = new AdjustmentRule[_adjustmentRules.Length];
for (int i = 0; i < _adjustmentRules.Length; i++)
{
var rule = _adjustmentRules[i];
var start = rule.DateStart.Kind == DateTimeKind.Utc ?
// At the daylight start we didn't start the daylight saving yet then we convert to Local time
// by adding the _baseUtcOffset to the UTC time
new DateTime(rule.DateStart.Ticks + _baseUtcOffset.Ticks, DateTimeKind.Unspecified) :
rule.DateStart;
var end = rule.DateEnd.Kind == DateTimeKind.Utc ?
// At the daylight saving end, the UTC time is mapped to local time which is already shifted by the daylight delta
// we calculate the local time by adding _baseUtcOffset + DaylightDelta to the UTC time
new DateTime(rule.DateEnd.Ticks + _baseUtcOffset.Ticks + rule.DaylightDelta.Ticks, DateTimeKind.Unspecified) :
rule.DateEnd;
var startTransition = TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, start.Hour, start.Minute, start.Second), start.Month, start.Day);
var endTransition = TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, end.Hour, end.Minute, end.Second), end.Month, end.Day);
rules[i] = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(start.Date, end.Date, rule.DaylightDelta, startTransition, endTransition);
}
return rules;
}
private static void PopulateAllSystemTimeZones(CachedData cachedData)
{
Debug.Assert(Monitor.IsEntered(cachedData));
string timeZoneDirectory = GetTimeZoneDirectory();
foreach (string timeZoneId in GetTimeZoneIds(timeZoneDirectory))
{
TimeZoneInfo value;
Exception ex;
TryGetTimeZone(timeZoneId, false, out value, out ex, cachedData, alwaysFallbackToLocalMachine: true); // populate the cache
}
}
///
/// Helper function for retrieving the local system time zone.
/// May throw COMException, TimeZoneNotFoundException, InvalidTimeZoneException.
/// Assumes cachedData lock is taken.
///
/// A new TimeZoneInfo instance.
private static TimeZoneInfo GetLocalTimeZone(CachedData cachedData)
{
Debug.Assert(Monitor.IsEntered(cachedData));
// Without Registry support, create the TimeZoneInfo from a TZ file
return GetLocalTimeZoneFromTzFile();
}
private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out TimeZoneInfo value, out Exception e)
{
value = null;
e = null;
string timeZoneDirectory = GetTimeZoneDirectory();
string timeZoneFilePath = Path.Combine(timeZoneDirectory, id);
byte[] rawData;
try
{
rawData = File.ReadAllBytes(timeZoneFilePath);
}
catch (UnauthorizedAccessException ex)
{
e = ex;
return TimeZoneInfoResult.SecurityException;
}
catch (FileNotFoundException ex)
{
e = ex;
return TimeZoneInfoResult.TimeZoneNotFoundException;
}
catch (DirectoryNotFoundException ex)
{
e = ex;
return TimeZoneInfoResult.TimeZoneNotFoundException;
}
catch (IOException ex)
{
e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, timeZoneFilePath), ex);
return TimeZoneInfoResult.InvalidTimeZoneException;
}
value = GetTimeZoneFromTzData(rawData, id);
if (value == null)
{
e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, timeZoneFilePath));
return TimeZoneInfoResult.InvalidTimeZoneException;
}
return TimeZoneInfoResult.Success;
}
///
/// Returns a collection of TimeZone Id values from the zone.tab file in the timeZoneDirectory.
///
///
/// Lines that start with # are comments and are skipped.
///
private static List GetTimeZoneIds(string timeZoneDirectory)
{
string[] zoneTabFileLines = null;
try
{
zoneTabFileLines = File.ReadAllLines(Path.Combine(timeZoneDirectory, ZoneTabFileName));
}
catch (IOException) { }
catch (UnauthorizedAccessException) { }
if (zoneTabFileLines == null)
{
return new List();
}
List timeZoneIds = new List(zoneTabFileLines.Length);
foreach (string zoneTabFileLine in zoneTabFileLines)
{
if (!string.IsNullOrEmpty(zoneTabFileLine) && zoneTabFileLine[0] != '#')
{
// the format of the line is "country-code \t coordinates \t TimeZone Id \t comments"
int firstTabIndex = zoneTabFileLine.IndexOf('\t');
if (firstTabIndex != -1)
{
int secondTabIndex = zoneTabFileLine.IndexOf('\t', firstTabIndex + 1);
if (secondTabIndex != -1)
{
string timeZoneId;
int startIndex = secondTabIndex + 1;
int thirdTabIndex = zoneTabFileLine.IndexOf('\t', startIndex);
if (thirdTabIndex != -1)
{
int length = thirdTabIndex - startIndex;
timeZoneId = zoneTabFileLine.Substring(startIndex, length);
}
else
{
timeZoneId = zoneTabFileLine.Substring(startIndex);
}
if (!string.IsNullOrEmpty(timeZoneId))
{
timeZoneIds.Add(timeZoneId);
}
}
}
}
}
return timeZoneIds;
}
///
/// Gets the tzfile raw data for the current 'local' time zone using the following rules.
/// 1. Read the TZ environment variable. If it is set, use it.
/// 2. Look for the data in /etc/localtime.
/// 3. Look for the data in GetTimeZoneDirectory()/localtime.
/// 4. Use UTC if all else fails.
///
private static bool TryGetLocalTzFile(out byte[] rawData, out string id)
{
rawData = null;
id = null;
string tzVariable = GetTzEnvironmentVariable();
// If the env var is null, use the localtime file
if (tzVariable == null)
{
return
TryLoadTzFile("/etc/localtime", ref rawData, ref id) ||
TryLoadTzFile(Path.Combine(GetTimeZoneDirectory(), "localtime"), ref rawData, ref id);
}
// If it's empty, use UTC (TryGetLocalTzFile() should return false).
if (tzVariable.Length == 0)
{
return false;
}
// Otherwise, use the path from the env var. If it's not absolute, make it relative
// to the system timezone directory
string tzFilePath;
if (tzVariable[0] != '/')
{
id = tzVariable;
tzFilePath = Path.Combine(GetTimeZoneDirectory(), tzVariable);
}
else
{
tzFilePath = tzVariable;
}
return TryLoadTzFile(tzFilePath, ref rawData, ref id);
}
private static string GetTzEnvironmentVariable()
{
string result = Environment.GetEnvironmentVariable(TimeZoneEnvironmentVariable);
if (!string.IsNullOrEmpty(result))
{
if (result[0] == ':')
{
// strip off the ':' prefix
result = result.Substring(1);
}
}
return result;
}
private static bool TryLoadTzFile(string tzFilePath, ref byte[] rawData, ref string id)
{
if (File.Exists(tzFilePath))
{
try
{
rawData = File.ReadAllBytes(tzFilePath);
if (string.IsNullOrEmpty(id))
{
id = FindTimeZoneIdUsingReadLink(tzFilePath);
if (string.IsNullOrEmpty(id))
{
id = FindTimeZoneId(rawData);
}
}
return true;
}
catch (IOException) { }
catch (SecurityException) { }
catch (UnauthorizedAccessException) { }
}
return false;
}
///
/// Finds the time zone id by using 'readlink' on the path to see if tzFilePath is
/// a symlink to a file.
///
private static string FindTimeZoneIdUsingReadLink(string tzFilePath)
{
string id = null;
StringBuilder symlinkPathBuilder = StringBuilderCache.Acquire(Path.MaxPath);
bool result = Interop.GlobalizationInterop.ReadLink(tzFilePath, symlinkPathBuilder, (uint)symlinkPathBuilder.Capacity);
if (result)
{
string symlinkPath = StringBuilderCache.GetStringAndRelease(symlinkPathBuilder);
// time zone Ids have to point under the time zone directory
string timeZoneDirectory = GetTimeZoneDirectory();
if (symlinkPath.StartsWith(timeZoneDirectory))
{
id = symlinkPath.Substring(timeZoneDirectory.Length);
}
}
else
{
StringBuilderCache.Release(symlinkPathBuilder);
}
return id;
}
///
/// Find the time zone id by searching all the tzfiles for the one that matches rawData
/// and return its file name.
///
private static string FindTimeZoneId(byte[] rawData)
{
// default to "Local" if we can't find the right tzfile
string id = LocalId;
string timeZoneDirectory = GetTimeZoneDirectory();
string localtimeFilePath = Path.Combine(timeZoneDirectory, "localtime");
string posixrulesFilePath = Path.Combine(timeZoneDirectory, "posixrules");
byte[] buffer = new byte[rawData.Length];
try
{
foreach (string filePath in Directory.EnumerateFiles(timeZoneDirectory, "*", SearchOption.AllDirectories))
{
// skip the localtime and posixrules file, since they won't give us the correct id
if (!string.Equals(filePath, localtimeFilePath, StringComparison.OrdinalIgnoreCase)
&& !string.Equals(filePath, posixrulesFilePath, StringComparison.OrdinalIgnoreCase))
{
if (CompareTimeZoneFile(filePath, buffer, rawData))
{
// if all bytes are the same, this must be the right tz file
id = filePath;
// strip off the root time zone directory
if (id.StartsWith(timeZoneDirectory))
{
id = id.Substring(timeZoneDirectory.Length);
}
break;
}
}
}
}
catch (IOException) { }
catch (SecurityException) { }
catch (UnauthorizedAccessException) { }
return id;
}
private static bool CompareTimeZoneFile(string filePath, byte[] buffer, byte[] rawData)
{
try
{
using (FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
if (stream.Length == rawData.Length)
{
int index = 0;
int count = rawData.Length;
while (count > 0)
{
int n = stream.Read(buffer, index, count);
if (n == 0)
__Error.EndOfFile();
int end = index + n;
for (; index < end; index++)
{
if (buffer[index] != rawData[index])
{
return false;
}
}
count -= n;
}
return true;
}
}
}
catch (IOException) { }
catch (SecurityException) { }
catch (UnauthorizedAccessException) { }
return false;
}
///
/// Helper function used by 'GetLocalTimeZone()' - this function wraps the call
/// for loading time zone data from computers without Registry support.
///
/// The TryGetLocalTzFile() call returns a Byte[] containing the compiled tzfile.
///
private static TimeZoneInfo GetLocalTimeZoneFromTzFile()
{
byte[] rawData;
string id;
if (TryGetLocalTzFile(out rawData, out id))
{
TimeZoneInfo result = GetTimeZoneFromTzData(rawData, id);
if (result != null)
{
return result;
}
}
// if we can't find a local time zone, return UTC
return Utc;
}
private static TimeZoneInfo GetTimeZoneFromTzData(byte[] rawData, string id)
{
if (rawData != null)
{
try
{
return new TimeZoneInfo(rawData, id, dstDisabled: false); // create a TimeZoneInfo instance from the TZif data w/ DST support
}
catch (ArgumentException) { }
catch (InvalidTimeZoneException) { }
try
{
return new TimeZoneInfo(rawData, id, dstDisabled: true); // create a TimeZoneInfo instance from the TZif data w/o DST support
}
catch (ArgumentException) { }
catch (InvalidTimeZoneException) { }
}
return null;
}
private static string GetTimeZoneDirectory()
{
string tzDirectory = Environment.GetEnvironmentVariable(TimeZoneDirectoryEnvironmentVariable);
if (tzDirectory == null)
{
tzDirectory = DefaultTimeZoneDirectory;
}
else if (!tzDirectory.EndsWith(Path.DirectorySeparatorChar))
{
tzDirectory += Path.DirectorySeparatorChar;
}
return tzDirectory;
}
///
/// Helper function for retrieving a TimeZoneInfo object by .
/// This function wraps the logic necessary to keep the private
/// SystemTimeZones cache in working order
///
/// This function will either return a valid TimeZoneInfo instance or
/// it will throw 'InvalidTimeZoneException' / 'TimeZoneNotFoundException'.
///
public static TimeZoneInfo FindSystemTimeZoneById(string id)
{
// Special case for Utc as it will not exist in the dictionary with the rest
// of the system time zones. There is no need to do this check for Local.Id
// since Local is a real time zone that exists in the dictionary cache
if (string.Equals(id, UtcId, StringComparison.OrdinalIgnoreCase))
{
return Utc;
}
if (id == null)
{
throw new ArgumentNullException(nameof(id));
}
else if (id.Length == 0 || id.Contains("\0"))
{
throw new TimeZoneNotFoundException(SR.Format(SR.TimeZoneNotFound_MissingData, id));
}
TimeZoneInfo value;
Exception e;
TimeZoneInfoResult result;
CachedData cachedData = s_cachedData;
lock (cachedData)
{
result = TryGetTimeZone(id, false, out value, out e, cachedData, alwaysFallbackToLocalMachine: true);
}
if (result == TimeZoneInfoResult.Success)
{
return value;
}
else if (result == TimeZoneInfoResult.InvalidTimeZoneException)
{
Debug.Assert(e is InvalidTimeZoneException,
"TryGetTimeZone must create an InvalidTimeZoneException when it returns TimeZoneInfoResult.InvalidTimeZoneException");
throw e;
}
else if (result == TimeZoneInfoResult.SecurityException)
{
throw new SecurityException(SR.Format(SR.Security_CannotReadFileData, id), e);
}
else
{
throw new TimeZoneNotFoundException(SR.Format(SR.TimeZoneNotFound_MissingData, id), e);
}
}
// DateTime.Now fast path that avoids allocating an historically accurate TimeZoneInfo.Local and just creates a 1-year (current year) accurate time zone
internal static TimeSpan GetDateTimeNowUtcOffsetFromUtc(DateTime time, out bool isAmbiguousLocalDst)
{
bool isDaylightSavings;
// Use the standard code path for Unix since there isn't a faster way of handling current-year-only time zones
return GetUtcOffsetFromUtc(time, Local, out isDaylightSavings, out isAmbiguousLocalDst);
}
// TZFILE(5) BSD File Formats Manual TZFILE(5)
//
// NAME
// tzfile -- timezone information
//
// SYNOPSIS
// #include "/usr/src/lib/libc/stdtime/tzfile.h"
//
// DESCRIPTION
// The time zone information files used by tzset(3) begin with the magic
// characters ``TZif'' to identify them as time zone information files, fol-
// lowed by sixteen bytes reserved for future use, followed by four four-
// byte values written in a ``standard'' byte order (the high-order byte of
// the value is written first). These values are, in order:
//
// tzh_ttisgmtcnt The number of UTC/local indicators stored in the file.
// tzh_ttisstdcnt The number of standard/wall indicators stored in the
// file.
// tzh_leapcnt The number of leap seconds for which data is stored in
// the file.
// tzh_timecnt The number of ``transition times'' for which data is
// stored in the file.
// tzh_typecnt The number of ``local time types'' for which data is
// stored in the file (must not be zero).
// tzh_charcnt The number of characters of ``time zone abbreviation
// strings'' stored in the file.
//
// The above header is followed by tzh_timecnt four-byte values of type
// long, sorted in ascending order. These values are written in ``stan-
// dard'' byte order. Each is used as a transition time (as returned by
// time(3)) at which the rules for computing local time change. Next come
// tzh_timecnt one-byte values of type unsigned char; each one tells which
// of the different types of ``local time'' types described in the file is
// associated with the same-indexed transition time. These values serve as
// indices into an array of ttinfo structures that appears next in the file;
// these structures are defined as follows:
//
// struct ttinfo {
// long tt_gmtoff;
// int tt_isdst;
// unsigned int tt_abbrind;
// };
//
// Each structure is written as a four-byte value for tt_gmtoff of type
// long, in a standard byte order, followed by a one-byte value for tt_isdst
// and a one-byte value for tt_abbrind. In each structure, tt_gmtoff gives
// the number of seconds to be added to UTC, tt_isdst tells whether tm_isdst
// should be set by localtime(3) and tt_abbrind serves as an index into the
// array of time zone abbreviation characters that follow the ttinfo struc-
// ture(s) in the file.
//
// Then there are tzh_leapcnt pairs of four-byte values, written in standard
// byte order; the first value of each pair gives the time (as returned by
// time(3)) at which a leap second occurs; the second gives the total number
// of leap seconds to be applied after the given time. The pairs of values
// are sorted in ascending order by time.b
//
// Then there are tzh_ttisstdcnt standard/wall indicators, each stored as a
// one-byte value; they tell whether the transition times associated with
// local time types were specified as standard time or wall clock time, and
// are used when a time zone file is used in handling POSIX-style time zone
// environment variables.
//
// Finally there are tzh_ttisgmtcnt UTC/local indicators, each stored as a
// one-byte value; they tell whether the transition times associated with
// local time types were specified as UTC or local time, and are used when a
// time zone file is used in handling POSIX-style time zone environment
// variables.
//
// localtime uses the first standard-time ttinfo structure in the file (or
// simply the first ttinfo structure in the absence of a standard-time
// structure) if either tzh_timecnt is zero or the time argument is less
// than the first transition time recorded in the file.
//
// SEE ALSO
// ctime(3), time2posix(3), zic(8)
//
// BSD September 13, 1994 BSD
//
//
//
// TIME(3) BSD Library Functions Manual TIME(3)
//
// NAME
// time -- get time of day
//
// LIBRARY
// Standard C Library (libc, -lc)
//
// SYNOPSIS
// #include
//
// time_t
// time(time_t *tloc);
//
// DESCRIPTION
// The time() function returns the value of time in seconds since 0 hours, 0
// minutes, 0 seconds, January 1, 1970, Coordinated Universal Time, without
// including leap seconds. If an error occurs, time() returns the value
// (time_t)-1.
//
// The return value is also stored in *tloc, provided that tloc is non-null.
//
// ERRORS
// The time() function may fail for any of the reasons described in
// gettimeofday(2).
//
// SEE ALSO
// gettimeofday(2), ctime(3)
//
// STANDARDS
// The time function conforms to IEEE Std 1003.1-2001 (``POSIX.1'').
//
// BUGS
// Neither ISO/IEC 9899:1999 (``ISO C99'') nor IEEE Std 1003.1-2001
// (``POSIX.1'') requires time() to set errno on failure; thus, it is impos-
// sible for an application to distinguish the valid time value -1 (repre-
// senting the last UTC second of 1969) from the error return value.
//
// Systems conforming to earlier versions of the C and POSIX standards
// (including older versions of FreeBSD) did not set *tloc in the error
// case.
//
// HISTORY
// A time() function appeared in Version 6 AT&T UNIX.
//
// BSD July 18, 2003 BSD
//
//
private static void TZif_GenerateAdjustmentRules(out AdjustmentRule[] rules, TimeSpan baseUtcOffset, DateTime[] dts, byte[] typeOfLocalTime,
TZifType[] transitionType, bool[] StandardTime, bool[] GmtTime, string futureTransitionsPosixFormat)
{
rules = null;
if (dts.Length > 0)
{
int index = 0;
List rulesList = new List();
while (index <= dts.Length)
{
TZif_GenerateAdjustmentRule(ref index, baseUtcOffset, rulesList, dts, typeOfLocalTime, transitionType, StandardTime, GmtTime, futureTransitionsPosixFormat);
}
rules = rulesList.ToArray();
if (rules != null && rules.Length == 0)
{
rules = null;
}
}
}
private static void TZif_GenerateAdjustmentRule(ref int index, TimeSpan timeZoneBaseUtcOffset, List rulesList, DateTime[] dts,
byte[] typeOfLocalTime, TZifType[] transitionTypes, bool[] StandardTime, bool[] GmtTime, string futureTransitionsPosixFormat)
{
// To generate AdjustmentRules, use the following approach:
// The first AdjustmentRule will go from DateTime.MinValue to the first transition time greater than DateTime.MinValue.
// Each middle AdjustmentRule wil go from dts[index-1] to dts[index].
// The last AdjustmentRule will go from dts[dts.Length-1] to Datetime.MaxValue.
// 0. Skip any DateTime.MinValue transition times. In newer versions of the tzfile, there
// is a "big bang" transition time, which is before the year 0001. Since any times before year 0001
// cannot be represented by DateTime, there is no reason to make AdjustmentRules for these unrepresentable time periods.
// 1. If there are no DateTime.MinValue times, the first AdjustmentRule goes from DateTime.MinValue
// to the first transition and uses the first standard transitionType (or the first transitionType if none of them are standard)
// 2. Create an AdjustmentRule for each transition, i.e. from dts[index - 1] to dts[index].
// This rule uses the transitionType[index - 1] and the whole AdjustmentRule only describes a single offset - either
// all daylight savings, or all stanard time.
// 3. After all the transitions are filled out, the last AdjustmentRule is created from either:
// a. a POSIX-style timezone description ("futureTransitionsPosixFormat"), if there is one or
// b. continue the last transition offset until DateTime.Max
while (index < dts.Length && dts[index] == DateTime.MinValue)
{
index++;
}
if (index == 0)
{
TZifType transitionType = TZif_GetEarlyDateTransitionType(transitionTypes);
DateTime endTransitionDate = dts[index];
TimeSpan transitionOffset = TZif_CalculateTransitionOffsetFromBase(transitionType.UtcOffset, timeZoneBaseUtcOffset);
TimeSpan daylightDelta = transitionType.IsDst ? transitionOffset : TimeSpan.Zero;
TimeSpan baseUtcDelta = transitionType.IsDst ? TimeSpan.Zero : transitionOffset;
AdjustmentRule r = AdjustmentRule.CreateAdjustmentRule(
DateTime.MinValue,
endTransitionDate.AddTicks(-1),
daylightDelta,
default(TransitionTime),
default(TransitionTime),
baseUtcDelta,
noDaylightTransitions: true);
rulesList.Add(r);
}
else if (index < dts.Length)
{
DateTime startTransitionDate = dts[index - 1];
TZifType startTransitionType = transitionTypes[typeOfLocalTime[index - 1]];
DateTime endTransitionDate = dts[index];
TimeSpan transitionOffset = TZif_CalculateTransitionOffsetFromBase(startTransitionType.UtcOffset, timeZoneBaseUtcOffset);
TimeSpan daylightDelta = startTransitionType.IsDst ? transitionOffset : TimeSpan.Zero;
TimeSpan baseUtcDelta = startTransitionType.IsDst ? TimeSpan.Zero : transitionOffset;
TransitionTime dstStart;
if (startTransitionType.IsDst)
{
// the TransitionTime fields are not used when AdjustmentRule.NoDaylightTransitions == true.
// However, there are some cases in the past where DST = true, and the daylight savings offset
// now equals what the current BaseUtcOffset is. In that case, the AdjustmentRule.DaylightOffset
// is going to be TimeSpan.Zero. But we still need to return 'true' from AdjustmentRule.HasDaylightSaving.
// To ensure we always return true from HasDaylightSaving, make a "special" dstStart that will make the logic
// in HasDaylightSaving return true.
dstStart = TransitionTime.CreateFixedDateRule(DateTime.MinValue.AddMilliseconds(2), 1, 1);
}
else
{
dstStart = default(TransitionTime);
}
AdjustmentRule r = AdjustmentRule.CreateAdjustmentRule(
startTransitionDate,
endTransitionDate.AddTicks(-1),
daylightDelta,
dstStart,
default(TransitionTime),
baseUtcDelta,
noDaylightTransitions: true);
rulesList.Add(r);
}
else
{
// create the AdjustmentRule that will be used for all DateTimes after the last transition
// NOTE: index == dts.Length
DateTime startTransitionDate = dts[index - 1];
if (!string.IsNullOrEmpty(futureTransitionsPosixFormat))
{
AdjustmentRule r = TZif_CreateAdjustmentRuleForPosixFormat(futureTransitionsPosixFormat, startTransitionDate, timeZoneBaseUtcOffset);
if (r != null)
{
rulesList.Add(r);
}
}
else
{
// just use the last transition as the rule which will be used until the end of time
TZifType transitionType = transitionTypes[typeOfLocalTime[index - 1]];
TimeSpan transitionOffset = TZif_CalculateTransitionOffsetFromBase(transitionType.UtcOffset, timeZoneBaseUtcOffset);
TimeSpan daylightDelta = transitionType.IsDst ? transitionOffset : TimeSpan.Zero;
TimeSpan baseUtcDelta = transitionType.IsDst ? TimeSpan.Zero : transitionOffset;
AdjustmentRule r = AdjustmentRule.CreateAdjustmentRule(
startTransitionDate,
DateTime.MaxValue,
daylightDelta,
default(TransitionTime),
default(TransitionTime),
baseUtcDelta,
noDaylightTransitions: true);
rulesList.Add(r);
}
}
index++;
}
private static TimeSpan TZif_CalculateTransitionOffsetFromBase(TimeSpan transitionOffset, TimeSpan timeZoneBaseUtcOffset)
{
TimeSpan result = transitionOffset - timeZoneBaseUtcOffset;
// TZif supports seconds-level granularity with offsets but TimeZoneInfo only supports minutes since it aligns
// with DateTimeOffset, SQL Server, and the W3C XML Specification
if (result.Ticks % TimeSpan.TicksPerMinute != 0)
{
result = new TimeSpan(result.Hours, result.Minutes, 0);
}
return result;
}
///
/// Gets the first standard-time transition type, or simply the first transition type
/// if there are no standard transition types.
/// >
///
/// from 'man tzfile':
/// localtime(3) uses the first standard-time ttinfo structure in the file
/// (or simply the first ttinfo structure in the absence of a standard-time
/// structure) if either tzh_timecnt is zero or the time argument is less
/// than the first transition time recorded in the file.
///
private static TZifType TZif_GetEarlyDateTransitionType(TZifType[] transitionTypes)
{
foreach (TZifType transitionType in transitionTypes)
{
if (!transitionType.IsDst)
{
return transitionType;
}
}
if (transitionTypes.Length > 0)
{
return transitionTypes[0];
}
throw new InvalidTimeZoneException(SR.InvalidTimeZone_NoTTInfoStructures);
}
///
/// Creates an AdjustmentRule given the POSIX TZ environment variable string.
///
///
/// See http://man7.org/linux/man-pages/man3/tzset.3.html for the format and semantics of this POSX string.
///
private static AdjustmentRule TZif_CreateAdjustmentRuleForPosixFormat(string posixFormat, DateTime startTransitionDate, TimeSpan timeZoneBaseUtcOffset)
{
string standardName;
string standardOffset;
string daylightSavingsName;
string daylightSavingsOffset;
string start;
string startTime;
string end;
string endTime;
if (TZif_ParsePosixFormat(posixFormat, out standardName, out standardOffset, out daylightSavingsName,
out daylightSavingsOffset, out start, out startTime, out end, out endTime))
{
// a valid posixFormat has at least standardName and standardOffset
TimeSpan? parsedBaseOffset = TZif_ParseOffsetString(standardOffset);
if (parsedBaseOffset.HasValue)
{
TimeSpan baseOffset = parsedBaseOffset.Value.Negate(); // offsets are backwards in POSIX notation
baseOffset = TZif_CalculateTransitionOffsetFromBase(baseOffset, timeZoneBaseUtcOffset);
// having a daylightSavingsName means there is a DST rule
if (!string.IsNullOrEmpty(daylightSavingsName))
{
TimeSpan? parsedDaylightSavings = TZif_ParseOffsetString(daylightSavingsOffset);
TimeSpan daylightSavingsTimeSpan;
if (!parsedDaylightSavings.HasValue)
{
// default DST to 1 hour if it isn't specified
daylightSavingsTimeSpan = new TimeSpan(1, 0, 0);
}
else
{
daylightSavingsTimeSpan = parsedDaylightSavings.Value.Negate(); // offsets are backwards in POSIX notation
daylightSavingsTimeSpan = TZif_CalculateTransitionOffsetFromBase(daylightSavingsTimeSpan, timeZoneBaseUtcOffset);
daylightSavingsTimeSpan = TZif_CalculateTransitionOffsetFromBase(daylightSavingsTimeSpan, baseOffset);
}
TransitionTime dstStart = TZif_CreateTransitionTimeFromPosixRule(start, startTime);
TransitionTime dstEnd = TZif_CreateTransitionTimeFromPosixRule(end, endTime);
return AdjustmentRule.CreateAdjustmentRule(
startTransitionDate,
DateTime.MaxValue,
daylightSavingsTimeSpan,
dstStart,
dstEnd,
baseOffset,
noDaylightTransitions: false);
}
else
{
// if there is no daylightSavingsName, the whole AdjustmentRule should be with no transitions - just the baseOffset
return AdjustmentRule.CreateAdjustmentRule(
startTransitionDate,
DateTime.MaxValue,
TimeSpan.Zero,
default(TransitionTime),
default(TransitionTime),
baseOffset,
noDaylightTransitions: true);
}
}
}
return null;
}
private static TimeSpan? TZif_ParseOffsetString(string offset)
{
TimeSpan? result = null;
if (!string.IsNullOrEmpty(offset))
{
bool negative = offset[0] == '-';
if (negative || offset[0] == '+')
{
offset = offset.Substring(1);
}
// Try parsing just hours first.
// Note, TimeSpan.TryParseExact "%h" can't be used here because some time zones using values
// like "26" or "144" and TimeSpan parsing would turn that into 26 or 144 *days* instead of hours.
int hours;
if (int.TryParse(offset, out hours))
{
result = new TimeSpan(hours, 0, 0);
}
else
{
TimeSpan parsedTimeSpan;
if (TimeSpan.TryParseExact(offset, "g", CultureInfo.InvariantCulture, out parsedTimeSpan))
{
result = parsedTimeSpan;
}
}
if (result.HasValue && negative)
{
result = result.Value.Negate();
}
}
return result;
}
private static TransitionTime TZif_CreateTransitionTimeFromPosixRule(string date, string time)
{
if (string.IsNullOrEmpty(date))
{
return default(TransitionTime);
}
if (date[0] == 'M')
{
// Mm.w.d
// 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;
// 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.
int month;
int week;
DayOfWeek day;
if (!TZif_ParseMDateRule(date, out month, out week, out day))
{
throw new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_UnparseablePosixMDateString, date));
}
DateTime timeOfDay;
TimeSpan? timeOffset = TZif_ParseOffsetString(time);
if (timeOffset.HasValue)
{
// This logic isn't correct and can't be corrected until https://github.com/dotnet/corefx/issues/2618 is fixed.
// Some time zones use time values like, "26", "144", or "-2".
// This allows the week to sometimes be week 4 and sometimes week 5 in the month.
// For now, strip off any 'days' in the offset, and just get the time of day correct
timeOffset = new TimeSpan(timeOffset.Value.Hours, timeOffset.Value.Minutes, timeOffset.Value.Seconds);
if (timeOffset.Value < TimeSpan.Zero)
{
timeOfDay = new DateTime(1, 1, 2, 0, 0, 0);
}
else
{
timeOfDay = new DateTime(1, 1, 1, 0, 0, 0);
}
timeOfDay += timeOffset.Value;
}
else
{
// default to 2AM.
timeOfDay = new DateTime(1, 1, 1, 2, 0, 0);
}
return TransitionTime.CreateFloatingDateRule(timeOfDay, month, week, day);
}
else
{
// Jn
// This specifies the Julian day, with n between 1 and 365.February 29 is never counted, even in leap years.
// n
// This specifies the Julian day, with n between 0 and 365.February 29 is counted in leap years.
// These two rules cannot be expressed with the current AdjustmentRules
// One of them *could* be supported if we relaxed the TransitionTime validation rules, and allowed
// "IsFixedDateRule = true, Month = 0, Day = n" to mean the nth day of the year, picking one of the rules above
throw new InvalidTimeZoneException(SR.InvalidTimeZone_JulianDayNotSupported);
}
}
///
/// Parses a string like Mm.w.d into month, week and DayOfWeek values.
///
///
/// true if the parsing succeeded; otherwise, false.
///
private static bool TZif_ParseMDateRule(string dateRule, out int month, out int week, out DayOfWeek dayOfWeek)
{
month = 0;
week = 0;
dayOfWeek = default(DayOfWeek);
if (dateRule[0] == 'M')
{
int firstDotIndex = dateRule.IndexOf('.');
if (firstDotIndex > 0)
{
int secondDotIndex = dateRule.IndexOf('.', firstDotIndex + 1);
if (secondDotIndex > 0)
{
string monthString = dateRule.Substring(1, firstDotIndex - 1);
string weekString = dateRule.Substring(firstDotIndex + 1, secondDotIndex - firstDotIndex - 1);
string dayString = dateRule.Substring(secondDotIndex + 1);
if (int.TryParse(monthString, out month))
{
if (int.TryParse(weekString, out week))
{
int day;
if (int.TryParse(dayString, out day))
{
dayOfWeek = (DayOfWeek)day;
return true;
}
}
}
}
}
}
return false;
}
private static bool TZif_ParsePosixFormat(
string posixFormat,
out string standardName,
out string standardOffset,
out string daylightSavingsName,
out string daylightSavingsOffset,
out string start,
out string startTime,
out string end,
out string endTime)
{
standardName = null;
standardOffset = null;
daylightSavingsName = null;
daylightSavingsOffset = null;
start = null;
startTime = null;
end = null;
endTime = null;
int index = 0;
standardName = TZif_ParsePosixName(posixFormat, ref index);
standardOffset = TZif_ParsePosixOffset(posixFormat, ref index);
daylightSavingsName = TZif_ParsePosixName(posixFormat, ref index);
if (!string.IsNullOrEmpty(daylightSavingsName))
{
daylightSavingsOffset = TZif_ParsePosixOffset(posixFormat, ref index);
if (index < posixFormat.Length && posixFormat[index] == ',')
{
index++;
TZif_ParsePosixDateTime(posixFormat, ref index, out start, out startTime);
if (index < posixFormat.Length && posixFormat[index] == ',')
{
index++;
TZif_ParsePosixDateTime(posixFormat, ref index, out end, out endTime);
}
}
}
return !string.IsNullOrEmpty(standardName) && !string.IsNullOrEmpty(standardOffset);
}
private static string TZif_ParsePosixName(string posixFormat, ref int index)
{
bool isBracketEnclosed = index < posixFormat.Length && posixFormat[index] == '<';
if (isBracketEnclosed)
{
// move past the opening bracket
index++;
string result = TZif_ParsePosixString(posixFormat, ref index, c => c == '>');
// move past the closing bracket
if (index < posixFormat.Length && posixFormat[index] == '>')
{
index++;
}
return result;
}
else
{
return TZif_ParsePosixString(
posixFormat,
ref index,
c => char.IsDigit(c) || c == '+' || c == '-' || c == ',');
}
}
private static string TZif_ParsePosixOffset(string posixFormat, ref int index) =>
TZif_ParsePosixString(posixFormat, ref index, c => !char.IsDigit(c) && c != '+' && c != '-' && c != ':');
private static void TZif_ParsePosixDateTime(string posixFormat, ref int index, out string date, out string time)
{
time = null;
date = TZif_ParsePosixDate(posixFormat, ref index);
if (index < posixFormat.Length && posixFormat[index] == '/')
{
index++;
time = TZif_ParsePosixTime(posixFormat, ref index);
}
}
private static string TZif_ParsePosixDate(string posixFormat, ref int index) =>
TZif_ParsePosixString(posixFormat, ref index, c => c == '/' || c == ',');
private static string TZif_ParsePosixTime(string posixFormat, ref int index) =>
TZif_ParsePosixString(posixFormat, ref index, c => c == ',');
private static string TZif_ParsePosixString(string posixFormat, ref int index, Func breakCondition)
{
int startIndex = index;
for (; index < posixFormat.Length; index++)
{
char current = posixFormat[index];
if (breakCondition(current))
{
break;
}
}
return posixFormat.Substring(startIndex, index - startIndex);
}
// Returns the Substring from zoneAbbreviations starting at index and ending at '\0'
// zoneAbbreviations is expected to be in the form: "PST\0PDT\0PWT\0\PPT"
private static string TZif_GetZoneAbbreviation(string zoneAbbreviations, int index)
{
int lastIndex = zoneAbbreviations.IndexOf('\0', index);
return lastIndex > 0 ?
zoneAbbreviations.Substring(index, lastIndex - index) :
zoneAbbreviations.Substring(index);
}
// Converts an array of bytes into an int - always using standard byte order (Big Endian)
// per TZif file standard
private static unsafe int TZif_ToInt32(byte[] value, int startIndex)
{
fixed (byte* pbyte = &value[startIndex])
{
return (*pbyte << 24) | (*(pbyte + 1) << 16) | (*(pbyte + 2) << 8) | (*(pbyte + 3));
}
}
// Converts an array of bytes into a long - always using standard byte order (Big Endian)
// per TZif file standard
private static unsafe long TZif_ToInt64(byte[] value, int startIndex)
{
fixed (byte* pbyte = &value[startIndex])
{
int i1 = (*pbyte << 24) | (*(pbyte + 1) << 16) | (*(pbyte + 2) << 8) | (*(pbyte + 3));
int i2 = (*(pbyte + 4) << 24) | (*(pbyte + 5) << 16) | (*(pbyte + 6) << 8) | (*(pbyte + 7));
return (uint)i2 | ((long)i1 << 32);
}
}
private static long TZif_ToUnixTime(byte[] value, int startIndex, TZVersion version) =>
version != TZVersion.V1 ?
TZif_ToInt64(value, startIndex) :
TZif_ToInt32(value, startIndex);
private static DateTime TZif_UnixTimeToDateTime(long unixTime) =>
unixTime < DateTimeOffset.UnixMinSeconds ? DateTime.MinValue :
unixTime > DateTimeOffset.UnixMaxSeconds ? DateTime.MaxValue :
DateTimeOffset.FromUnixTimeSeconds(unixTime).UtcDateTime;
private static void TZif_ParseRaw(byte[] data, out TZifHead t, out DateTime[] dts, out byte[] typeOfLocalTime, out TZifType[] transitionType,
out string zoneAbbreviations, out bool[] StandardTime, out bool[] GmtTime, out string futureTransitionsPosixFormat)
{
// initialize the out parameters in case the TZifHead ctor throws
dts = null;
typeOfLocalTime = null;
transitionType = null;
zoneAbbreviations = string.Empty;
StandardTime = null;
GmtTime = null;
futureTransitionsPosixFormat = null;
// read in the 44-byte TZ header containing the count/length fields
//
int index = 0;
t = new TZifHead(data, index);
index += TZifHead.Length;
int timeValuesLength = 4; // the first version uses 4-bytes to specify times
if (t.Version != TZVersion.V1)
{
// move index past the V1 information to read the V2 information
index += (int)((timeValuesLength * t.TimeCount) + t.TimeCount + (6 * t.TypeCount) + ((timeValuesLength + 4) * t.LeapCount) + t.IsStdCount + t.IsGmtCount + t.CharCount);
// read the V2 header
t = new TZifHead(data, index);
index += TZifHead.Length;
timeValuesLength = 8; // the second version uses 8-bytes
}
// initialize the containers for the rest of the TZ data
dts = new DateTime[t.TimeCount];
typeOfLocalTime = new byte[t.TimeCount];
transitionType = new TZifType[t.TypeCount];
zoneAbbreviations = string.Empty;
StandardTime = new bool[t.TypeCount];
GmtTime = new bool[t.TypeCount];
// read in the UTC transition points and convert them to Windows
//
for (int i = 0; i < t.TimeCount; i++)
{
long unixTime = TZif_ToUnixTime(data, index, t.Version);
dts[i] = TZif_UnixTimeToDateTime(unixTime);
index += timeValuesLength;
}
// read in the Type Indices; there is a 1:1 mapping of UTC transition points to Type Indices
// these indices directly map to the array index in the transitionType array below
//
for (int i = 0; i < t.TimeCount; i++)
{
typeOfLocalTime[i] = data[index];
index += 1;
}
// read in the Type table. Each 6-byte entry represents
// {UtcOffset, IsDst, AbbreviationIndex}
//
// each AbbreviationIndex is a character index into the zoneAbbreviations string below
//
for (int i = 0; i < t.TypeCount; i++)
{
transitionType[i] = new TZifType(data, index);
index += 6;
}
// read in the Abbreviation ASCII string. This string will be in the form:
// "PST\0PDT\0PWT\0\PPT"
//
Encoding enc = Encoding.UTF8;
zoneAbbreviations = enc.GetString(data, index, (int)t.CharCount);
index += (int)t.CharCount;
// skip ahead of the Leap-Seconds Adjustment data. In a future release, consider adding
// support for Leap-Seconds
//
index += (int)(t.LeapCount * (timeValuesLength + 4)); // skip the leap second transition times
// read in the Standard Time table. There should be a 1:1 mapping between Type-Index and Standard
// Time table entries.
//
// TRUE = transition time is standard time
// FALSE = transition time is wall clock time
// ABSENT = transition time is wall clock time
//
for (int i = 0; i < t.IsStdCount && i < t.TypeCount && index < data.Length; i++)
{
StandardTime[i] = (data[index++] != 0);
}
// read in the GMT Time table. There should be a 1:1 mapping between Type-Index and GMT Time table
// entries.
//
// TRUE = transition time is UTC
// FALSE = transition time is local time
// ABSENT = transition time is local time
//
for (int i = 0; i < t.IsGmtCount && i < t.TypeCount && index < data.Length; i++)
{
GmtTime[i] = (data[index++] != 0);
}
if (t.Version != TZVersion.V1)
{
// read the POSIX-style format, which should be wrapped in newlines with the last newline at the end of the file
if (data[index++] == '\n' && data[data.Length - 1] == '\n')
{
futureTransitionsPosixFormat = enc.GetString(data, index, data.Length - index - 1);
}
}
}
private struct TZifType
{
public const int Length = 6;
public readonly TimeSpan UtcOffset;
public readonly bool IsDst;
public readonly byte AbbreviationIndex;
public TZifType(byte[] data, int index)
{
if (data == null || data.Length < index + Length)
{
throw new ArgumentException(SR.Argument_TimeZoneInfoInvalidTZif, nameof(data));
}
Contract.EndContractBlock();
UtcOffset = new TimeSpan(0, 0, TZif_ToInt32(data, index + 00));
IsDst = (data[index + 4] != 0);
AbbreviationIndex = data[index + 5];
}
}
private struct TZifHead
{
public const int Length = 44;
public readonly uint Magic; // TZ_MAGIC "TZif"
public readonly TZVersion Version; // 1 byte for a \0 or 2 or 3
// public byte[15] Reserved; // reserved for future use
public readonly uint IsGmtCount; // number of transition time flags
public readonly uint IsStdCount; // number of transition time flags
public readonly uint LeapCount; // number of leap seconds
public readonly uint TimeCount; // number of transition times
public readonly uint TypeCount; // number of local time types
public readonly uint CharCount; // number of abbreviated characters
public TZifHead(byte[] data, int index)
{
if (data == null || data.Length < Length)
{
throw new ArgumentException("bad data", nameof(data));
}
Contract.EndContractBlock();
Magic = (uint)TZif_ToInt32(data, index + 00);
if (Magic != 0x545A6966)
{
// 0x545A6966 = {0x54, 0x5A, 0x69, 0x66} = "TZif"
throw new ArgumentException(SR.Argument_TimeZoneInfoBadTZif, nameof(data));
}
byte version = data[index + 04];
Version =
version == '2' ? TZVersion.V2 :
version == '3' ? TZVersion.V3 :
TZVersion.V1; // default/fallback to V1 to guard against future, unsupported version numbers
// skip the 15 byte reserved field
// don't use the BitConverter class which parses data
// based on the Endianess of the machine architecture.
// this data is expected to always be in "standard byte order",
// regardless of the machine it is being processed on.
IsGmtCount = (uint)TZif_ToInt32(data, index + 20);
IsStdCount = (uint)TZif_ToInt32(data, index + 24);
LeapCount = (uint)TZif_ToInt32(data, index + 28);
TimeCount = (uint)TZif_ToInt32(data, index + 32);
TypeCount = (uint)TZif_ToInt32(data, index + 36);
CharCount = (uint)TZif_ToInt32(data, index + 40);
}
}
private enum TZVersion : byte
{
V1 = 0,
V2,
V3,
// when adding more versions, ensure all the logic using TZVersion is still correct
}
}
}