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 ;
using LibHac.FsSystem ;
using LibHac.FsSystem.NcaUtils ;
2019-10-08 05:48:49 +02:00
using Ryujinx.Common.Logging ;
2020-03-25 23:23:21 +01:00
using Ryujinx.Configuration ;
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-03-29 23:23:05 +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#requirements for more information)" ;
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
}
public string SanityCheckDeviceLocationName ( )
{
string locationName = ConfigurationState . Instance . System . TimeZone ;
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
ConfigurationState . Instance . System . TimeZone . Value = "UTC" ;
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 ) ;
2020-03-25 23:23:21 +01:00
string deviceLocationName = SanityCheckDeviceLocationName ( ) ;
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
2020-03-25 09:14:35 +01:00
romfs . OpenFile ( out IFile binaryListFile , "/binaryList.txt" . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2019-10-17 08:17:44 +02:00
StreamReader reader = new StreamReader ( binaryListFile . 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 ;
}
if ( romfs . OpenFile ( out IFile tzif , $"/zoneinfo/{locName}" . ToU8Span ( ) , OpenMode . Read ) . IsFailure ( ) )
{
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 ;
}
using ( tzif )
{
TimeZone . ParseTimeZoneBinary ( out TimeZoneRule tzRule , tzif . AsStream ( ) ) ;
TimeTypeInfo ttInfo ;
if ( tzRule . TimeCount > 0 ) // Find the current transition period
{
int fin = 0 ;
for ( int i = 0 ; i < tzRule . TimeCount ; + + i )
{
if ( tzRule . Ats [ i ] < = now )
{
fin = i ;
}
}
ttInfo = tzRule . Ttis [ tzRule . Types [ fin ] ] ;
}
else if ( tzRule . TypeCount > = 1 ) // Otherwise, use the first offset in TTInfo
{
ttInfo = tzRule . Ttis [ 0 ] ;
}
else
{
2020-08-04 01:32:53 +02:00
Logger . Error ? . Print ( LogClass . ServiceTime , $"Couldn't find UTC offset for zone {locName}" ) ;
2020-07-21 06:14:42 +02:00
continue ;
}
var abbrStart = tzRule . Chars . AsSpan ( ttInfo . AbbreviationListIndex ) ;
int abbrEnd = abbrStart . IndexOf ( '\0' ) ;
outList . Add ( ( ttInfo . GmtOffset , locName , abbrStart . Slice ( 0 , abbrEnd ) . ToString ( ) ) ) ;
}
}
}
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
2020-03-25 09:14:35 +01:00
Result result = romfs . OpenFile ( out IFile timeZoneBinaryFile , $"/zoneinfo/{locationName}" . ToU8Span ( ) , OpenMode . Read ) ;
2019-10-08 05:48:49 +02:00
2019-10-17 08:17:44 +02:00
timeZoneBinaryStream = timeZoneBinaryFile . AsStream ( ) ;
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 ;
}
}
}