From 708620252e837f2111213c6e36d5f8cc8cfd03e8 Mon Sep 17 00:00:00 2001 From: Thomas Guillemard Date: Thu, 4 Jul 2019 17:20:40 +0200 Subject: [PATCH] ITimeZoneService rewrite (#722) * Clean up ITimeZoneService Add error codes and simplify parsing * Add accurate timezone logic TOOD: LoadTimeZoneRule and location name cmds. * Integrate the new TimeZone logic * SCREAMING_UNIX_CASE => PascalCase * Address comments * Reduce use of pointer in the LoadTimeZoneRule logic * Address comments * Realign tzIfStream logic in LoadTimeZoneRule * Address gdk's comments --- .../FileSystem/Content/ContentManager.cs | 3 + .../HOS/Services/Time/ITimeZoneService.cs | 274 ++- .../Services/Time/ITimeZoneServiceTypes.cs | 128 ++ Ryujinx.HLE/HOS/Services/Time/TimeError.cs | 12 + .../HOS/Services/Time/TimeZone/TimeZone.cs | 1707 +++++++++++++++++ .../Services/Time/TimeZone/TimeZoneManager.cs | 289 +++ Ryujinx.HLE/Ryujinx.HLE.csproj | 1 + Ryujinx.HLE/Utilities/StreamUtils.cs | 16 + Ryujinx.HLE/Utilities/StringUtils.cs | 26 + 9 files changed, 2296 insertions(+), 160 deletions(-) create mode 100644 Ryujinx.HLE/HOS/Services/Time/ITimeZoneServiceTypes.cs create mode 100644 Ryujinx.HLE/HOS/Services/Time/TimeError.cs create mode 100644 Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZone.cs create mode 100644 Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZoneManager.cs create mode 100644 Ryujinx.HLE/Utilities/StreamUtils.cs diff --git a/Ryujinx.HLE/FileSystem/Content/ContentManager.cs b/Ryujinx.HLE/FileSystem/Content/ContentManager.cs index 3812e5808..fe6642c3e 100644 --- a/Ryujinx.HLE/FileSystem/Content/ContentManager.cs +++ b/Ryujinx.HLE/FileSystem/Content/ContentManager.cs @@ -1,5 +1,6 @@ using LibHac.Fs; using LibHac.Fs.NcaUtils; +using Ryujinx.HLE.HOS.Services.Time.TimeZone; using Ryujinx.HLE.Utilities; using System; using System.Collections.Generic; @@ -141,6 +142,8 @@ namespace Ryujinx.HLE.FileSystem.Content _locationEntries.Add(storageId, locationList); } } + + TimeZoneManager.Instance.Initialize(_device); } public void ClearEntry(long titleId, ContentType contentType, StorageId storageId) diff --git a/Ryujinx.HLE/HOS/Services/Time/ITimeZoneService.cs b/Ryujinx.HLE/HOS/Services/Time/ITimeZoneService.cs index 563a9753c..056f80aec 100644 --- a/Ryujinx.HLE/HOS/Services/Time/ITimeZoneService.cs +++ b/Ryujinx.HLE/HOS/Services/Time/ITimeZoneService.cs @@ -1,5 +1,8 @@ +using ChocolArm64.Memory; +using Ryujinx.Common; using Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Ipc; +using Ryujinx.HLE.HOS.Services.Time.TimeZone; using System; using System.Collections.Generic; using System.Text; @@ -14,34 +17,38 @@ namespace Ryujinx.HLE.HOS.Services.Time public override IReadOnlyDictionary Commands => _commands; - private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - - private TimeZoneInfo _timeZone = TimeZoneInfo.Local; - public ITimeZoneService() { _commands = new Dictionary { - { 0, GetDeviceLocationName }, - { 1, SetDeviceLocationName }, - { 2, GetTotalLocationNameCount }, - { 3, LoadLocationNameList }, - { 4, LoadTimeZoneRule }, - { 100, ToCalendarTime }, - { 101, ToCalendarTimeWithMyRule }, - { 201, ToPosixTime }, - { 202, ToPosixTimeWithMyRule } + { 0, GetDeviceLocationName }, + { 1, SetDeviceLocationName }, + { 2, GetTotalLocationNameCount }, + { 3, LoadLocationNameList }, + { 4, LoadTimeZoneRule }, + //{ 5, GetTimeZoneRuleVersion }, // 2.0.0+ + //{ 6, GetDeviceLocationNameAndUpdatedTime }, // 5.0.0+ + { 100, ToCalendarTime }, + { 101, ToCalendarTimeWithMyRule }, + { 201, ToPosixTime }, + { 202, ToPosixTimeWithMyRule } }; } + // GetDeviceLocationName() -> nn::time::LocationName public long GetDeviceLocationName(ServiceCtx context) { - char[] tzName = _timeZone.Id.ToCharArray(); - - context.ResponseData.Write(tzName); + char[] tzName = TimeZoneManager.Instance.GetDeviceLocationName().ToCharArray(); int padding = 0x24 - tzName.Length; + if (padding < 0) + { + return MakeError(ErrorModule.Time, TimeError.LocationNameTooLong); + } + + context.ResponseData.Write(tzName); + for (int index = 0; index < padding; index++) { context.ResponseData.Write((byte)0); @@ -50,59 +57,58 @@ namespace Ryujinx.HLE.HOS.Services.Time return 0; } + // SetDeviceLocationName(nn::time::LocationName) public long SetDeviceLocationName(ServiceCtx context) { - byte[] locationName = context.RequestData.ReadBytes(0x24); + string locationName = Encoding.ASCII.GetString(context.RequestData.ReadBytes(0x24)).TrimEnd('\0'); - string tzId = Encoding.ASCII.GetString(locationName).TrimEnd('\0'); - - long resultCode = 0; - - try - { - _timeZone = TimeZoneInfo.FindSystemTimeZoneById(tzId); - } - catch (TimeZoneNotFoundException) - { - resultCode = MakeError(ErrorModule.Time, 0x3dd); - } - - return resultCode; + return TimeZoneManager.Instance.SetDeviceLocationName(locationName); } + // GetTotalLocationNameCount() -> u32 public long GetTotalLocationNameCount(ServiceCtx context) { - context.ResponseData.Write(TimeZoneInfo.GetSystemTimeZones().Count); + context.ResponseData.Write(TimeZoneManager.Instance.GetTotalLocationNameCount()); return 0; } + // LoadLocationNameList(u32 index) -> (u32 outCount, buffer) public long LoadLocationNameList(ServiceCtx context) { - long bufferPosition = context.Response.SendBuff[0].Position; - long bufferSize = context.Response.SendBuff[0].Size; + // TODO: fix logic to use index + uint index = context.RequestData.ReadUInt32(); + long bufferPosition = context.Request.ReceiveBuff[0].Position; + long bufferSize = context.Request.ReceiveBuff[0].Size; - int offset = 0; + uint errorCode = TimeZoneManager.Instance.LoadLocationNameList(index, out string[] locationNameArray, (uint)bufferSize / 0x24); - foreach (TimeZoneInfo info in TimeZoneInfo.GetSystemTimeZones()) + if (errorCode == 0) { - byte[] tzData = Encoding.ASCII.GetBytes(info.Id); + uint offset = 0; - context.Memory.WriteBytes(bufferPosition + offset, tzData); - - int padding = 0x24 - tzData.Length; - - for (int index = 0; index < padding; index++) + foreach (string locationName in locationNameArray) { - context.ResponseData.Write((byte)0); + int padding = 0x24 - locationName.Length; + + if (padding < 0) + { + return MakeError(ErrorModule.Time, TimeError.LocationNameTooLong); + } + + context.Memory.WriteBytes(bufferPosition + offset, Encoding.ASCII.GetBytes(locationName)); + MemoryHelper.FillWithZeros(context.Memory, bufferPosition + offset + locationName.Length, padding); + + offset += 0x24; } - offset += 0x24; + context.ResponseData.Write((uint)locationNameArray.Length); } - return 0; + return errorCode; } + // LoadTimeZoneRule(nn::time::LocationName locationName) -> buffer public long LoadTimeZoneRule(ServiceCtx context) { long bufferPosition = context.Request.ReceiveBuff[0].Position; @@ -110,58 +116,27 @@ namespace Ryujinx.HLE.HOS.Services.Time if (bufferSize != 0x4000) { - Logger.PrintWarning(LogClass.ServiceTime, $"TimeZoneRule buffer size is 0x{bufferSize:x} (expected 0x4000)"); + // TODO: find error code here + Logger.PrintError(LogClass.ServiceTime, $"TimeZoneRule buffer size is 0x{bufferSize:x} (expected 0x4000)"); + + throw new InvalidOperationException(); } - long resultCode = 0; - byte[] locationName = context.RequestData.ReadBytes(0x24); + string locationName = Encoding.ASCII.GetString(context.RequestData.ReadBytes(0x24)).TrimEnd('\0'); - string tzId = Encoding.ASCII.GetString(locationName).TrimEnd('\0'); - - // Check if the Time Zone exists, otherwise error out. - try + long resultCode = TimeZoneManager.Instance.LoadTimeZoneRules(out TimeZoneRule rules, locationName); + + // Write TimeZoneRule if success + if (resultCode == 0) { - TimeZoneInfo info = TimeZoneInfo.FindSystemTimeZoneById(tzId); - - byte[] tzData = Encoding.ASCII.GetBytes(info.Id); - - // FIXME: This is not in ANY cases accurate, but the games don't care about the content of the buffer, they only pass it. - // TODO: Reverse the TZif2 conversion in PCV to make this match with real hardware. - context.Memory.WriteBytes(bufferPosition, tzData); - } - catch (TimeZoneNotFoundException) - { - Logger.PrintWarning(LogClass.ServiceTime, $"Timezone not found for string: {tzId} (len: {tzId.Length})"); - - resultCode = MakeError(ErrorModule.Time, 0x3dd); + MemoryHelper.Write(context.Memory, bufferPosition, rules); } return resultCode; } - private long ToCalendarTimeWithTz(ServiceCtx context, long posixTime, TimeZoneInfo info) - { - DateTime currentTime = Epoch.AddSeconds(posixTime); - - currentTime = TimeZoneInfo.ConvertTimeFromUtc(currentTime, info); - - context.ResponseData.Write((ushort)currentTime.Year); - context.ResponseData.Write((byte)currentTime.Month); - context.ResponseData.Write((byte)currentTime.Day); - context.ResponseData.Write((byte)currentTime.Hour); - context.ResponseData.Write((byte)currentTime.Minute); - context.ResponseData.Write((byte)currentTime.Second); - context.ResponseData.Write((byte)0); //MilliSecond ? - context.ResponseData.Write((int)currentTime.DayOfWeek); - context.ResponseData.Write(currentTime.DayOfYear - 1); - context.ResponseData.Write(new byte[8]); //TODO: Find out the names used. - context.ResponseData.Write((byte)(currentTime.IsDaylightSavingTime() ? 1 : 0)); - context.ResponseData.Write((int)info.GetUtcOffset(currentTime).TotalSeconds); - - return 0; - } - + // ToCalendarTime(nn::time::PosixTime time, buffer rules) -> (nn::time::CalendarTime, nn::time::sf::CalendarAdditionalInfo) public long ToCalendarTime(ServiceCtx context) { long posixTime = context.RequestData.ReadInt64(); @@ -170,111 +145,90 @@ namespace Ryujinx.HLE.HOS.Services.Time if (bufferSize != 0x4000) { - Logger.PrintWarning(LogClass.ServiceTime, $"TimeZoneRule buffer size is 0x{bufferSize:x} (expected 0x4000)"); + // TODO: find error code here + Logger.PrintError(LogClass.ServiceTime, $"TimeZoneRule buffer size is 0x{bufferSize:x} (expected 0x4000)"); + + throw new InvalidOperationException(); } - // TODO: Reverse the TZif2 conversion in PCV to make this match with real hardware. - byte[] tzData = context.Memory.ReadBytes(bufferPosition, 0x24); + TimeZoneRule rules = MemoryHelper.Read(context.Memory, bufferPosition); - string tzId = Encoding.ASCII.GetString(tzData).TrimEnd('\0'); + long resultCode = TimeZoneManager.ToCalendarTime(rules, posixTime, out CalendarInfo calendar); - long resultCode = 0; - - // Check if the Time Zone exists, otherwise error out. - try + if (resultCode == 0) { - TimeZoneInfo info = TimeZoneInfo.FindSystemTimeZoneById(tzId); - - resultCode = ToCalendarTimeWithTz(context, posixTime, info); - } - catch (TimeZoneNotFoundException) - { - Logger.PrintWarning(LogClass.ServiceTime, $"Timezone not found for string: {tzId} (len: {tzId.Length})"); - - resultCode = MakeError(ErrorModule.Time, 0x3dd); + context.ResponseData.WriteStruct(calendar); } return resultCode; } + // ToCalendarTimeWithMyRule(nn::time::PosixTime) -> (nn::time::CalendarTime, nn::time::sf::CalendarAdditionalInfo) public long ToCalendarTimeWithMyRule(ServiceCtx context) { long posixTime = context.RequestData.ReadInt64(); - return ToCalendarTimeWithTz(context, posixTime, _timeZone); - } + long resultCode = TimeZoneManager.Instance.ToCalendarTimeWithMyRules(posixTime, out CalendarInfo calendar); - public long ToPosixTime(ServiceCtx context) - { - long bufferPosition = context.Request.SendBuff[0].Position; - long bufferSize = context.Request.SendBuff[0].Size; - - ushort year = context.RequestData.ReadUInt16(); - byte month = context.RequestData.ReadByte(); - byte day = context.RequestData.ReadByte(); - byte hour = context.RequestData.ReadByte(); - byte minute = context.RequestData.ReadByte(); - byte second = context.RequestData.ReadByte(); - - DateTime calendarTime = new DateTime(year, month, day, hour, minute, second); - - if (bufferSize != 0x4000) + if (resultCode == 0) { - Logger.PrintWarning(LogClass.ServiceTime, $"TimeZoneRule buffer size is 0x{bufferSize:x} (expected 0x4000)"); - } - - // TODO: Reverse the TZif2 conversion in PCV to make this match with real hardware. - byte[] tzData = context.Memory.ReadBytes(bufferPosition, 0x24); - - string tzId = Encoding.ASCII.GetString(tzData).TrimEnd('\0'); - - long resultCode = 0; - - // Check if the Time Zone exists, otherwise error out. - try - { - TimeZoneInfo info = TimeZoneInfo.FindSystemTimeZoneById(tzId); - - return ToPosixTimeWithTz(context, calendarTime, info); - } - catch (TimeZoneNotFoundException) - { - Logger.PrintWarning(LogClass.ServiceTime, $"Timezone not found for string: {tzId} (len: {tzId.Length})"); - - resultCode = MakeError(ErrorModule.Time, 0x3dd); + context.ResponseData.WriteStruct(calendar); } return resultCode; } - public long ToPosixTimeWithMyRule(ServiceCtx context) + // ToPosixTime(nn::time::CalendarTime calendarTime, buffer rules) -> (u32 outCount, buffer) + public long ToPosixTime(ServiceCtx context) { - ushort year = context.RequestData.ReadUInt16(); - byte month = context.RequestData.ReadByte(); - byte day = context.RequestData.ReadByte(); - byte hour = context.RequestData.ReadByte(); - byte minute = context.RequestData.ReadByte(); - byte second = context.RequestData.ReadByte(); + long inBufferPosition = context.Request.SendBuff[0].Position; + long inBufferSize = context.Request.SendBuff[0].Size; - DateTime calendarTime = new DateTime(year, month, day, hour, minute, second, DateTimeKind.Local); + CalendarTime calendarTime = context.RequestData.ReadStruct(); - return ToPosixTimeWithTz(context, calendarTime, _timeZone); + if (inBufferSize != 0x4000) + { + // TODO: find error code here + Logger.PrintError(LogClass.ServiceTime, $"TimeZoneRule buffer size is 0x{inBufferSize:x} (expected 0x4000)"); + + throw new InvalidOperationException(); + } + + TimeZoneRule rules = MemoryHelper.Read(context.Memory, inBufferPosition); + + long resultCode = TimeZoneManager.ToPosixTime(rules, calendarTime, out long posixTime); + + if (resultCode == 0) + { + long outBufferPosition = context.Request.RecvListBuff[0].Position; + long outBufferSize = context.Request.RecvListBuff[0].Size; + + context.Memory.WriteInt64(outBufferPosition, posixTime); + context.ResponseData.Write(1); + } + + return resultCode; } - private long ToPosixTimeWithTz(ServiceCtx context, DateTime calendarTime, TimeZoneInfo info) + // ToPosixTimeWithMyRule(nn::time::CalendarTime calendarTime) -> (u32 outCount, buffer) + public long ToPosixTimeWithMyRule(ServiceCtx context) { - DateTime calenderTimeUtc = TimeZoneInfo.ConvertTimeToUtc(calendarTime, info); + CalendarTime calendarTime = context.RequestData.ReadStruct(); - long posixTime = ((DateTimeOffset)calenderTimeUtc).ToUnixTimeSeconds(); + long resultCode = TimeZoneManager.Instance.ToPosixTimeWithMyRules(calendarTime, out long posixTime); - long position = context.Request.RecvListBuff[0].Position; - long size = context.Request.RecvListBuff[0].Size; + if (resultCode == 0) + { + long outBufferPosition = context.Request.RecvListBuff[0].Position; + long outBufferSize = context.Request.RecvListBuff[0].Size; - context.Memory.WriteInt64(position, posixTime); + context.Memory.WriteInt64(outBufferPosition, posixTime); - context.ResponseData.Write(1); + // There could be only one result on one calendar as leap seconds aren't supported. + context.ResponseData.Write(1); + } - return 0; + return resultCode; } } } diff --git a/Ryujinx.HLE/HOS/Services/Time/ITimeZoneServiceTypes.cs b/Ryujinx.HLE/HOS/Services/Time/ITimeZoneServiceTypes.cs new file mode 100644 index 000000000..e50bd5d6e --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Time/ITimeZoneServiceTypes.cs @@ -0,0 +1,128 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Time +{ + [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 4)] + struct TimeTypeInfo + { + public int GmtOffset; + + [MarshalAs(UnmanagedType.I1)] + public bool IsDaySavingTime; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] + char[] Padding1; + + public int AbbreviationListIndex; + + [MarshalAs(UnmanagedType.I1)] + public bool IsStandardTimeDaylight; + + [MarshalAs(UnmanagedType.I1)] + public bool IsGMT; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] + char[] Padding2; + } + + [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 0x4000, CharSet = CharSet.Ansi)] + struct TimeZoneRule + { + public const int TzMaxTypes = 128; + public const int TzMaxChars = 50; + public const int TzMaxLeaps = 50; + public const int TzMaxTimes = 1000; + public const int TzNameMax = 255; + public const int TzCharsArraySize = 2 * (TzNameMax + 1); + + public int TimeCount; + public int TypeCount; + public int CharCount; + + [MarshalAs(UnmanagedType.I1)] + public bool GoBack; + + [MarshalAs(UnmanagedType.I1)] + public bool GoAhead; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = TzMaxTimes)] + public long[] Ats; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = TzMaxTimes)] + public byte[] Types; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = TzMaxTypes)] + public TimeTypeInfo[] Ttis; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = TzCharsArraySize)] + public char[] Chars; + + public int DefaultType; + } + + [StructLayout(LayoutKind.Sequential, Pack = 0x4, Size = 0x2C)] + struct TzifHeader + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public char[] Magic; + + public char Version; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 15)] + public byte[] Reserved; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] TtisGMTCount; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] TtisSTDCount; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] LeapCount; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] TimeCount; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] TypeCount; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] CharCount; + } + + [StructLayout(LayoutKind.Sequential, Pack = 0x4, Size = 0x8)] + struct CalendarTime + { + public short Year; + public sbyte Month; + public sbyte Day; + public sbyte Hour; + public sbyte Minute; + public sbyte Second; + } + + [StructLayout(LayoutKind.Sequential, Pack = 0x4, Size = 0x18, CharSet = CharSet.Ansi)] + struct CalendarAdditionalInfo + { + public uint DayOfWeek; + public uint DayOfYear; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public char[] TimezoneName; + + [MarshalAs(UnmanagedType.I1)] + public bool IsDaySavingTime; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] + char[] Padding; + + public int GmtOffset; + } + + [StructLayout(LayoutKind.Sequential, Pack = 0x4, Size = 0x20, CharSet = CharSet.Ansi)] + struct CalendarInfo + { + public CalendarTime Time; + public CalendarAdditionalInfo AdditionalInfo; + } +} diff --git a/Ryujinx.HLE/HOS/Services/Time/TimeError.cs b/Ryujinx.HLE/HOS/Services/Time/TimeError.cs new file mode 100644 index 000000000..20b2375c6 --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Time/TimeError.cs @@ -0,0 +1,12 @@ +namespace Ryujinx.HLE.HOS.Services.Time +{ + static class TimeError + { + public const int TimeNotFound = 200; + public const int Overflow = 201; + public const int LocationNameTooLong = 801; + public const int OutOfRange = 902; + public const int TimeZoneConversionFailed = 903; + public const int TimeZoneNotFound = 989; + } +} diff --git a/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZone.cs b/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZone.cs new file mode 100644 index 000000000..8039dc89d --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZone.cs @@ -0,0 +1,1707 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using Ryujinx.Common; +using Ryujinx.HLE.Utilities; +using static Ryujinx.HLE.HOS.Services.Time.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 const string TimeZoneDefaultRule = ",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(byte[] bytes) + { + if (BitConverter.IsLittleEndian) + { + Array.Reverse(bytes, 0, bytes.Length); + } + + return BitConverter.ToInt32(bytes, 0); + } + + private static unsafe int Detzcode32(int* data) + { + int result = *data; + if (BitConverter.IsLittleEndian) + { + byte[] bytes = BitConverter.GetBytes(result); + Array.Reverse(bytes, 0, bytes.Length); + result = BitConverter.ToInt32(bytes, 0); + } + + return result; + } + + private static unsafe long Detzcode64(long* data) + { + long result = *data; + if (BitConverter.IsLittleEndian) + { + byte[] bytes = BitConverter.GetBytes(result); + Array.Reverse(bytes, 0, bytes.Length); + result = BitConverter.ToInt64(bytes, 0); + } + + return result; + } + + private static bool DifferByRepeat(long t1, long t0) + { + return (t1 - t0) == SecondsPerRepeat; + } + + private static unsafe bool TimeTypeEquals(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]; + + fixed (char* chars = outRules.Chars) + { + return a.GmtOffset == b.GmtOffset && + a.IsDaySavingTime == b.IsDaySavingTime && + a.IsStandardTimeDaylight == b.IsStandardTimeDaylight && + a.IsGMT == b.IsGMT && + StringUtils.CompareCStr(chars + a.AbbreviationListIndex, chars + b.AbbreviationListIndex) == 0; + } + } + + private static int GetQZName(char[] name, int namePosition, char delimiter) + { + int i = namePosition; + + while (name[i] != '\0' && name[i] != delimiter) + { + i++; + } + + return i; + } + + private static int GetTZName(char[] name, int namePosition) + { + int i = namePosition; + + char c = name[i]; + + while (c != '\0' && !char.IsDigit(c) && c != ',' && c != '-' && c != '+') + { + c = name[i]; + i++; + } + + return i; + } + + private static bool GetNum(char[] name, ref int namePosition, out int num, int min, int max) + { + num = 0; + + char c = name[namePosition]; + + if (!char.IsDigit(c)) + { + return false; + } + + do + { + num = num * 10 + (c - '0'); + if (num > max) + { + return false; + } + + c = name[++namePosition]; + } + while (char.IsDigit(c)); + + if (num < min) + { + return false; + } + + return true; + } + + private static bool GetSeconds(char[] name, ref int namePosition, out int seconds) + { + seconds = 0; + + int num; + + bool isValid = GetNum(name, ref namePosition, out num, 0, HoursPerDays * DaysPerWekk - 1); + if (!isValid) + { + return false; + } + + seconds = num * SecondsPerHour; + if (name[namePosition] == ':') + { + namePosition++; + isValid = GetNum(name, ref namePosition, out num, 0, MinutesPerHour - 1); + if (!isValid) + { + return false; + } + + seconds += num * SecondsPerMinute; + 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(char[] name, ref int namePosition, ref int offset) + { + bool isNegative = false; + + if (name[namePosition] == '-') + { + isNegative = true; + namePosition++; + } + else if (name[namePosition] == '+') + { + namePosition++; + } + + bool isValid = GetSeconds(name, ref namePosition, out offset); + if (!isValid) + { + return false; + } + + if (isNegative) + { + offset = -offset; + } + + return true; + } + + private static bool GetRule(char[] 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(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(Span name, out TimeZoneRule outRules, bool lastDitch) + { + outRules = new TimeZoneRule + { + Ats = new long[TzMaxTimes], + Types = new byte[TzMaxTimes], + Ttis = new TimeTypeInfo[TzMaxTypes], + Chars = new char[TzCharsArraySize] + }; + + int stdLen; + Span 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.ToArray(), namePosition, '>'); + if (name[namePosition] != '>') + { + return false; + } + + stdLen = namePosition - stdNamePosition; + namePosition++; + } + else + { + namePosition = GetTZName(name.ToArray(), 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; + + Span 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.ToArray(), 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.ToCharArray(); + namePosition = 0; + } + + if (name[namePosition] == ',' || name[namePosition] == ';') + { + namePosition++; + + bool IsRuleValid = GetRule(name.ToArray(), ref namePosition, out Rule start); + if (!IsRuleValid) + { + return false; + } + + if (name[namePosition++] != ',') + { + return false; + } + + IsRuleValid = GetRule(name.ToArray(), 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, out TimeZoneRule outRules) + { + return ParsePosixName(name.ToCharArray(), out outRules, false); + } + + internal static unsafe bool LoadTimeZoneRules(out TimeZoneRule outRules, Stream inputData) + { + outRules = new TimeZoneRule + { + Ats = new long[TzMaxTimes], + Types = new byte[TzMaxTimes], + Ttis = new TimeTypeInfo[TzMaxTypes], + Chars = new char[TzCharsArraySize] + }; + + BinaryReader reader = new BinaryReader(inputData); + + long streamLength = reader.BaseStream.Length; + + if (streamLength < Marshal.SizeOf()) + { + return false; + } + + TzifHeader header = reader.ReadStruct(); + + streamLength -= Marshal.SizeOf(); + + 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; + + fixed (byte* workBufferPtrStart = workBuffer) + { + byte* p = workBufferPtrStart; + for (int i = 0; i < outRules.TimeCount; i++) + { + long at = Detzcode64((long*)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 += TimeTypeSize; + } + + timeCount = 0; + for (int i = 0; i < outRules.TimeCount; i++) + { + byte type = *p++; + 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((int*)p); + p += 4; + + if (*p >= 2) + { + return false; + } + + ttis.IsDaySavingTime = *p != 0; + p++; + + int abbreviationListIndex = *p++; + if (abbreviationListIndex >= outRules.CharCount) + { + return false; + } + + ttis.AbbreviationListIndex = abbreviationListIndex; + + outRules.Ttis[i] = ttis; + } + + fixed (char* chars = outRules.Chars) + { + Encoding.ASCII.GetChars(p, outRules.CharCount, chars, outRules.CharCount); + } + + 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 >= 2) + { + return false; + } + + outRules.Ttis[i].IsStandardTimeDaylight = *p++ != 0; + } + + } + + for (int i = 0; i < outRules.TypeCount; i++) + { + if (ttisSTDCount == 0) + { + outRules.Ttis[i].IsGMT = false; + } + else + { + if (*p >= 2) + { + return false; + } + + outRules.Ttis[i].IsGMT = *p++ != 0; + } + + } + + long position = (p - workBufferPtrStart); + 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(); + } + + char[] tempName = new char[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'; + + char[] name = new char[TzNameMax]; + Array.Copy(tempName, 1, name, 0, nRead - 1); + + if (ParsePosixName(name, out TimeZoneRule tempRules, false)) + { + int abbreviationCount = 0; + charCount = outRules.CharCount; + + fixed (char* chars = outRules.Chars) + { + for (int i = 0; i < tempRules.TypeCount; i++) + { + fixed (char* tempChars = tempRules.Chars) + { + char* 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(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(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 int 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() + { + TimezoneName = new char[8] + }; + + 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 TimeError.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 TimeError.OutOfRange; + } + + dayOfYear += YearLengths[IsLeap((int)year)]; + } + + while (dayOfYear >= YearLengths[IsLeap((int)year)]) + { + dayOfYear -= YearLengths[IsLeap((int)year)]; + + if (IncrementOverflow64(ref year, 1)) + { + return TimeError.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)]; + + while (dayOfYear >= ip[calendarTime.Month]) + { + calendarTime.Month += 1; + + dayOfYear -= ip[calendarTime.Month]; + } + + calendarTime.Day = (sbyte)(dayOfYear + 1); + + calendarAdditionalInfo.IsDaySavingTime = false; + calendarAdditionalInfo.GmtOffset = gmtOffset; + + return 0; + } + + private static int ToCalendarTimeInternal(TimeZoneRule rules, long time, out CalendarTimeInternal calendarTime, out CalendarAdditionalInfo calendarAdditionalInfo) + { + calendarTime = new CalendarTimeInternal(); + calendarAdditionalInfo = new CalendarAdditionalInfo() + { + TimezoneName = new char[8] + }; + + int 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 TimeError.TimeNotFound; + } + + result = ToCalendarTimeInternal(rules, newTime, out calendarTime, out calendarAdditionalInfo); + if (result != 0) + { + return result; + } + + if (time < rules.Ats[0]) + { + calendarTime.Year -= years; + } + else + { + calendarTime.Year += years; + } + + return 0; + } + + 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; + + unsafe + { + fixed (char* timeZoneAbbreviation = &rules.Chars[rules.Ttis[ttiIndex].AbbreviationListIndex]) + { + int timeZoneSize = Math.Min(StringUtils.LengthCstr(timeZoneAbbreviation), 8); + for (int i = 0; i < timeZoneSize; i++) + { + calendarAdditionalInfo.TimezoneName[i] = timeZoneAbbreviation[i]; + } + } + } + } + + return result; + } + + private static int ToPosixTimeInternal(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 TimeError.Overflow; + } + + calendarTime.Minute = (sbyte)minute; + + int day = calendarTime.Day; + if (NormalizeOverflow32(ref day, ref hour, HoursPerDays)) + { + return TimeError.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 TimeError.Overflow; + } + + calendarTime.Month = (sbyte)month; + + if (IncrementOverflow64(ref year, YearBase)) + { + return TimeError.Overflow; + } + + while (day <= 0) + { + if (IncrementOverflow64(ref year, -1)) + { + return TimeError.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 TimeError.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 TimeError.Overflow; + } + } + } + + calendarTime.Day = (sbyte)day; + + if (IncrementOverflow64(ref year, -YearBase)) + { + return TimeError.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 TimeError.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; + + int result = ToCalendarTimeInternal(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 TimeError.Overflow; + } + + posixTime = timeResult; + break; + } + else + { + if (pivot == low) + { + if (pivot == long.MaxValue) + { + return TimeError.TimeNotFound; + } + + pivot += 1; + low += 1; + } + else if (pivot == high) + { + if (pivot == long.MinValue) + { + return TimeError.TimeNotFound; + } + + pivot -= 1; + high -= 1; + } + + if (low > high) + { + return TimeError.TimeNotFound; + } + + if (direction > 0) + { + high = pivot; + } + else + { + low = pivot; + } + } + } + + return 0; + } + + internal static int ToCalendarTime(TimeZoneRule rules, long time, out CalendarInfo calendar) + { + int result = ToCalendarTimeInternal(rules, time, out CalendarTimeInternal calendarTime, out CalendarAdditionalInfo calendarAdditionalInfo); + + calendar = new CalendarInfo() + { + Time = new CalendarTime() + { + Year = (short)calendarTime.Year, + Month = calendarTime.Month, + Day = calendarTime.Day, + Hour = calendarTime.Hour, + Minute = calendarTime.Minute, + Second = calendarTime.Second + }, + AdditionalInfo = calendarAdditionalInfo + }; + + return result; + } + + internal static int ToPosixTime(TimeZoneRule rules, CalendarTime calendarTime, out long posixTime) + { + CalendarTimeInternal calendarTimeInternal = new CalendarTimeInternal() + { + Year = calendarTime.Year, + Month = calendarTime.Month, + Day = calendarTime.Day, + Hour = calendarTime.Hour, + Minute = calendarTime.Minute, + Second = calendarTime.Second + }; + + return ToPosixTimeInternal(rules, calendarTimeInternal, out posixTime); + } + } +} diff --git a/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZoneManager.cs b/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZoneManager.cs new file mode 100644 index 000000000..b3dc4c34e --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZoneManager.cs @@ -0,0 +1,289 @@ +using LibHac.Fs.NcaUtils; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.FileSystem; +using System; +using System.Collections.ObjectModel; +using LibHac.Fs; +using System.IO; +using System.Collections.Generic; +using TimeZoneConverter.Posix; +using TimeZoneConverter; + +using static Ryujinx.HLE.HOS.Services.Time.TimeZoneRule; +using static Ryujinx.HLE.HOS.ErrorCode; + +namespace Ryujinx.HLE.HOS.Services.Time.TimeZone +{ + public sealed class TimeZoneManager + { + private const long TimeZoneBinaryTitleId = 0x010000000000080E; + + private static TimeZoneManager instance; + + private static object instanceLock = new object(); + + private Switch _device; + private TimeZoneRule _myRules; + private string _deviceLocationName; + private string[] _locationNameCache; + + public static TimeZoneManager Instance + { + get + { + lock (instanceLock) + { + if (instance == null) + { + instance = new TimeZoneManager(); + } + + return instance; + } + } + } + + TimeZoneManager() + { + // Empty rules (UTC) + _myRules = new TimeZoneRule + { + Ats = new long[TzMaxTimes], + Types = new byte[TzMaxTimes], + Ttis = new TimeTypeInfo[TzMaxTypes], + Chars = new char[TzCharsArraySize] + }; + + _deviceLocationName = "UTC"; + } + + internal void Initialize(Switch device) + { + _device = device; + + InitializeLocationNameCache(); + } + + private void InitializeLocationNameCache() + { + if (HasTimeZoneBinaryTitle()) + { + using (IStorage ncaFileStream = new LocalStorage(_device.FileSystem.SwitchPathToSystemPath(GetTimeZoneBinaryTitleContentPath()), FileAccess.Read, FileMode.Open)) + { + Nca nca = new Nca(_device.System.KeySet, ncaFileStream); + IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel); + Stream binaryListStream = romfs.OpenFile("binaryList.txt", OpenMode.Read).AsStream(); + + StreamReader reader = new StreamReader(binaryListStream); + + List locationNameList = new List(); + + string locationName; + while ((locationName = reader.ReadLine()) != null) + { + locationNameList.Add(locationName); + } + + _locationNameCache = locationNameList.ToArray(); + } + } + else + { + ReadOnlyCollection timeZoneInfos = TimeZoneInfo.GetSystemTimeZones(); + _locationNameCache = new string[timeZoneInfos.Count]; + + int i = 0; + + foreach (TimeZoneInfo timeZoneInfo in timeZoneInfos) + { + bool needConversion = TZConvert.TryWindowsToIana(timeZoneInfo.Id, out string convertedName); + if (needConversion) + { + _locationNameCache[i] = convertedName; + } + else + { + _locationNameCache[i] = timeZoneInfo.Id; + } + i++; + } + + // As we aren't using the system archive, "UTC" might not exist on the host system. + // Load from C# TimeZone APIs UTC id. + string utcId = TimeZoneInfo.Utc.Id; + bool utcNeedConversion = TZConvert.TryWindowsToIana(utcId, out string utcConvertedName); + if (utcNeedConversion) + { + utcId = utcConvertedName; + } + + _deviceLocationName = utcId; + } + } + + private bool IsLocationNameValid(string locationName) + { + foreach (string cachedLocationName in _locationNameCache) + { + if (cachedLocationName.Equals(locationName)) + { + return true; + } + } + return false; + } + + public string GetDeviceLocationName() + { + return _deviceLocationName; + } + + public uint SetDeviceLocationName(string locationName) + { + uint resultCode = LoadTimeZoneRules(out TimeZoneRule rules, locationName); + + if (resultCode == 0) + { + _myRules = rules; + _deviceLocationName = locationName; + } + + return resultCode; + } + + public uint LoadLocationNameList(uint index, out string[] outLocationNameArray, uint maxLength) + { + List locationNameList = new List(); + + for (int i = 0; i < _locationNameCache.Length && i < maxLength; i++) + { + if (i < index) + { + continue; + } + + string locationName = _locationNameCache[i]; + + // If the location name is too long, error out. + if (locationName.Length > 0x24) + { + outLocationNameArray = new string[0]; + + return MakeError(ErrorModule.Time, TimeError.LocationNameTooLong); + } + + locationNameList.Add(locationName); + } + + outLocationNameArray = locationNameList.ToArray(); + + return 0; + } + + public uint GetTotalLocationNameCount() + { + return (uint)_locationNameCache.Length; + } + + public string GetTimeZoneBinaryTitleContentPath() + { + return _device.System.ContentManager.GetInstalledContentPath(TimeZoneBinaryTitleId, StorageId.NandSystem, ContentType.Data); + } + + public bool HasTimeZoneBinaryTitle() + { + return !string.IsNullOrEmpty(GetTimeZoneBinaryTitleContentPath()); + } + + internal uint LoadTimeZoneRules(out TimeZoneRule outRules, string locationName) + { + outRules = new TimeZoneRule + { + Ats = new long[TzMaxTimes], + Types = new byte[TzMaxTimes], + Ttis = new TimeTypeInfo[TzMaxTypes], + Chars = new char[TzCharsArraySize] + }; + + if (!IsLocationNameValid(locationName)) + { + return MakeError(ErrorModule.Time, TimeError.TimeZoneNotFound); + } + + if (!HasTimeZoneBinaryTitle()) + { + // If the user doesn't have the system archives, we generate a POSIX rule string and parse it to generate a incomplete TimeZoneRule + // TODO: As for now not having system archives is fine, we should enforce the usage of system archives later. + Logger.PrintWarning(LogClass.ServiceTime, "TimeZoneBinary system archive not found! Time conversions will not be accurate!"); + try + { + TimeZoneInfo info = TZConvert.GetTimeZoneInfo(locationName); + string posixRule = PosixTimeZone.FromTimeZoneInfo(info); + + if (!TimeZone.ParsePosixName(posixRule, out outRules)) + { + return MakeError(ErrorModule.Time, TimeError.TimeZoneConversionFailed); + } + + return 0; + } + catch (TimeZoneNotFoundException) + { + Logger.PrintWarning(LogClass.ServiceTime, $"Timezone not found for string: {locationName})"); + + return MakeError(ErrorModule.Time, TimeError.TimeZoneNotFound); + } + } + else + { + using (IStorage ncaFileStream = new LocalStorage(_device.FileSystem.SwitchPathToSystemPath(GetTimeZoneBinaryTitleContentPath()), FileAccess.Read, FileMode.Open)) + { + Nca nca = new Nca(_device.System.KeySet, ncaFileStream); + IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel); + Stream tzIfStream = romfs.OpenFile($"zoneinfo/{locationName}", OpenMode.Read).AsStream(); + + if (!TimeZone.LoadTimeZoneRules(out outRules, tzIfStream)) + { + return MakeError(ErrorModule.Time, TimeError.TimeZoneConversionFailed); + } + } + + return 0; + } + } + + internal uint ToCalendarTimeWithMyRules(long time, out CalendarInfo calendar) + { + return ToCalendarTime(_myRules, time, out calendar); + } + + internal static uint ToCalendarTime(TimeZoneRule rules, long time, out CalendarInfo calendar) + { + int error = TimeZone.ToCalendarTime(rules, time, out calendar); + + if (error != 0) + { + return MakeError(ErrorModule.Time, error); + } + + return 0; + } + + internal uint ToPosixTimeWithMyRules(CalendarTime calendarTime, out long posixTime) + { + return ToPosixTime(_myRules, calendarTime, out posixTime); + } + + internal static uint ToPosixTime(TimeZoneRule rules, CalendarTime calendarTime, out long posixTime) + { + int error = TimeZone.ToPosixTime(rules, calendarTime, out posixTime); + + if (error != 0) + { + return MakeError(ErrorModule.Time, error); + } + + return 0; + } + } +} diff --git a/Ryujinx.HLE/Ryujinx.HLE.csproj b/Ryujinx.HLE/Ryujinx.HLE.csproj index 5079f0303..50d84c3fd 100644 --- a/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -47,6 +47,7 @@ + diff --git a/Ryujinx.HLE/Utilities/StreamUtils.cs b/Ryujinx.HLE/Utilities/StreamUtils.cs new file mode 100644 index 000000000..7b44bc17a --- /dev/null +++ b/Ryujinx.HLE/Utilities/StreamUtils.cs @@ -0,0 +1,16 @@ +using System.IO; + +namespace Ryujinx.HLE.Utilities +{ + static class StreamUtils + { + public static byte[] StreamToBytes(Stream input) + { + using (MemoryStream ms = new MemoryStream()) + { + input.CopyTo(ms); + return ms.ToArray(); + } + } + } +} diff --git a/Ryujinx.HLE/Utilities/StringUtils.cs b/Ryujinx.HLE/Utilities/StringUtils.cs index ad18192ee..4ce0823c1 100644 --- a/Ryujinx.HLE/Utilities/StringUtils.cs +++ b/Ryujinx.HLE/Utilities/StringUtils.cs @@ -95,5 +95,31 @@ namespace Ryujinx.HLE.Utilities return Encoding.UTF8.GetString(ms.ToArray()); } } + + public static unsafe int CompareCStr(char* s1, char* s2) + { + int s1Index = 0; + int s2Index = 0; + + while (s1[s1Index] != 0 && s2[s2Index] != 0 && s1[s1Index] == s2[s2Index]) + { + s1Index += 1; + s2Index += 1; + } + + return s2[s2Index] - s1[s1Index]; + } + + public static unsafe int LengthCstr(char* s) + { + int i = 0; + + while (s[i] != '\0') + { + i++; + } + + return i; + } } }