2019-10-17 08:17:44 +02:00
using LibHac ;
2020-03-25 09:14:35 +01:00
using LibHac.Common ;
2019-10-17 08:17:44 +02:00
using LibHac.Fs ;
2020-09-01 22:08:59 +02:00
using LibHac.Fs.Fsa ;
2019-10-17 08:17:44 +02:00
using LibHac.FsSystem ;
2022-01-12 12:22:19 +01:00
using LibHac.Tools.FsSystem ;
using LibHac.Tools.FsSystem.NcaUtils ;
2019-10-08 05:48:49 +02:00
using Ryujinx.Common.Logging ;
2019-10-16 02:30:36 +02:00
using Ryujinx.HLE.Exceptions ;
2019-10-08 05:48:49 +02:00
using Ryujinx.HLE.FileSystem ;
2020-03-25 23:23:21 +01:00
using Ryujinx.HLE.FileSystem.Content ;
2019-10-08 05:48:49 +02:00
using Ryujinx.HLE.HOS.Services.Time.Clock ;
using Ryujinx.HLE.Utilities ;
2020-07-21 06:14:42 +02:00
using System ;
2019-10-08 05:48:49 +02:00
using System.Collections.Generic ;
using System.IO ;
using static Ryujinx . HLE . HOS . Services . Time . TimeZone . TimeZoneRule ;
namespace Ryujinx.HLE.HOS.Services.Time.TimeZone
{
2020-03-25 23:23:21 +01:00
public class TimeZoneContentManager
2019-10-08 05:48:49 +02:00
{
private const long TimeZoneBinaryTitleId = 0x010000000000080E ;
2020-08-30 18:35:42 +02:00
private readonly string TimeZoneSystemTitleMissingErrorMessage = "TimeZoneBinary system title not found! TimeZone conversions will not work, provide the system archive to fix this error. (See https://github.com/Ryujinx/Ryujinx/wiki/Ryujinx-Setup-&-Configuration-Guide#initial-setup-continued---installation-of-firmware for more information)" ;
2020-03-29 23:23:05 +02:00
2020-03-25 23:23:21 +01:00
private VirtualFileSystem _virtualFileSystem ;
private IntegrityCheckLevel _fsIntegrityCheckLevel ;
private ContentManager _contentManager ;
2019-10-08 05:48:49 +02:00
2020-03-25 23:23:21 +01:00
public string [ ] LocationNameCache { get ; private set ; }
internal TimeZoneManager Manager { get ; private set ; }
2019-10-08 05:48:49 +02:00
public TimeZoneContentManager ( )
{
Manager = new TimeZoneManager ( ) ;
}
2020-03-25 23:23:21 +01:00
public void InitializeInstance ( VirtualFileSystem virtualFileSystem , ContentManager contentManager , IntegrityCheckLevel fsIntegrityCheckLevel )
2019-10-08 05:48:49 +02:00
{
2020-03-25 23:23:21 +01:00
_virtualFileSystem = virtualFileSystem ;
_contentManager = contentManager ;
_fsIntegrityCheckLevel = fsIntegrityCheckLevel ;
2019-10-08 05:48:49 +02:00
InitializeLocationNameCache ( ) ;
2020-03-25 23:23:21 +01:00
}
2021-05-16 17:12:14 +02:00
public string SanityCheckDeviceLocationName ( string locationName )
2020-03-25 23:23:21 +01:00
{
if ( IsLocationNameValid ( locationName ) )
{
return locationName ;
}
2020-08-04 01:32:53 +02:00
Logger . Warning ? . Print ( LogClass . ServiceTime , $"Invalid device TimeZone {locationName}, switching back to UTC" ) ;
2020-03-25 23:23:21 +01:00
return "UTC" ;
}
internal void Initialize ( TimeManager timeManager , Switch device )
{
InitializeInstance ( device . FileSystem , device . System . ContentManager , device . System . FsIntegrityCheckLevel ) ;
2019-10-08 05:48:49 +02:00
SteadyClockTimePoint timeZoneUpdatedTimePoint = timeManager . StandardSteadyClock . GetCurrentTimePoint ( null ) ;
2021-05-16 17:12:14 +02:00
string deviceLocationName = SanityCheckDeviceLocationName ( device . Configuration . TimeZone ) ;
2020-03-25 23:23:21 +01:00
ResultCode result = GetTimeZoneBinary ( deviceLocationName , out Stream timeZoneBinaryStream , out LocalStorage ncaFile ) ;
2019-10-08 05:48:49 +02:00
if ( result = = ResultCode . Success )
{
// TODO: Read TimeZoneVersion from sysarchive.
2020-03-25 23:23:21 +01:00
timeManager . SetupTimeZoneManager ( deviceLocationName , timeZoneUpdatedTimePoint , ( uint ) LocationNameCache . Length , new UInt128 ( ) , timeZoneBinaryStream ) ;
2019-10-11 18:05:10 +02:00
ncaFile . Dispose ( ) ;
2019-10-08 05:48:49 +02:00
}
else
{
// In the case the user don't have the timezone system archive, we just mark the manager as initialized.
Manager . MarkInitialized ( ) ;
}
}
private void InitializeLocationNameCache ( )
{
if ( HasTimeZoneBinaryTitle ( ) )
{
2020-03-25 23:23:21 +01:00
using ( IStorage ncaFileStream = new LocalStorage ( _virtualFileSystem . SwitchPathToSystemPath ( GetTimeZoneBinaryTitleContentPath ( ) ) , FileAccess . Read , FileMode . Open ) )
2019-10-08 05:48:49 +02:00
{
2020-03-25 23:23:21 +01:00
Nca nca = new Nca ( _virtualFileSystem . KeySet , ncaFileStream ) ;
IFileSystem romfs = nca . OpenFileSystem ( NcaSectionType . Data , _fsIntegrityCheckLevel ) ;
2019-10-08 05:48:49 +02:00
2021-12-23 17:55:50 +01:00
using var binaryListFile = new UniqueRef < IFile > ( ) ;
2019-10-17 08:17:44 +02:00
2021-12-23 17:55:50 +01:00
romfs . OpenFile ( ref binaryListFile . Ref ( ) , "/binaryList.txt" . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
StreamReader reader = new StreamReader ( binaryListFile . Get . AsStream ( ) ) ;
2019-10-08 05:48:49 +02:00
List < string > locationNameList = new List < string > ( ) ;
string locationName ;
while ( ( locationName = reader . ReadLine ( ) ) ! = null )
{
locationNameList . Add ( locationName ) ;
}
2020-03-25 23:23:21 +01:00
LocationNameCache = locationNameList . ToArray ( ) ;
2019-10-08 05:48:49 +02:00
}
}
else
{
2020-03-25 23:23:21 +01:00
LocationNameCache = new string [ ] { "UTC" } ;
2019-10-08 05:48:49 +02:00
2020-08-04 01:32:53 +02:00
Logger . Error ? . Print ( LogClass . ServiceTime , TimeZoneSystemTitleMissingErrorMessage ) ;
2019-10-08 05:48:49 +02:00
}
}
2020-07-21 06:14:42 +02:00
public IEnumerable < ( int Offset , string Location , string Abbr ) > ParseTzOffsets ( )
{
var tzBinaryContentPath = GetTimeZoneBinaryTitleContentPath ( ) ;
if ( string . IsNullOrEmpty ( tzBinaryContentPath ) )
{
return new [ ] { ( 0 , "UTC" , "UTC" ) } ;
}
List < ( int Offset , string Location , string Abbr ) > outList = new List < ( int Offset , string Location , string Abbr ) > ( ) ;
var now = System . DateTimeOffset . Now . ToUnixTimeSeconds ( ) ;
using ( IStorage ncaStorage = new LocalStorage ( _virtualFileSystem . SwitchPathToSystemPath ( tzBinaryContentPath ) , FileAccess . Read , FileMode . Open ) )
using ( IFileSystem romfs = new Nca ( _virtualFileSystem . KeySet , ncaStorage ) . OpenFileSystem ( NcaSectionType . Data , _fsIntegrityCheckLevel ) )
{
foreach ( string locName in LocationNameCache )
{
if ( locName . StartsWith ( "Etc" ) )
{
continue ;
}
2021-12-23 17:55:50 +01:00
using var tzif = new UniqueRef < IFile > ( ) ;
if ( romfs . OpenFile ( ref tzif . Ref ( ) , $"/zoneinfo/{locName}" . ToU8Span ( ) , OpenMode . Read ) . IsFailure ( ) )
2020-07-21 06:14:42 +02:00
{
2020-08-04 01:32:53 +02:00
Logger . Error ? . Print ( LogClass . ServiceTime , $"Error opening /zoneinfo/{locName}" ) ;
2020-07-21 06:14:42 +02:00
continue ;
}
2021-12-23 17:55:50 +01:00
TimeZone . ParseTimeZoneBinary ( out TimeZoneRule tzRule , tzif . Get . AsStream ( ) ) ;
2020-07-21 06:14:42 +02:00
2021-12-23 17:55:50 +01:00
TimeTypeInfo ttInfo ;
if ( tzRule . TimeCount > 0 ) // Find the current transition period
{
int fin = 0 ;
for ( int i = 0 ; i < tzRule . TimeCount ; + + i )
2020-07-21 06:14:42 +02:00
{
2021-12-23 17:55:50 +01:00
if ( tzRule . Ats [ i ] < = now )
2020-07-21 06:14:42 +02:00
{
2021-12-23 17:55:50 +01:00
fin = i ;
2020-07-21 06:14:42 +02:00
}
}
2021-12-23 17:55:50 +01:00
ttInfo = tzRule . Ttis [ tzRule . Types [ fin ] ] ;
}
else if ( tzRule . TypeCount > = 1 ) // Otherwise, use the first offset in TTInfo
{
ttInfo = tzRule . Ttis [ 0 ] ;
}
else
{
Logger . Error ? . Print ( LogClass . ServiceTime , $"Couldn't find UTC offset for zone {locName}" ) ;
continue ;
}
2020-07-21 06:14:42 +02:00
2021-12-23 17:55:50 +01:00
var abbrStart = tzRule . Chars . AsSpan ( ttInfo . AbbreviationListIndex ) ;
int abbrEnd = abbrStart . IndexOf ( '\0' ) ;
2020-07-21 06:14:42 +02:00
2021-12-23 17:55:50 +01:00
outList . Add ( ( ttInfo . GmtOffset , locName , abbrStart . Slice ( 0 , abbrEnd ) . ToString ( ) ) ) ;
2020-07-21 06:14:42 +02:00
}
}
outList . Sort ( ) ;
return outList ;
}
2019-10-08 05:48:49 +02:00
private bool IsLocationNameValid ( string locationName )
{
2020-03-25 23:23:21 +01:00
foreach ( string cachedLocationName in LocationNameCache )
2019-10-08 05:48:49 +02:00
{
if ( cachedLocationName . Equals ( locationName ) )
{
return true ;
}
}
return false ;
}
public ResultCode SetDeviceLocationName ( string locationName )
{
2019-10-11 18:05:10 +02:00
ResultCode result = GetTimeZoneBinary ( locationName , out Stream timeZoneBinaryStream , out LocalStorage ncaFile ) ;
2019-10-08 05:48:49 +02:00
if ( result = = ResultCode . Success )
{
result = Manager . SetDeviceLocationNameWithTimeZoneRule ( locationName , timeZoneBinaryStream ) ;
2019-10-11 18:05:10 +02:00
ncaFile . Dispose ( ) ;
2019-10-08 05:48:49 +02:00
}
return result ;
}
public ResultCode LoadLocationNameList ( uint index , out string [ ] outLocationNameArray , uint maxLength )
{
List < string > locationNameList = new List < string > ( ) ;
2020-03-25 23:23:21 +01:00
for ( int i = 0 ; i < LocationNameCache . Length & & i < maxLength ; i + + )
2019-10-08 05:48:49 +02:00
{
if ( i < index )
{
continue ;
}
2020-03-25 23:23:21 +01:00
string locationName = LocationNameCache [ i ] ;
2019-10-08 05:48:49 +02:00
// If the location name is too long, error out.
if ( locationName . Length > 0x24 )
{
outLocationNameArray = new string [ 0 ] ;
return ResultCode . LocationNameTooLong ;
}
locationNameList . Add ( locationName ) ;
}
outLocationNameArray = locationNameList . ToArray ( ) ;
return ResultCode . Success ;
}
public string GetTimeZoneBinaryTitleContentPath ( )
{
2020-03-25 23:23:21 +01:00
return _contentManager . GetInstalledContentPath ( TimeZoneBinaryTitleId , StorageId . NandSystem , NcaContentType . Data ) ;
2019-10-08 05:48:49 +02:00
}
public bool HasTimeZoneBinaryTitle ( )
{
return ! string . IsNullOrEmpty ( GetTimeZoneBinaryTitleContentPath ( ) ) ;
}
2019-10-11 18:05:10 +02:00
internal ResultCode GetTimeZoneBinary ( string locationName , out Stream timeZoneBinaryStream , out LocalStorage ncaFile )
2019-10-08 05:48:49 +02:00
{
timeZoneBinaryStream = null ;
2019-10-11 18:05:10 +02:00
ncaFile = null ;
2019-10-08 05:48:49 +02:00
2020-03-29 23:23:05 +02:00
if ( ! HasTimeZoneBinaryTitle ( ) | | ! IsLocationNameValid ( locationName ) )
2019-10-08 05:48:49 +02:00
{
return ResultCode . TimeZoneNotFound ;
}
2020-03-25 23:23:21 +01:00
ncaFile = new LocalStorage ( _virtualFileSystem . SwitchPathToSystemPath ( GetTimeZoneBinaryTitleContentPath ( ) ) , FileAccess . Read , FileMode . Open ) ;
2019-10-08 05:48:49 +02:00
2020-03-25 23:23:21 +01:00
Nca nca = new Nca ( _virtualFileSystem . KeySet , ncaFile ) ;
IFileSystem romfs = nca . OpenFileSystem ( NcaSectionType . Data , _fsIntegrityCheckLevel ) ;
2019-10-11 18:05:10 +02:00
2021-12-23 17:55:50 +01:00
using var timeZoneBinaryFile = new UniqueRef < IFile > ( ) ;
Result result = romfs . OpenFile ( ref timeZoneBinaryFile . Ref ( ) , $"/zoneinfo/{locationName}" . ToU8Span ( ) , OpenMode . Read ) ;
2019-10-08 05:48:49 +02:00
2021-12-23 17:55:50 +01:00
timeZoneBinaryStream = timeZoneBinaryFile . Release ( ) . AsStream ( ) ;
2019-10-17 08:17:44 +02:00
return ( ResultCode ) result . Value ;
2019-10-08 05:48:49 +02:00
}
internal ResultCode LoadTimeZoneRule ( 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 ( ! HasTimeZoneBinaryTitle ( ) )
{
2020-03-29 23:23:05 +02:00
throw new InvalidSystemResourceException ( TimeZoneSystemTitleMissingErrorMessage ) ;
2019-10-08 05:48:49 +02:00
}
2019-10-11 18:05:10 +02:00
ResultCode result = GetTimeZoneBinary ( locationName , out Stream timeZoneBinaryStream , out LocalStorage ncaFile ) ;
2019-10-08 05:48:49 +02:00
if ( result = = ResultCode . Success )
{
result = Manager . ParseTimeZoneRuleBinary ( out outRules , timeZoneBinaryStream ) ;
2019-10-11 18:05:10 +02:00
ncaFile . Dispose ( ) ;
2019-10-08 05:48:49 +02:00
}
return result ;
}
}
2020-08-30 18:35:42 +02:00
}