using Ryujinx.Common; using Ryujinx.Common.Memory; using Ryujinx.Common.Utilities; using Ryujinx.HLE.Utilities; using System; using System.Buffers.Binary; using System.IO; using System.Runtime.InteropServices; using System.Text; using static Ryujinx.HLE.HOS.Services.Time.TimeZone.TimeZoneRule; namespace Ryujinx.HLE.HOS.Services.Time.TimeZone { public class TimeZone { private const int TimeTypeSize = 8; private const int EpochYear = 1970; private const int YearBase = 1900; private const int EpochWeekDay = 4; private const int SecondsPerMinute = 60; private const int MinutesPerHour = 60; private const int HoursPerDays = 24; private const int DaysPerWekk = 7; private const int DaysPerNYear = 365; private const int DaysPerLYear = 366; private const int MonthsPerYear = 12; private const int SecondsPerHour = SecondsPerMinute * MinutesPerHour; private const int SecondsPerDay = SecondsPerHour * HoursPerDays; private const int YearsPerRepeat = 400; private const long AverageSecondsPerYear = 31556952; private const long SecondsPerRepeat = YearsPerRepeat * AverageSecondsPerYear; private static readonly int[] YearLengths = { DaysPerNYear, DaysPerLYear }; private static readonly int[][] MonthsLengths = new int[][] { new int[] { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }, new int[] { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 } }; private static readonly byte[] TimeZoneDefaultRule = Encoding.ASCII.GetBytes(",M4.1.0,M10.5.0"); [StructLayout(LayoutKind.Sequential, Pack = 0x4, Size = 0x10)] private struct CalendarTimeInternal { // NOTE: On the IPC side this is supposed to be a 16 bits value but internally this need to be a 64 bits value for ToPosixTime. public long Year; public sbyte Month; public sbyte Day; public sbyte Hour; public sbyte Minute; public sbyte Second; public int CompareTo(CalendarTimeInternal other) { if (Year != other.Year) { if (Year < other.Year) { return -1; } return 1; } if (Month != other.Month) { return Month - other.Month; } if (Day != other.Day) { return Day - other.Day; } if (Hour != other.Hour) { return Hour - other.Hour; } if (Minute != other.Minute) { return Minute - other.Minute; } if (Second != other.Second) { return Second - other.Second; } return 0; } } private enum RuleType { JulianDay, DayOfYear, MonthNthDayOfWeek } private struct Rule { public RuleType Type; public int Day; public int Week; public int Month; public int TransitionTime; } private static int Detzcode32(ReadOnlySpan<byte> bytes) { return BinaryPrimitives.ReadInt32BigEndian(bytes); } private static int Detzcode32(int value) { if (BitConverter.IsLittleEndian) { return BinaryPrimitives.ReverseEndianness(value); } return value; } private static long Detzcode64(ReadOnlySpan<byte> bytes) { return BinaryPrimitives.ReadInt64BigEndian(bytes); } private static bool DifferByRepeat(long t1, long t0) { return (t1 - t0) == SecondsPerRepeat; } private static bool TimeTypeEquals(in TimeZoneRule outRules, byte aIndex, byte bIndex) { if (aIndex < 0 || aIndex >= outRules.TypeCount || bIndex < 0 || bIndex >= outRules.TypeCount) { return false; } TimeTypeInfo a = outRules.Ttis[aIndex]; TimeTypeInfo b = outRules.Ttis[bIndex]; return a.GmtOffset == b.GmtOffset && a.IsDaySavingTime == b.IsDaySavingTime && a.IsStandardTimeDaylight == b.IsStandardTimeDaylight && a.IsGMT == b.IsGMT && StringUtils.CompareCStr(outRules.Chars[a.AbbreviationListIndex..], outRules.Chars[b.AbbreviationListIndex..]) == 0; } private static int GetQZName(ReadOnlySpan<byte> name, int namePosition, char delimiter) { int i = namePosition; while (name[i] != '\0' && name[i] != delimiter) { i++; } return i; } private static int GetTZName(ReadOnlySpan<byte> name, int namePosition) { int i = namePosition; char c; while ((c = (char)name[i]) != '\0' && !char.IsDigit(c) && c != ',' && c != '-' && c != '+') { i++; } return i; } private static bool GetNum(ReadOnlySpan<byte> name, ref int namePosition, out int num, int min, int max) { num = 0; if (namePosition >= name.Length) { return false; } char c = (char)name[namePosition]; if (!char.IsDigit(c)) { return false; } do { num = num * 10 + (c - '0'); if (num > max) { return false; } if (++namePosition >= name.Length) { return false; } c = (char)name[namePosition]; } while (char.IsDigit(c)); if (num < min) { return false; } return true; } private static bool GetSeconds(ReadOnlySpan<byte> name, ref int namePosition, out int seconds) { seconds = 0; bool isValid = GetNum(name, ref namePosition, out int num, 0, HoursPerDays * DaysPerWekk - 1); if (!isValid) { return false; } seconds = num * SecondsPerHour; if (namePosition >= name.Length) { return false; } if (name[namePosition] == ':') { namePosition++; isValid = GetNum(name, ref namePosition, out num, 0, MinutesPerHour - 1); if (!isValid) { return false; } seconds += num * SecondsPerMinute; if (namePosition >= name.Length) { return false; } if (name[namePosition] == ':') { namePosition++; isValid = GetNum(name, ref namePosition, out num, 0, SecondsPerMinute); if (!isValid) { return false; } seconds += num; } } return true; } private static bool GetOffset(ReadOnlySpan<byte> name, ref int namePosition, ref int offset) { bool isNegative = false; if (namePosition >= name.Length) { return false; } if (name[namePosition] == '-') { isNegative = true; namePosition++; } else if (name[namePosition] == '+') { namePosition++; } if (namePosition >= name.Length) { return false; } bool isValid = GetSeconds(name, ref namePosition, out offset); if (!isValid) { return false; } if (isNegative) { offset = -offset; } return true; } private static bool GetRule(ReadOnlySpan<byte> name, ref int namePosition, out Rule rule) { rule = new Rule(); bool isValid = false; if (name[namePosition] == 'J') { namePosition++; rule.Type = RuleType.JulianDay; isValid = GetNum(name, ref namePosition, out rule.Day, 1, DaysPerNYear); } else if (name[namePosition] == 'M') { namePosition++; rule.Type = RuleType.MonthNthDayOfWeek; isValid = GetNum(name, ref namePosition, out rule.Month, 1, MonthsPerYear); if (!isValid) { return false; } if (name[namePosition++] != '.') { return false; } isValid = GetNum(name, ref namePosition, out rule.Week, 1, 5); if (!isValid) { return false; } if (name[namePosition++] != '.') { return false; } isValid = GetNum(name, ref namePosition, out rule.Day, 0, DaysPerWekk - 1); } else if (char.IsDigit((char)name[namePosition])) { rule.Type = RuleType.DayOfYear; isValid = GetNum(name, ref namePosition, out rule.Day, 0, DaysPerLYear - 1); } else { return false; } if (!isValid) { return false; } if (name[namePosition] == '/') { namePosition++; return GetOffset(name, ref namePosition, ref rule.TransitionTime); } else { rule.TransitionTime = 2 * SecondsPerHour; } return true; } private static int IsLeap(int year) { if (((year) % 4) == 0 && (((year) % 100) != 0 || ((year) % 400) == 0)) { return 1; } return 0; } private static bool ParsePosixName(ReadOnlySpan<byte> name, ref TimeZoneRule outRules, bool lastDitch) { outRules = new TimeZoneRule(); int stdLen; ReadOnlySpan<byte> stdName = name; int namePosition = 0; int stdOffset = 0; if (lastDitch) { stdLen = 3; namePosition += stdLen; } else { if (name[namePosition] == '<') { namePosition++; stdName = name.Slice(namePosition); int stdNamePosition = namePosition; namePosition = GetQZName(name, namePosition, '>'); if (name[namePosition] != '>') { return false; } stdLen = namePosition - stdNamePosition; namePosition++; } else { namePosition = GetTZName(name, namePosition); stdLen = namePosition; } if (stdLen == 0) { return false; } bool isValid = GetOffset(name.ToArray(), ref namePosition, ref stdOffset); if (!isValid) { return false; } } int charCount = stdLen + 1; int destLen = 0; int dstOffset = 0; ReadOnlySpan<byte> destName = name.Slice(namePosition); if (TzCharsArraySize < charCount) { return false; } if (name[namePosition] != '\0') { if (name[namePosition] == '<') { destName = name.Slice(++namePosition); int destNamePosition = namePosition; namePosition = GetQZName(name.ToArray(), namePosition, '>'); if (name[namePosition] != '>') { return false; } destLen = namePosition - destNamePosition; namePosition++; } else { destName = name.Slice(namePosition); namePosition = GetTZName(name, namePosition); destLen = namePosition; } if (destLen == 0) { return false; } charCount += destLen + 1; if (TzCharsArraySize < charCount) { return false; } if (name[namePosition] != '\0' && name[namePosition] != ',' && name[namePosition] != ';') { bool isValid = GetOffset(name.ToArray(), ref namePosition, ref dstOffset); if (!isValid) { return false; } } else { dstOffset = stdOffset - SecondsPerHour; } if (name[namePosition] == '\0') { name = TimeZoneDefaultRule; namePosition = 0; } if (name[namePosition] == ',' || name[namePosition] == ';') { namePosition++; bool IsRuleValid = GetRule(name, ref namePosition, out Rule start); if (!IsRuleValid) { return false; } if (name[namePosition++] != ',') { return false; } IsRuleValid = GetRule(name, ref namePosition, out Rule end); if (!IsRuleValid) { return false; } if (name[namePosition] != '\0') { return false; } outRules.TypeCount = 2; outRules.Ttis[0] = new TimeTypeInfo { GmtOffset = -dstOffset, IsDaySavingTime = true, AbbreviationListIndex = stdLen + 1 }; outRules.Ttis[1] = new TimeTypeInfo { GmtOffset = -stdOffset, IsDaySavingTime = false, AbbreviationListIndex = 0 }; outRules.DefaultType = 0; int timeCount = 0; long janFirst = 0; int janOffset = 0; int yearBegining = EpochYear; do { int yearSeconds = YearLengths[IsLeap(yearBegining - 1)] * SecondsPerDay; yearBegining--; if (IncrementOverflow64(ref janFirst, -yearSeconds)) { janOffset = -yearSeconds; break; } } while (EpochYear - YearsPerRepeat / 2 < yearBegining); int yearLimit = yearBegining + YearsPerRepeat + 1; int year; for (year = yearBegining; year < yearLimit; year++) { int startTime = TransitionTime(year, start, stdOffset); int endTime = TransitionTime(year, end, dstOffset); int yearSeconds = YearLengths[IsLeap(year)] * SecondsPerDay; bool isReversed = endTime < startTime; if (isReversed) { int swap = startTime; startTime = endTime; endTime = swap; } if (isReversed || (startTime < endTime && (endTime - startTime < (yearSeconds + (stdOffset - dstOffset))))) { if (TzMaxTimes - 2 < timeCount) { break; } outRules.Ats[timeCount] = janFirst; if (!IncrementOverflow64(ref outRules.Ats[timeCount], janOffset + startTime)) { outRules.Types[timeCount++] = isReversed ? (byte)1 : (byte)0; } else if (janOffset != 0) { outRules.DefaultType = isReversed ? 1 : 0; } outRules.Ats[timeCount] = janFirst; if (!IncrementOverflow64(ref outRules.Ats[timeCount], janOffset + endTime)) { outRules.Types[timeCount++] = isReversed ? (byte)0 : (byte)1; yearLimit = year + YearsPerRepeat + 1; } else if (janOffset != 0) { outRules.DefaultType = isReversed ? 0 : 1; } } if (IncrementOverflow64(ref janFirst, janOffset + yearSeconds)) { break; } janOffset = 0; } outRules.TimeCount = timeCount; // There is no time variation, this is then a perpetual DST rule if (timeCount == 0) { outRules.TypeCount = 1; } else if (YearsPerRepeat < year - yearBegining) { outRules.GoBack = true; outRules.GoAhead = true; } } else { if (name[namePosition] == '\0') { return false; } long theirStdOffset = 0; for (int i = 0; i < outRules.TimeCount; i++) { int j = outRules.Types[i]; if (outRules.Ttis[j].IsStandardTimeDaylight) { theirStdOffset = -outRules.Ttis[j].GmtOffset; } } long theirDstOffset = 0; for (int i = 0; i < outRules.TimeCount; i++) { int j = outRules.Types[i]; if (outRules.Ttis[j].IsDaySavingTime) { theirDstOffset = -outRules.Ttis[j].GmtOffset; } } bool isDaySavingTime = false; long theirOffset = theirStdOffset; for (int i = 0; i < outRules.TimeCount; i++) { int j = outRules.Types[i]; outRules.Types[i] = outRules.Ttis[j].IsDaySavingTime ? (byte)1 : (byte)0; if (!outRules.Ttis[j].IsGMT) { if (isDaySavingTime && !outRules.Ttis[j].IsStandardTimeDaylight) { outRules.Ats[i] += dstOffset - theirStdOffset; } else { outRules.Ats[i] += stdOffset - theirStdOffset; } } theirOffset = -outRules.Ttis[j].GmtOffset; if (outRules.Ttis[j].IsDaySavingTime) { theirDstOffset = theirOffset; } else { theirStdOffset = theirOffset; } } outRules.Ttis[0] = new TimeTypeInfo { GmtOffset = -stdOffset, IsDaySavingTime = false, AbbreviationListIndex = 0 }; outRules.Ttis[1] = new TimeTypeInfo { GmtOffset = -dstOffset, IsDaySavingTime = true, AbbreviationListIndex = stdLen + 1 }; outRules.TypeCount = 2; outRules.DefaultType = 0; } } else { // default is perpetual standard time outRules.TypeCount = 1; outRules.TimeCount = 0; outRules.DefaultType = 0; outRules.Ttis[0] = new TimeTypeInfo { GmtOffset = -stdOffset, IsDaySavingTime = false, AbbreviationListIndex = 0 }; } outRules.CharCount = charCount; int charsPosition = 0; for (int i = 0; i < stdLen; i++) { outRules.Chars[i] = stdName[i]; } charsPosition += stdLen; outRules.Chars[charsPosition++] = 0; if (destLen != 0) { for (int i = 0; i < destLen; i++) { outRules.Chars[charsPosition + i] = destName[i]; } outRules.Chars[charsPosition + destLen] = 0; } return true; } private static int TransitionTime(int year, Rule rule, int offset) { int leapYear = IsLeap(year); int value; switch (rule.Type) { case RuleType.JulianDay: value = (rule.Day - 1) * SecondsPerDay; if (leapYear == 1 && rule.Day >= 60) { value += SecondsPerDay; } break; case RuleType.DayOfYear: value = rule.Day * SecondsPerDay; break; case RuleType.MonthNthDayOfWeek: // Here we use Zeller's Congruence to get the day of week of the first month. int m1 = (rule.Month + 9) % 12 + 1; int yy0 = (rule.Month <= 2) ? (year - 1) : year; int yy1 = yy0 / 100; int yy2 = yy0 % 100; int dayOfWeek = ((26 * m1 - 2) / 10 + 1 + yy2 + yy2 / 4 + yy1 / 4 - 2 * yy1) % 7; if (dayOfWeek < 0) { dayOfWeek += DaysPerWekk; } // Get the zero origin int d = rule.Day - dayOfWeek; if (d < 0) { d += DaysPerWekk; } for (int i = 1; i < rule.Week; i++) { if (d + DaysPerWekk >= MonthsLengths[leapYear][rule.Month - 1]) { break; } d += DaysPerWekk; } value = d * SecondsPerDay; for (int i = 0; i < rule.Month - 1; i++) { value += MonthsLengths[leapYear][i] * SecondsPerDay; } break; default: throw new NotImplementedException("Unknown time transition!"); } return value + rule.TransitionTime + offset; } private static bool NormalizeOverflow32(ref int ip, ref int unit, int baseValue) { int delta; if (unit >= 0) { delta = unit / baseValue; } else { delta = -1 - (-1 - unit) / baseValue; } unit -= delta * baseValue; return IncrementOverflow32(ref ip, delta); } private static bool NormalizeOverflow64(ref long ip, ref long unit, long baseValue) { long delta; if (unit >= 0) { delta = unit / baseValue; } else { delta = -1 - (-1 - unit) / baseValue; } unit -= delta * baseValue; return IncrementOverflow64(ref ip, delta); } private static bool IncrementOverflow32(ref int time, int j) { try { time = checked(time + j); return false; } catch (OverflowException) { return true; } } private static bool IncrementOverflow64(ref long time, long j) { try { time = checked(time + j); return false; } catch (OverflowException) { return true; } } internal static bool ParsePosixName(string name, ref TimeZoneRule outRules) { return ParsePosixName(Encoding.ASCII.GetBytes(name), ref outRules, false); } internal static bool ParseTimeZoneBinary(ref TimeZoneRule outRules, Stream inputData) { outRules = new TimeZoneRule(); BinaryReader reader = new BinaryReader(inputData); long streamLength = reader.BaseStream.Length; if (streamLength < Marshal.SizeOf<TzifHeader>()) { return false; } TzifHeader header = reader.ReadStruct<TzifHeader>(); streamLength -= Marshal.SizeOf<TzifHeader>(); int ttisGMTCount = Detzcode32(header.TtisGMTCount); int ttisSTDCount = Detzcode32(header.TtisSTDCount); int leapCount = Detzcode32(header.LeapCount); int timeCount = Detzcode32(header.TimeCount); int typeCount = Detzcode32(header.TypeCount); int charCount = Detzcode32(header.CharCount); if (!(0 <= leapCount && leapCount < TzMaxLeaps && 0 < typeCount && typeCount < TzMaxTypes && 0 <= timeCount && timeCount < TzMaxTimes && 0 <= charCount && charCount < TzMaxChars && (ttisSTDCount == typeCount || ttisSTDCount == 0) && (ttisGMTCount == typeCount || ttisGMTCount == 0))) { return false; } if (streamLength < (timeCount * TimeTypeSize + timeCount + typeCount * 6 + charCount + leapCount * (TimeTypeSize + 4) + ttisSTDCount + ttisGMTCount)) { return false; } outRules.TimeCount = timeCount; outRules.TypeCount = typeCount; outRules.CharCount = charCount; byte[] workBuffer = StreamUtils.StreamToBytes(inputData); timeCount = 0; { Span<byte> p = workBuffer; for (int i = 0; i < outRules.TimeCount; i++) { long at = Detzcode64(p); outRules.Types[i] = 1; if (timeCount != 0 && at <= outRules.Ats[timeCount - 1]) { if (at < outRules.Ats[timeCount - 1]) { return false; } outRules.Types[i - 1] = 0; timeCount--; } outRules.Ats[timeCount++] = at; p = p[TimeTypeSize..]; } timeCount = 0; for (int i = 0; i < outRules.TimeCount; i++) { byte type = p[0]; p = p[1..]; if (outRules.TypeCount <= type) { return false; } if (outRules.Types[i] != 0) { outRules.Types[timeCount++] = type; } } outRules.TimeCount = timeCount; for (int i = 0; i < outRules.TypeCount; i++) { TimeTypeInfo ttis = outRules.Ttis[i]; ttis.GmtOffset = Detzcode32(p); p = p[sizeof(int)..]; if (p[0] >= 2) { return false; } ttis.IsDaySavingTime = p[0] != 0; p = p[1..]; int abbreviationListIndex = p[0]; p = p[1..]; if (abbreviationListIndex >= outRules.CharCount) { return false; } ttis.AbbreviationListIndex = abbreviationListIndex; outRules.Ttis[i] = ttis; } p[..outRules.CharCount].CopyTo(outRules.Chars); p = p[outRules.CharCount..]; outRules.Chars[outRules.CharCount] = 0; for (int i = 0; i < outRules.TypeCount; i++) { if (ttisSTDCount == 0) { outRules.Ttis[i].IsStandardTimeDaylight = false; } else { if (p[0] >= 2) { return false; } outRules.Ttis[i].IsStandardTimeDaylight = p[0] != 0; p = p[1..]; } } for (int i = 0; i < outRules.TypeCount; i++) { if (ttisSTDCount == 0) { outRules.Ttis[i].IsGMT = false; } else { if (p[0] >= 2) { return false; } outRules.Ttis[i].IsGMT = p[0] != 0; p = p[1..]; } } long position = (workBuffer.Length - p.Length); long nRead = streamLength - position; if (nRead < 0) { return false; } // Nintendo abort in case of a TzIf file with a POSIX TZ Name too long to fit inside a TimeZoneRule. // As it's impossible in normal usage to achive this, we also force a crash. if (nRead > (TzNameMax + 1)) { throw new InvalidOperationException(); } byte[] tempName = new byte[TzNameMax + 1]; Array.Copy(workBuffer, position, tempName, 0, nRead); if (nRead > 2 && tempName[0] == '\n' && tempName[nRead - 1] == '\n' && outRules.TypeCount + 2 <= TzMaxTypes) { tempName[nRead - 1] = 0; byte[] name = new byte[TzNameMax]; Array.Copy(tempName, 1, name, 0, nRead - 1); Box<TimeZoneRule> tempRulesBox = new Box<TimeZoneRule>(); ref TimeZoneRule tempRules = ref tempRulesBox.Data; if (ParsePosixName(name, ref tempRulesBox.Data, false)) { int abbreviationCount = 0; charCount = outRules.CharCount; Span<byte> chars = outRules.Chars; for (int i = 0; i < tempRules.TypeCount; i++) { ReadOnlySpan<byte> tempChars = tempRules.Chars; ReadOnlySpan<byte> tempAbbreviation = tempChars[tempRules.Ttis[i].AbbreviationListIndex..]; int j; for (j = 0; j < charCount; j++) { if (StringUtils.CompareCStr(chars[j..], tempAbbreviation) == 0) { tempRules.Ttis[i].AbbreviationListIndex = j; abbreviationCount++; break; } } if (j >= charCount) { int abbreviationLength = StringUtils.LengthCstr(tempAbbreviation); if (j + abbreviationLength < TzMaxChars) { for (int x = 0; x < abbreviationLength; x++) { chars[j + x] = tempAbbreviation[x]; } charCount = j + abbreviationLength + 1; tempRules.Ttis[i].AbbreviationListIndex = j; abbreviationCount++; } } } if (abbreviationCount == tempRules.TypeCount) { outRules.CharCount = charCount; // Remove trailing while (1 < outRules.TimeCount && (outRules.Types[outRules.TimeCount - 1] == outRules.Types[outRules.TimeCount - 2])) { outRules.TimeCount--; } int i; for (i = 0; i < tempRules.TimeCount; i++) { if (outRules.TimeCount == 0 || outRules.Ats[outRules.TimeCount - 1] < tempRules.Ats[i]) { break; } } while (i < tempRules.TimeCount && outRules.TimeCount < TzMaxTimes) { outRules.Ats[outRules.TimeCount] = tempRules.Ats[i]; outRules.Types[outRules.TimeCount] = (byte)(outRules.TypeCount + (byte)tempRules.Types[i]); outRules.TimeCount++; i++; } for (i = 0; i < tempRules.TypeCount; i++) { outRules.Ttis[outRules.TypeCount++] = tempRules.Ttis[i]; } } } } if (outRules.TypeCount == 0) { return false; } if (outRules.TimeCount > 1) { for (int i = 1; i < outRules.TimeCount; i++) { if (TimeTypeEquals(in outRules, outRules.Types[i], outRules.Types[0]) && DifferByRepeat(outRules.Ats[i], outRules.Ats[0])) { outRules.GoBack = true; break; } } for (int i = outRules.TimeCount - 2; i >= 0; i--) { if (TimeTypeEquals(in outRules, outRules.Types[outRules.TimeCount - 1], outRules.Types[i]) && DifferByRepeat(outRules.Ats[outRules.TimeCount - 1], outRules.Ats[i])) { outRules.GoAhead = true; break; } } } int defaultType; for (defaultType = 0; defaultType < outRules.TimeCount; defaultType++) { if (outRules.Types[defaultType] == 0) { break; } } defaultType = defaultType < outRules.TimeCount ? -1 : 0; if (defaultType < 0 && outRules.TimeCount > 0 && outRules.Ttis[outRules.Types[0]].IsDaySavingTime) { defaultType = outRules.Types[0]; while (--defaultType >= 0) { if (!outRules.Ttis[defaultType].IsDaySavingTime) { break; } } } if (defaultType < 0) { defaultType = 0; while (outRules.Ttis[defaultType].IsDaySavingTime) { if (++defaultType >= outRules.TypeCount) { defaultType = 0; break; } } } outRules.DefaultType = defaultType; } return true; } private static long GetLeapDaysNotNeg(long year) { return year / 4 - year / 100 + year / 400; } private static long GetLeapDays(long year) { if (year < 0) { return -1 - GetLeapDaysNotNeg(-1 - year); } else { return GetLeapDaysNotNeg(year); } } private static ResultCode CreateCalendarTime(long time, int gmtOffset, out CalendarTimeInternal calendarTime, out CalendarAdditionalInfo calendarAdditionalInfo) { long year = EpochYear; long timeDays = time / SecondsPerDay; long remainingSeconds = time % SecondsPerDay; calendarTime = new CalendarTimeInternal(); calendarAdditionalInfo = new CalendarAdditionalInfo(); while (timeDays < 0 || timeDays >= YearLengths[IsLeap((int)year)]) { long timeDelta = timeDays / DaysPerLYear; long delta = timeDelta; if (delta == 0) { delta = timeDays < 0 ? -1 : 1; } long newYear = year; if (IncrementOverflow64(ref newYear, delta)) { return ResultCode.OutOfRange; } long leapDays = GetLeapDays(newYear - 1) - GetLeapDays(year - 1); timeDays -= (newYear - year) * DaysPerNYear; timeDays -= leapDays; year = newYear; } long dayOfYear = timeDays; remainingSeconds += gmtOffset; while (remainingSeconds < 0) { remainingSeconds += SecondsPerDay; dayOfYear -= 1; } while (remainingSeconds >= SecondsPerDay) { remainingSeconds -= SecondsPerDay; dayOfYear += 1; } while (dayOfYear < 0) { if (IncrementOverflow64(ref year, -1)) { return ResultCode.OutOfRange; } dayOfYear += YearLengths[IsLeap((int)year)]; } while (dayOfYear >= YearLengths[IsLeap((int)year)]) { dayOfYear -= YearLengths[IsLeap((int)year)]; if (IncrementOverflow64(ref year, 1)) { return ResultCode.OutOfRange; } } calendarTime.Year = year; calendarAdditionalInfo.DayOfYear = (uint)dayOfYear; long dayOfWeek = (EpochWeekDay + ((year - EpochYear) % DaysPerWekk) * (DaysPerNYear % DaysPerWekk) + GetLeapDays(year - 1) - GetLeapDays(EpochYear - 1) + dayOfYear) % DaysPerWekk; if (dayOfWeek < 0) { dayOfWeek += DaysPerWekk; } calendarAdditionalInfo.DayOfWeek = (uint)dayOfWeek; calendarTime.Hour = (sbyte)((remainingSeconds / SecondsPerHour) % SecondsPerHour); remainingSeconds %= SecondsPerHour; calendarTime.Minute = (sbyte)(remainingSeconds / SecondsPerMinute); calendarTime.Second = (sbyte)(remainingSeconds % SecondsPerMinute); int[] ip = MonthsLengths[IsLeap((int)year)]; for (calendarTime.Month = 0; dayOfYear >= ip[calendarTime.Month]; ++calendarTime.Month) { dayOfYear -= ip[calendarTime.Month]; } calendarTime.Day = (sbyte)(dayOfYear + 1); calendarAdditionalInfo.IsDaySavingTime = false; calendarAdditionalInfo.GmtOffset = gmtOffset; return 0; } private static ResultCode ToCalendarTimeInternal(in TimeZoneRule rules, long time, out CalendarTimeInternal calendarTime, out CalendarAdditionalInfo calendarAdditionalInfo) { calendarTime = new CalendarTimeInternal(); calendarAdditionalInfo = new CalendarAdditionalInfo(); ResultCode result; if ((rules.GoAhead && time < rules.Ats[0]) || (rules.GoBack && time > rules.Ats[rules.TimeCount - 1])) { long newTime = time; long seconds; long years; if (time < rules.Ats[0]) { seconds = rules.Ats[0] - time; } else { seconds = time - rules.Ats[rules.TimeCount - 1]; } seconds -= 1; years = (seconds / SecondsPerRepeat + 1) * YearsPerRepeat; seconds = years * AverageSecondsPerYear; if (time < rules.Ats[0]) { newTime += seconds; } else { newTime -= seconds; } if (newTime < rules.Ats[0] && newTime > rules.Ats[rules.TimeCount - 1]) { return ResultCode.TimeNotFound; } result = ToCalendarTimeInternal(in rules, newTime, out calendarTime, out calendarAdditionalInfo); if (result != 0) { return result; } if (time < rules.Ats[0]) { calendarTime.Year -= years; } else { calendarTime.Year += years; } return ResultCode.Success; } int ttiIndex; if (rules.TimeCount == 0 || time < rules.Ats[0]) { ttiIndex = rules.DefaultType; } else { int low = 1; int high = rules.TimeCount; while (low < high) { int mid = (low + high) >> 1; if (time < rules.Ats[mid]) { high = mid; } else { low = mid + 1; } } ttiIndex = rules.Types[low - 1]; } result = CreateCalendarTime(time, rules.Ttis[ttiIndex].GmtOffset, out calendarTime, out calendarAdditionalInfo); if (result == 0) { calendarAdditionalInfo.IsDaySavingTime = rules.Ttis[ttiIndex].IsDaySavingTime; ReadOnlySpan<byte> timeZoneAbbreviation = rules.Chars[rules.Ttis[ttiIndex].AbbreviationListIndex..]; int timeZoneSize = Math.Min(StringUtils.LengthCstr(timeZoneAbbreviation), 8); timeZoneAbbreviation[..timeZoneSize].CopyTo(calendarAdditionalInfo.TimezoneName.AsSpan()); } return result; } private static ResultCode ToPosixTimeInternal(in TimeZoneRule rules, CalendarTimeInternal calendarTime, out long posixTime) { posixTime = 0; int hour = calendarTime.Hour; int minute = calendarTime.Minute; if (NormalizeOverflow32(ref hour, ref minute, MinutesPerHour)) { return ResultCode.Overflow; } calendarTime.Minute = (sbyte)minute; int day = calendarTime.Day; if (NormalizeOverflow32(ref day, ref hour, HoursPerDays)) { return ResultCode.Overflow; } calendarTime.Day = (sbyte)day; calendarTime.Hour = (sbyte)hour; long year = calendarTime.Year; long month = calendarTime.Month; if (NormalizeOverflow64(ref year, ref month, MonthsPerYear)) { return ResultCode.Overflow; } calendarTime.Month = (sbyte)month; if (IncrementOverflow64(ref year, YearBase)) { return ResultCode.Overflow; } while (day <= 0) { if (IncrementOverflow64(ref year, -1)) { return ResultCode.Overflow; } long li = year; if (1 < calendarTime.Month) { li++; } day += YearLengths[IsLeap((int)li)]; } while (day > DaysPerLYear) { long li = year; if (1 < calendarTime.Month) { li++; } day -= YearLengths[IsLeap((int)li)]; if (IncrementOverflow64(ref year, 1)) { return ResultCode.Overflow; } } while (true) { int i = MonthsLengths[IsLeap((int)year)][calendarTime.Month]; if (day <= i) { break; } day -= i; calendarTime.Month += 1; if (calendarTime.Month >= MonthsPerYear) { calendarTime.Month = 0; if (IncrementOverflow64(ref year, 1)) { return ResultCode.Overflow; } } } calendarTime.Day = (sbyte)day; if (IncrementOverflow64(ref year, -YearBase)) { return ResultCode.Overflow; } calendarTime.Year = year; int savedSeconds; if (calendarTime.Second >= 0 && calendarTime.Second < SecondsPerMinute) { savedSeconds = 0; } else if (year + YearBase < EpochYear) { int second = calendarTime.Second; if (IncrementOverflow32(ref second, 1 - SecondsPerMinute)) { return ResultCode.Overflow; } savedSeconds = second; calendarTime.Second = 1 - SecondsPerMinute; } else { savedSeconds = calendarTime.Second; calendarTime.Second = 0; } long low = long.MinValue; long high = long.MaxValue; while (true) { long pivot = low / 2 + high / 2; if (pivot < low) { pivot = low; } else if (pivot > high) { pivot = high; } int direction; ResultCode result = ToCalendarTimeInternal(in rules, pivot, out CalendarTimeInternal candidateCalendarTime, out _); if (result != 0) { if (pivot > 0) { direction = 1; } else { direction = -1; } } else { direction = candidateCalendarTime.CompareTo(calendarTime); } if (direction == 0) { long timeResult = pivot + savedSeconds; if ((timeResult < pivot) != (savedSeconds < 0)) { return ResultCode.Overflow; } posixTime = timeResult; break; } else { if (pivot == low) { if (pivot == long.MaxValue) { return ResultCode.TimeNotFound; } pivot += 1; low += 1; } else if (pivot == high) { if (pivot == long.MinValue) { return ResultCode.TimeNotFound; } pivot -= 1; high -= 1; } if (low > high) { return ResultCode.TimeNotFound; } if (direction > 0) { high = pivot; } else { low = pivot; } } } return ResultCode.Success; } internal static ResultCode ToCalendarTime(in TimeZoneRule rules, long time, out CalendarInfo calendar) { ResultCode result = ToCalendarTimeInternal(in rules, time, out CalendarTimeInternal calendarTime, out CalendarAdditionalInfo calendarAdditionalInfo); calendar = new CalendarInfo() { Time = new CalendarTime() { Year = (short)calendarTime.Year, // NOTE: Nintendo's month range is 1-12, internal range is 0-11. Month = (sbyte)(calendarTime.Month + 1), Day = calendarTime.Day, Hour = calendarTime.Hour, Minute = calendarTime.Minute, Second = calendarTime.Second }, AdditionalInfo = calendarAdditionalInfo }; return result; } internal static ResultCode ToPosixTime(in TimeZoneRule rules, CalendarTime calendarTime, out long posixTime) { CalendarTimeInternal calendarTimeInternal = new CalendarTimeInternal() { Year = calendarTime.Year, // NOTE: Nintendo's month range is 1-12, internal range is 0-11. Month = (sbyte)(calendarTime.Month - 1), Day = calendarTime.Day, Hour = calendarTime.Hour, Minute = calendarTime.Minute, Second = calendarTime.Second }; return ToPosixTimeInternal(in rules, calendarTimeInternal, out posixTime); } } }