2020-01-21 17:23:11 -05:00
using LibHac ;
2020-05-15 02:16:46 -04:00
using LibHac.Common ;
2021-08-12 17:56:24 -04:00
using LibHac.Common.Keys ;
2020-01-21 17:23:11 -05:00
using LibHac.Fs ;
2020-09-01 16:08:59 -04:00
using LibHac.Fs.Fsa ;
2021-08-12 17:56:24 -04:00
using LibHac.Fs.Shim ;
2020-09-01 16:08:59 -04:00
using LibHac.FsSrv ;
2020-01-21 17:23:11 -05:00
using LibHac.FsSystem ;
2021-08-12 17:56:24 -04:00
using LibHac.Ncm ;
2020-05-15 02:16:46 -04:00
using LibHac.Spl ;
2022-01-12 06:22:19 -05:00
using LibHac.Tools.Es ;
using LibHac.Tools.Fs ;
using LibHac.Tools.FsSystem ;
2020-08-30 12:51:53 -04:00
using Ryujinx.Common.Configuration ;
2021-08-12 17:56:24 -04:00
using Ryujinx.Common.Logging ;
2018-09-08 18:04:26 -04:00
using Ryujinx.HLE.HOS ;
2018-02-04 18:08:20 -05:00
using System ;
2021-08-12 17:56:24 -04:00
using System.Buffers.Text ;
2023-01-18 08:50:42 -05:00
using System.Collections.Concurrent ;
2021-08-12 17:56:24 -04:00
using System.Collections.Generic ;
2018-02-04 18:08:20 -05:00
using System.IO ;
2021-08-12 17:56:24 -04:00
using System.Runtime.CompilerServices ;
2021-12-23 11:55:50 -05:00
using Path = System . IO . Path ;
2021-08-12 17:56:24 -04:00
using RightsId = LibHac . Fs . RightsId ;
2018-02-04 18:08:20 -05:00
2018-09-08 18:04:26 -04:00
namespace Ryujinx.HLE.FileSystem
2018-02-04 18:08:20 -05:00
{
2019-09-02 12:03:57 -04:00
public class VirtualFileSystem : IDisposable
2018-02-04 18:08:20 -05:00
{
2022-03-22 15:46:16 -04:00
public static string SafeNandPath = Path . Combine ( AppDataManager . DefaultNandDir , "safe" ) ;
public static string SystemNandPath = Path . Combine ( AppDataManager . DefaultNandDir , "system" ) ;
public static string UserNandPath = Path . Combine ( AppDataManager . DefaultNandDir , "user" ) ;
2018-09-08 18:04:26 -04:00
2022-03-22 15:46:16 -04:00
public KeySet KeySet { get ; private set ; }
public EmulatedGameCard GameCard { get ; private set ; }
public EmulatedSdCard SdCard { get ; private set ; }
public ModLoader ModLoader { get ; private set ; }
2023-01-18 08:50:42 -05:00
private readonly ConcurrentDictionary < ulong , Stream > _romFsByPid ;
2021-08-12 17:56:24 -04:00
2020-01-24 11:01:21 -05:00
private static bool _isInitialized = false ;
2022-03-22 15:46:16 -04:00
public static VirtualFileSystem CreateInstance ( )
{
if ( _isInitialized )
{
throw new InvalidOperationException ( "VirtualFileSystem can only be instantiated once!" ) ;
}
2020-01-21 17:23:11 -05:00
2022-03-22 15:46:16 -04:00
_isInitialized = true ;
return new VirtualFileSystem ( ) ;
}
2020-07-09 00:31:15 -04:00
2020-01-24 11:01:21 -05:00
private VirtualFileSystem ( )
2020-01-21 17:23:11 -05:00
{
2021-08-12 17:56:24 -04:00
ReloadKeySet ( ) ;
2020-07-09 00:31:15 -04:00
ModLoader = new ModLoader ( ) ; // Should only be created once
2023-01-18 08:50:42 -05:00
_romFsByPid = new ConcurrentDictionary < ulong , Stream > ( ) ;
}
public void LoadRomFs ( ulong pid , string fileName )
{
var romfsStream = new FileStream ( fileName , FileMode . Open , FileAccess . Read ) ;
_romFsByPid . AddOrUpdate ( pid , romfsStream , ( pid , oldStream ) = >
{
oldStream . Close ( ) ;
return romfsStream ;
} ) ;
2020-01-21 17:23:11 -05:00
}
2023-01-18 08:50:42 -05:00
public void SetRomFs ( ulong pid , Stream romfsStream )
2018-02-04 18:08:20 -05:00
{
2023-01-18 08:50:42 -05:00
_romFsByPid . AddOrUpdate ( pid , romfsStream , ( pid , oldStream ) = >
{
oldStream . Close ( ) ;
return romfsStream ;
} ) ;
2018-02-04 18:08:20 -05:00
}
2023-01-18 08:50:42 -05:00
public Stream GetRomFs ( ulong pid )
2018-09-08 14:33:27 -04:00
{
2023-01-18 08:50:42 -05:00
return _romFsByPid [ pid ] ;
2018-09-08 14:33:27 -04:00
}
2018-12-06 06:16:24 -05:00
public string GetFullPath ( string basePath , string fileName )
2018-02-04 18:08:20 -05:00
{
2018-12-06 06:16:24 -05:00
if ( fileName . StartsWith ( "//" ) )
2018-03-06 15:27:50 -05:00
{
2018-12-06 06:16:24 -05:00
fileName = fileName . Substring ( 2 ) ;
2018-03-06 15:27:50 -05:00
}
2018-12-06 06:16:24 -05:00
else if ( fileName . StartsWith ( '/' ) )
2018-02-04 18:08:20 -05:00
{
2018-12-06 06:16:24 -05:00
fileName = fileName . Substring ( 1 ) ;
2018-02-04 18:08:20 -05:00
}
2018-03-06 15:27:50 -05:00
else
{
return null ;
}
2018-02-04 18:08:20 -05:00
2018-12-06 06:16:24 -05:00
string fullPath = Path . GetFullPath ( Path . Combine ( basePath , fileName ) ) ;
2018-02-04 18:08:20 -05:00
2022-03-22 15:46:16 -04:00
if ( ! fullPath . StartsWith ( AppDataManager . BaseDirPath ) )
2018-02-04 18:08:20 -05:00
{
return null ;
}
2018-12-06 06:16:24 -05:00
return fullPath ;
2018-02-04 18:08:20 -05:00
}
2022-03-22 15:46:16 -04:00
internal string GetSdCardPath ( ) = > MakeFullPath ( AppDataManager . DefaultSdcardDir ) ;
public string GetNandPath ( ) = > MakeFullPath ( AppDataManager . DefaultNandDir ) ;
2018-11-18 14:37:41 -05:00
2018-12-06 06:16:24 -05:00
public string SwitchPathToSystemPath ( string switchPath )
2018-07-17 15:14:27 -04:00
{
2018-12-06 06:16:24 -05:00
string [ ] parts = switchPath . Split ( ":" ) ;
2018-11-18 14:37:41 -05:00
2018-12-06 06:16:24 -05:00
if ( parts . Length ! = 2 )
2018-07-17 15:14:27 -04:00
{
return null ;
}
2018-11-18 19:20:17 -05:00
2019-09-08 17:33:40 -04:00
return GetFullPath ( MakeFullPath ( parts [ 0 ] ) , parts [ 1 ] ) ;
2018-07-17 15:14:27 -04:00
}
2018-12-06 06:16:24 -05:00
public string SystemPathToSwitchPath ( string systemPath )
2018-07-17 15:14:27 -04:00
{
2022-03-22 15:46:16 -04:00
string baseSystemPath = AppDataManager . BaseDirPath + Path . DirectorySeparatorChar ;
2018-11-18 14:37:41 -05:00
2018-12-06 06:16:24 -05:00
if ( systemPath . StartsWith ( baseSystemPath ) )
2018-07-17 15:14:27 -04:00
{
2021-08-12 17:56:24 -04:00
string rawPath = systemPath . Replace ( baseSystemPath , "" ) ;
int firstSeparatorOffset = rawPath . IndexOf ( Path . DirectorySeparatorChar ) ;
2018-11-18 14:37:41 -05:00
2018-12-06 06:16:24 -05:00
if ( firstSeparatorOffset = = - 1 )
2018-07-17 15:14:27 -04:00
{
2018-12-06 06:16:24 -05:00
return $"{rawPath}:/" ;
2018-07-17 15:14:27 -04:00
}
2023-01-18 17:25:16 -05:00
var basePath = rawPath . AsSpan ( 0 , firstSeparatorOffset ) ;
var fileName = rawPath . AsSpan ( firstSeparatorOffset + 1 ) ;
2018-11-18 14:37:41 -05:00
2018-12-06 06:16:24 -05:00
return $"{basePath}:/{fileName}" ;
2018-07-17 15:14:27 -04:00
}
return null ;
}
2019-09-08 17:33:40 -04:00
private string MakeFullPath ( string path , bool isDirectory = true )
2018-02-04 18:08:20 -05:00
{
2018-11-18 14:37:41 -05:00
// Handles Common Switch Content Paths
2019-09-08 17:33:40 -04:00
switch ( path )
2018-11-18 14:37:41 -05:00
{
case ContentPath . SdCard :
2022-03-22 15:46:16 -04:00
path = AppDataManager . DefaultSdcardDir ;
2018-11-18 14:37:41 -05:00
break ;
case ContentPath . User :
2019-09-08 17:33:40 -04:00
path = UserNandPath ;
2018-11-18 14:37:41 -05:00
break ;
case ContentPath . System :
2019-09-08 17:33:40 -04:00
path = SystemNandPath ;
2018-11-18 14:37:41 -05:00
break ;
case ContentPath . SdCardContent :
2022-03-22 15:46:16 -04:00
path = Path . Combine ( AppDataManager . DefaultSdcardDir , "Nintendo" , "Contents" ) ;
2018-11-18 14:37:41 -05:00
break ;
case ContentPath . UserContent :
2019-09-08 17:33:40 -04:00
path = Path . Combine ( UserNandPath , "Contents" ) ;
2018-11-18 14:37:41 -05:00
break ;
case ContentPath . SystemContent :
2019-09-08 17:33:40 -04:00
path = Path . Combine ( SystemNandPath , "Contents" ) ;
2018-11-18 14:37:41 -05:00
break ;
}
2022-03-22 15:46:16 -04:00
string fullPath = Path . Combine ( AppDataManager . BaseDirPath , path ) ;
2018-02-04 18:08:20 -05:00
2022-03-22 15:46:16 -04:00
if ( isDirectory & & ! Directory . Exists ( fullPath ) )
2018-02-04 18:08:20 -05:00
{
2022-03-22 15:46:16 -04:00
Directory . CreateDirectory ( fullPath ) ;
2018-02-04 18:08:20 -05:00
}
2018-12-06 06:16:24 -05:00
return fullPath ;
2018-02-04 18:08:20 -05:00
}
2021-08-12 17:56:24 -04:00
public void InitializeFsServer ( LibHac . Horizon horizon , out HorizonClient fsServerClient )
2020-01-21 17:23:11 -05:00
{
2022-03-22 15:46:16 -04:00
LocalFileSystem serverBaseFs = new LocalFileSystem ( AppDataManager . BaseDirPath ) ;
2021-07-13 04:19:28 -04:00
2021-08-12 17:56:24 -04:00
fsServerClient = horizon . CreatePrivilegedHorizonClient ( ) ;
var fsServer = new FileSystemServer ( fsServerClient ) ;
2022-11-23 12:32:35 -05:00
RandomDataGenerator randomGenerator = buffer = > Random . Shared . NextBytes ( buffer ) ;
2020-01-21 17:23:11 -05:00
2022-11-23 12:32:35 -05:00
DefaultFsServerObjects fsServerObjects = DefaultFsServerObjects . GetDefaultEmulatedCreators ( serverBaseFs , KeySet , fsServer , randomGenerator ) ;
// Use our own encrypted fs creator that doesn't actually do any encryption
2021-08-17 13:46:52 -04:00
fsServerObjects . FsCreators . EncryptedFileSystemCreator = new EncryptedFileSystemCreator ( ) ;
2020-01-21 17:23:11 -05:00
GameCard = fsServerObjects . GameCard ;
2021-08-12 17:56:24 -04:00
SdCard = fsServerObjects . SdCard ;
2020-01-21 17:23:11 -05:00
2020-03-09 18:34:35 -04:00
SdCard . SetSdCardInsertionStatus ( true ) ;
2021-08-12 17:56:24 -04:00
var fsServerConfig = new FileSystemServerConfig
2020-01-21 17:23:11 -05:00
{
DeviceOperator = fsServerObjects . DeviceOperator ,
2021-08-12 17:56:24 -04:00
ExternalKeySet = KeySet . ExternalKeySet ,
2022-11-23 12:32:35 -05:00
FsCreators = fsServerObjects . FsCreators ,
RandomGenerator = randomGenerator
2020-01-21 17:23:11 -05:00
} ;
2021-08-12 17:56:24 -04:00
FileSystemServerInitializer . InitializeWithConfig ( fsServerClient , fsServer , fsServerConfig ) ;
2020-01-21 17:23:11 -05:00
}
2021-08-12 17:56:24 -04:00
public void ReloadKeySet ( )
2021-07-13 10:48:54 -04:00
{
2021-08-12 17:56:24 -04:00
KeySet ? ? = KeySet . CreateDefaultKeySet ( ) ;
string keyFile = null ;
string titleKeyFile = null ;
2020-01-21 17:23:11 -05:00
string consoleKeyFile = null ;
2021-03-15 17:10:36 -04:00
if ( AppDataManager . Mode = = AppDataManager . LaunchMode . UserProfile )
2020-08-30 12:51:53 -04:00
{
2021-03-15 17:10:36 -04:00
LoadSetAtPath ( AppDataManager . KeysDirPathUser ) ;
2020-08-30 12:51:53 -04:00
}
2020-01-21 17:23:11 -05:00
2020-08-30 12:51:53 -04:00
LoadSetAtPath ( AppDataManager . KeysDirPath ) ;
2020-01-21 17:23:11 -05:00
void LoadSetAtPath ( string basePath )
{
2021-08-12 17:56:24 -04:00
string localKeyFile = Path . Combine ( basePath , "prod.keys" ) ;
string localTitleKeyFile = Path . Combine ( basePath , "title.keys" ) ;
2020-01-21 17:23:11 -05:00
string localConsoleKeyFile = Path . Combine ( basePath , "console.keys" ) ;
if ( File . Exists ( localKeyFile ) )
{
keyFile = localKeyFile ;
}
if ( File . Exists ( localTitleKeyFile ) )
{
titleKeyFile = localTitleKeyFile ;
}
if ( File . Exists ( localConsoleKeyFile ) )
{
consoleKeyFile = localConsoleKeyFile ;
}
}
2021-08-12 17:56:24 -04:00
ExternalKeyReader . ReadKeyFile ( KeySet , keyFile , titleKeyFile , consoleKeyFile , null ) ;
2020-01-21 17:23:11 -05:00
}
2020-05-15 02:16:46 -04:00
public void ImportTickets ( IFileSystem fs )
{
foreach ( DirectoryEntryEx ticketEntry in fs . EnumerateEntries ( "/" , "*.tik" ) )
{
2021-12-23 11:55:50 -05:00
using var ticketFile = new UniqueRef < IFile > ( ) ;
2023-03-01 21:42:27 -05:00
Result result = fs . OpenFile ( ref ticketFile . Ref , ticketEntry . FullPath . ToU8Span ( ) , OpenMode . Read ) ;
2020-05-15 02:16:46 -04:00
if ( result . IsSuccess ( ) )
{
2021-12-23 11:55:50 -05:00
Ticket ticket = new Ticket ( ticketFile . Get . AsStream ( ) ) ;
2023-03-01 21:42:27 -05:00
var titleKey = ticket . GetTitleKey ( KeySet ) ;
2020-05-15 02:16:46 -04:00
2023-03-01 21:42:27 -05:00
if ( titleKey ! = null )
2021-01-10 22:47:13 -05:00
{
2023-03-01 21:42:27 -05:00
KeySet . ExternalKeySet . Add ( new RightsId ( ticket . RightsId ) , new AccessKey ( titleKey ) ) ;
2021-01-10 22:47:13 -05:00
}
2020-05-15 02:16:46 -04:00
}
}
}
2021-08-12 17:56:24 -04:00
// Save data created before we supported extra data in directory save data will not work properly if
// given empty extra data. Luckily some of that extra data can be created using the data from the
// save data indexer, which should be enough to check access permissions for user saves.
// Every single save data's extra data will be checked and fixed if needed each time the emulator is opened.
// Consider removing this at some point in the future when we don't need to worry about old saves.
public static Result FixExtraData ( HorizonClient hos )
{
Result rc = GetSystemSaveList ( hos , out List < ulong > systemSaveIds ) ;
if ( rc . IsFailure ( ) ) return rc ;
rc = FixUnindexedSystemSaves ( hos , systemSaveIds ) ;
if ( rc . IsFailure ( ) ) return rc ;
rc = FixExtraDataInSpaceId ( hos , SaveDataSpaceId . System ) ;
if ( rc . IsFailure ( ) ) return rc ;
rc = FixExtraDataInSpaceId ( hos , SaveDataSpaceId . User ) ;
if ( rc . IsFailure ( ) ) return rc ;
return Result . Success ;
}
private static Result FixExtraDataInSpaceId ( HorizonClient hos , SaveDataSpaceId spaceId )
{
Span < SaveDataInfo > info = stackalloc SaveDataInfo [ 8 ] ;
2021-12-23 11:55:50 -05:00
using var iterator = new UniqueRef < SaveDataIterator > ( ) ;
2023-03-01 21:42:27 -05:00
Result rc = hos . Fs . OpenSaveDataIterator ( ref iterator . Ref , spaceId ) ;
2021-08-12 17:56:24 -04:00
if ( rc . IsFailure ( ) ) return rc ;
while ( true )
{
2021-12-23 11:55:50 -05:00
rc = iterator . Get . ReadSaveDataInfo ( out long count , info ) ;
2021-08-12 17:56:24 -04:00
if ( rc . IsFailure ( ) ) return rc ;
if ( count = = 0 )
return Result . Success ;
for ( int i = 0 ; i < count ; i + + )
{
rc = FixExtraData ( out bool wasFixNeeded , hos , in info [ i ] ) ;
if ( ResultFs . TargetNotFound . Includes ( rc ) )
{
// If the save wasn't found, try to create the directory for its save data ID
rc = CreateSaveDataDirectory ( hos , in info [ i ] ) ;
if ( rc . IsFailure ( ) )
{
Logger . Warning ? . Print ( LogClass . Application , $"Error {rc.ToStringWithName()} when creating save data 0x{info[i].SaveDataId:x} in the {spaceId} save data space" ) ;
// Don't bother fixing the extra data if we couldn't create the directory
continue ;
}
Logger . Info ? . Print ( LogClass . Application , $"Recreated directory for save data 0x{info[i].SaveDataId:x} in the {spaceId} save data space" ) ;
// Try to fix the extra data in the new directory
rc = FixExtraData ( out wasFixNeeded , hos , in info [ i ] ) ;
}
if ( rc . IsFailure ( ) )
{
Logger . Warning ? . Print ( LogClass . Application , $"Error {rc.ToStringWithName()} when fixing extra data for save data 0x{info[i].SaveDataId:x} in the {spaceId} save data space" ) ;
}
else if ( wasFixNeeded )
{
Logger . Info ? . Print ( LogClass . Application , $"Fixed extra data for save data 0x{info[i].SaveDataId:x} in the {spaceId} save data space" ) ;
}
}
}
}
private static Result CreateSaveDataDirectory ( HorizonClient hos , in SaveDataInfo info )
{
if ( info . SpaceId ! = SaveDataSpaceId . User & & info . SpaceId ! = SaveDataSpaceId . System )
return Result . Success ;
const string mountName = "SaveDir" ;
var mountNameU8 = mountName . ToU8Span ( ) ;
BisPartitionId partitionId = info . SpaceId switch
{
SaveDataSpaceId . System = > BisPartitionId . System ,
SaveDataSpaceId . User = > BisPartitionId . User ,
_ = > throw new ArgumentOutOfRangeException ( )
} ;
Result rc = hos . Fs . MountBis ( mountNameU8 , partitionId ) ;
if ( rc . IsFailure ( ) ) return rc ;
try
{
var path = $"{mountName}:/save/{info.SaveDataId:x16}" . ToU8Span ( ) ;
rc = hos . Fs . GetEntryType ( out _ , path ) ;
if ( ResultFs . PathNotFound . Includes ( rc ) )
{
rc = hos . Fs . CreateDirectory ( path ) ;
}
return rc ;
}
finally
{
hos . Fs . Unmount ( mountNameU8 ) ;
}
}
// Gets a list of all the save data files or directories in the system partition.
private static Result GetSystemSaveList ( HorizonClient hos , out List < ulong > list )
{
list = null ;
var mountName = "system" . ToU8Span ( ) ;
DirectoryHandle handle = default ;
List < ulong > localList = new List < ulong > ( ) ;
try
{
Result rc = hos . Fs . MountBis ( mountName , BisPartitionId . System ) ;
if ( rc . IsFailure ( ) ) return rc ;
rc = hos . Fs . OpenDirectory ( out handle , "system:/save" . ToU8Span ( ) , OpenDirectoryMode . All ) ;
if ( rc . IsFailure ( ) ) return rc ;
DirectoryEntry entry = new DirectoryEntry ( ) ;
while ( true )
{
rc = hos . Fs . ReadDirectory ( out long readCount , SpanHelpers . AsSpan ( ref entry ) , handle ) ;
if ( rc . IsFailure ( ) ) return rc ;
if ( readCount = = 0 )
break ;
if ( Utf8Parser . TryParse ( entry . Name , out ulong saveDataId , out int bytesRead , 'x' ) & &
bytesRead = = 16 & & ( long ) saveDataId < 0 )
{
localList . Add ( saveDataId ) ;
}
}
list = localList ;
return Result . Success ;
}
finally
{
if ( handle . IsValid )
{
hos . Fs . CloseDirectory ( handle ) ;
}
if ( hos . Fs . IsMounted ( mountName ) )
{
hos . Fs . Unmount ( mountName ) ;
}
}
}
// Adds system save data that isn't in the save data indexer to the indexer and creates extra data for it.
// Only save data IDs added to SystemExtraDataFixInfo will be fixed.
private static Result FixUnindexedSystemSaves ( HorizonClient hos , List < ulong > existingSaveIds )
{
foreach ( var fixInfo in SystemExtraDataFixInfo )
{
if ( ! existingSaveIds . Contains ( fixInfo . StaticSaveDataId ) )
{
continue ;
}
Result rc = FixSystemExtraData ( out bool wasFixNeeded , hos , in fixInfo ) ;
if ( rc . IsFailure ( ) )
{
Logger . Warning ? . Print ( LogClass . Application ,
$"Error {rc.ToStringWithName()} when fixing extra data for system save data 0x{fixInfo.StaticSaveDataId:x}" ) ;
}
else if ( wasFixNeeded )
{
Logger . Info ? . Print ( LogClass . Application ,
$"Tried to rebuild extra data for system save data 0x{fixInfo.StaticSaveDataId:x}" ) ;
}
}
return Result . Success ;
}
private static Result FixSystemExtraData ( out bool wasFixNeeded , HorizonClient hos , in ExtraDataFixInfo info )
{
wasFixNeeded = true ;
Result rc = hos . Fs . Impl . ReadSaveDataFileSystemExtraData ( out SaveDataExtraData extraData , info . StaticSaveDataId ) ;
if ( ! rc . IsSuccess ( ) )
{
if ( ! ResultFs . TargetNotFound . Includes ( rc ) )
return rc ;
// We'll reach this point only if the save data directory exists but it's not in the save data indexer.
// Creating the save will add it to the indexer while leaving its existing contents intact.
return hos . Fs . CreateSystemSaveData ( info . StaticSaveDataId , UserId . InvalidId , info . OwnerId , info . DataSize ,
info . JournalSize , info . Flags ) ;
}
if ( extraData . Attribute . StaticSaveDataId ! = 0 & & extraData . OwnerId ! = 0 )
{
wasFixNeeded = false ;
return Result . Success ;
}
extraData = new SaveDataExtraData
{
Attribute = { StaticSaveDataId = info . StaticSaveDataId } ,
OwnerId = info . OwnerId ,
Flags = info . Flags ,
DataSize = info . DataSize ,
JournalSize = info . JournalSize
} ;
// Make a mask for writing the entire extra data
Unsafe . SkipInit ( out SaveDataExtraData extraDataMask ) ;
SpanHelpers . AsByteSpan ( ref extraDataMask ) . Fill ( 0xFF ) ;
return hos . Fs . Impl . WriteSaveDataFileSystemExtraData ( SaveDataSpaceId . System , info . StaticSaveDataId ,
in extraData , in extraDataMask ) ;
}
private static Result FixExtraData ( out bool wasFixNeeded , HorizonClient hos , in SaveDataInfo info )
{
wasFixNeeded = true ;
Result rc = hos . Fs . Impl . ReadSaveDataFileSystemExtraData ( out SaveDataExtraData extraData , info . SpaceId ,
info . SaveDataId ) ;
if ( rc . IsFailure ( ) ) return rc ;
// The extra data should have program ID or static save data ID set if it's valid.
// We only try to fix the extra data if the info from the save data indexer has a program ID or static save data ID.
bool canFixByProgramId = extraData . Attribute . ProgramId = = ProgramId . InvalidId & &
info . ProgramId ! = ProgramId . InvalidId ;
bool canFixBySaveDataId = extraData . Attribute . StaticSaveDataId = = 0 & & info . StaticSaveDataId ! = 0 ;
2022-03-22 15:46:16 -04:00
bool hasEmptyOwnerId = extraData . OwnerId = = 0 & & info . Type ! = SaveDataType . System ;
2022-02-02 16:49:49 -05:00
if ( ! canFixByProgramId & & ! canFixBySaveDataId & & ! hasEmptyOwnerId )
2021-08-12 17:56:24 -04:00
{
wasFixNeeded = false ;
return Result . Success ;
}
// The save data attribute struct can be completely created from the save data info.
extraData . Attribute . ProgramId = info . ProgramId ;
extraData . Attribute . UserId = info . UserId ;
extraData . Attribute . StaticSaveDataId = info . StaticSaveDataId ;
extraData . Attribute . Type = info . Type ;
extraData . Attribute . Rank = info . Rank ;
extraData . Attribute . Index = info . Index ;
// The rest of the extra data can't be created from the save data info.
// On user saves the owner ID will almost certainly be the same as the program ID.
2022-03-22 15:46:16 -04:00
if ( info . Type ! = SaveDataType . System )
2021-08-12 17:56:24 -04:00
{
extraData . OwnerId = info . ProgramId . Value ;
}
else
{
// Try to match the system save with one of the known saves
foreach ( ExtraDataFixInfo fixInfo in SystemExtraDataFixInfo )
{
if ( extraData . Attribute . StaticSaveDataId = = fixInfo . StaticSaveDataId )
{
extraData . OwnerId = fixInfo . OwnerId ;
extraData . Flags = fixInfo . Flags ;
extraData . DataSize = fixInfo . DataSize ;
extraData . JournalSize = fixInfo . JournalSize ;
break ;
}
}
}
// Make a mask for writing the entire extra data
Unsafe . SkipInit ( out SaveDataExtraData extraDataMask ) ;
SpanHelpers . AsByteSpan ( ref extraDataMask ) . Fill ( 0xFF ) ;
return hos . Fs . Impl . WriteSaveDataFileSystemExtraData ( info . SpaceId , info . SaveDataId , in extraData , in extraDataMask ) ;
}
struct ExtraDataFixInfo
{
public ulong StaticSaveDataId ;
public ulong OwnerId ;
public SaveDataFlags Flags ;
public long DataSize ;
public long JournalSize ;
}
private static readonly ExtraDataFixInfo [ ] SystemExtraDataFixInfo =
{
new ExtraDataFixInfo ( )
{
StaticSaveDataId = 0x8000000000000030 ,
OwnerId = 0x010000000000001F ,
Flags = SaveDataFlags . KeepAfterResettingSystemSaveDataWithoutUserSaveData ,
DataSize = 0x10000 ,
JournalSize = 0x10000
} ,
new ExtraDataFixInfo ( )
{
StaticSaveDataId = 0x8000000000001040 ,
OwnerId = 0x0100000000001009 ,
Flags = SaveDataFlags . None ,
DataSize = 0xC000 ,
JournalSize = 0xC000
}
} ;
2018-02-04 18:08:20 -05:00
public void Dispose ( )
{
Dispose ( true ) ;
}
protected virtual void Dispose ( bool disposing )
{
2018-02-20 05:54:00 -05:00
if ( disposing )
2018-02-04 18:08:20 -05:00
{
2023-01-18 08:50:42 -05:00
foreach ( var stream in _romFsByPid . Values )
{
stream . Close ( ) ;
}
_romFsByPid . Clear ( ) ;
2018-02-04 18:08:20 -05:00
}
}
}
}