diff --git a/Ryujinx.HLE/FileSystem/Content/ContentManager.cs b/Ryujinx.HLE/FileSystem/Content/ContentManager.cs index 67c3f2635f..f330e318b4 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/Horizon.cs b/Ryujinx.HLE/HOS/Horizon.cs index 9590cabf40..95cd30f0f7 100644 --- a/Ryujinx.HLE/HOS/Horizon.cs +++ b/Ryujinx.HLE/HOS/Horizon.cs @@ -9,7 +9,6 @@ using Ryujinx.HLE.HOS.Kernel.Memory; using Ryujinx.HLE.HOS.Kernel.Process; using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.HLE.HOS.Services.Sm; -using Ryujinx.HLE.HOS.Services.Time.TimeZone; using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.Loaders.Executables; using Ryujinx.HLE.Loaders.Npdm; @@ -196,8 +195,6 @@ namespace Ryujinx.HLE.HOS LoadKeySet(); ContentManager = new ContentManager(device); - - TimeZoneManager.Instance.Initialize(ContentManager); } public void LoadCart(string exeFsDir, string romFsFile = null) diff --git a/Ryujinx.HLE/HOS/Services/Time/ITimeZoneService.cs b/Ryujinx.HLE/HOS/Services/Time/ITimeZoneService.cs index 41ff087de7..4a888227f0 100644 --- a/Ryujinx.HLE/HOS/Services/Time/ITimeZoneService.cs +++ b/Ryujinx.HLE/HOS/Services/Time/ITimeZoneService.cs @@ -17,10 +17,6 @@ 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 @@ -42,12 +38,17 @@ namespace Ryujinx.HLE.HOS.Services.Time // 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); @@ -59,28 +60,14 @@ namespace Ryujinx.HLE.HOS.Services.Time // SetDeviceLocationName(nn::time::LocationName) public long SetDeviceLocationName(ServiceCtx context) { - byte[] locationName = context.RequestData.ReadBytes(0x24); - - string tzId = Encoding.ASCII.GetString(locationName).TrimEnd('\0'); - - long resultCode = 0; - - try - { - _timeZone = TimeZoneInfo.FindSystemTimeZoneById(tzId); - } - catch (TimeZoneNotFoundException) - { - resultCode = MakeError(ErrorModule.Time, TimeError.TimeZoneNotFound); - } - - return resultCode; + string locationName = Encoding.ASCII.GetString(context.RequestData.ReadBytes(0x24)).TrimEnd('\0'); + 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; } @@ -90,28 +77,33 @@ namespace Ryujinx.HLE.HOS.Services.Time { // TODO: fix logic to use index uint index = context.RequestData.ReadUInt32(); - long bufferPosition = context.Response.SendBuff[0].Position; - long bufferSize = context.Response.SendBuff[0].Size; + 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 i = 0; i < padding; i++) + 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 diff --git a/Ryujinx.HLE/HOS/Services/Time/TimeError.cs b/Ryujinx.HLE/HOS/Services/Time/TimeError.cs index 334cfa1287..20b2375c61 100644 --- a/Ryujinx.HLE/HOS/Services/Time/TimeError.cs +++ b/Ryujinx.HLE/HOS/Services/Time/TimeError.cs @@ -4,6 +4,7 @@ { 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 index 0673a7e1dc..7aee0093df 100644 --- a/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZone.cs +++ b/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZone.cs @@ -880,6 +880,17 @@ namespace Ryujinx.HLE.HOS.Services.Time.TimeZone } } + public static bool ParsePosixName(string name, out TimeZoneRule outRules) + { + unsafe + { + fixed (char *namePtr = name.ToCharArray()) + { + return ParsePosixName(namePtr, out outRules, false); + } + } + } + public static unsafe bool LoadTimeZoneRules(out TimeZoneRule outRules, Stream inputData) { outRules = new TimeZoneRule diff --git a/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZoneManager.cs b/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZoneManager.cs index a9858cb739..1e25b0791a 100644 --- a/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZoneManager.cs +++ b/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZoneManager.cs @@ -1,8 +1,13 @@ using LibHac.Fs.NcaUtils; using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.FileSystem.Content; 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; @@ -18,14 +23,16 @@ namespace Ryujinx.HLE.HOS.Services.Time.TimeZone private static object instanceLock = new object(); - private ContentManager _contentManager; - private TimeZoneRule _myRules; + private Switch _device; + private TimeZoneRule _myRules; + private string _deviceLocationName; + private string[] _locationNameCache; TimeZoneManager() { - _contentManager = null; + _device = null; - // Empty rules + // Empty rules (UTC) _myRules = new TimeZoneRule { ats = new long[TZ_MAX_TIMES], @@ -33,16 +40,139 @@ namespace Ryujinx.HLE.HOS.Services.Time.TimeZone ttis = new TimeTypeInfo[TZ_MAX_TYPES], chars = new char[TZ_NAME_MAX] }; + + _deviceLocationName = "UTC"; } - internal void Initialize(ContentManager contentManager) + internal void Initialize(Switch device) { - _contentManager = contentManager; + _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 _contentManager.GetInstalledContentPath(TimeZoneBinaryTitleId, StorageId.NandSystem, ContentType.Data); + return _device.System.ContentManager.GetInstalledContentPath(TimeZoneBinaryTitleId, StorageId.NandSystem, ContentType.Data); } public bool HasTimeZoneBinaryTitle() @@ -60,15 +190,27 @@ namespace Ryujinx.HLE.HOS.Services.Time.TimeZone chars = new char[TZ_NAME_MAX] }; + if (!IsLocationNameValid(locationName)) + { + return MakeError(ErrorModule.Time, TimeError.TimeZoneNotFound); + } + if (!HasTimeZoneBinaryTitle()) { - Logger.PrintWarning(LogClass.ServiceTime, "TimeZoneBinary system archive not found! Time conversion might not be accurate!"); + // 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 = TimeZoneInfo.FindSystemTimeZoneById(locationName); + TimeZoneInfo info = TZConvert.GetTimeZoneInfo(locationName); + string posixRule = PosixTimeZone.FromTimeZoneInfo(info); - // TODO convert TimeZoneInfo to a TimeZoneRule - throw new NotImplementedException(); + if (!TimeZone.ParsePosixName(posixRule, out outRules)) + { + return MakeError(ErrorModule.Time, TimeError.TimeZoneConversionFailed); + } + + return 0; } catch (TimeZoneNotFoundException) { @@ -79,8 +221,20 @@ namespace Ryujinx.HLE.HOS.Services.Time.TimeZone } else { - // TODO: system archive loading - throw new NotImplementedException(); + 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; } } diff --git a/Ryujinx.HLE/Ryujinx.HLE.csproj b/Ryujinx.HLE/Ryujinx.HLE.csproj index 5079f03035..50d84c3fd7 100644 --- a/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -47,6 +47,7 @@ +