2020-01-21 23:23:11 +01:00
using LibHac ;
2020-05-15 08:16:46 +02:00
using LibHac.Common ;
2021-08-12 23:56:24 +02:00
using LibHac.Common.Keys ;
2020-01-21 23:23:11 +01:00
using LibHac.Fs ;
2020-09-01 22:08:59 +02:00
using LibHac.Fs.Fsa ;
2021-08-12 23:56:24 +02:00
using LibHac.Fs.Shim ;
2020-09-01 22:08:59 +02:00
using LibHac.FsSrv ;
2020-01-21 23:23:11 +01:00
using LibHac.FsSystem ;
2021-08-12 23:56:24 +02:00
using LibHac.Ncm ;
2023-10-23 01:30:46 +02:00
using LibHac.Sdmmc ;
2020-05-15 08:16:46 +02:00
using LibHac.Spl ;
2022-01-12 12:22:19 +01:00
using LibHac.Tools.Es ;
using LibHac.Tools.Fs ;
using LibHac.Tools.FsSystem ;
2020-08-30 18:51:53 +02:00
using Ryujinx.Common.Configuration ;
2021-08-12 23:56:24 +02:00
using Ryujinx.Common.Logging ;
2018-09-09 00:04:26 +02:00
using Ryujinx.HLE.HOS ;
2018-02-05 00:08:20 +01:00
using System ;
2021-08-12 23:56:24 +02:00
using System.Buffers.Text ;
2023-01-18 14:50:42 +01:00
using System.Collections.Concurrent ;
2021-08-12 23:56:24 +02:00
using System.Collections.Generic ;
2018-02-05 00:08:20 +01:00
using System.IO ;
2021-08-12 23:56:24 +02:00
using System.Runtime.CompilerServices ;
2021-12-23 17:55:50 +01:00
using Path = System . IO . Path ;
2018-02-05 00:08:20 +01:00
2018-09-09 00:04:26 +02:00
namespace Ryujinx.HLE.FileSystem
2018-02-05 00:08:20 +01:00
{
2019-09-02 18:03:57 +02:00
public class VirtualFileSystem : IDisposable
2018-02-05 00:08:20 +01:00
{
2023-07-16 19:31:14 +02:00
public static readonly string SafeNandPath = Path . Combine ( AppDataManager . DefaultNandDir , "safe" ) ;
public static readonly string SystemNandPath = Path . Combine ( AppDataManager . DefaultNandDir , "system" ) ;
public static readonly string UserNandPath = Path . Combine ( AppDataManager . DefaultNandDir , "user" ) ;
2018-09-09 00:04:26 +02:00
2023-07-16 19:31:14 +02:00
public KeySet KeySet { get ; private set ; }
public EmulatedGameCard GameCard { get ; private set ; }
2023-10-23 01:30:46 +02:00
public SdmmcApi SdCard { get ; private set ; }
2023-07-16 19:31:14 +02:00
public ModLoader ModLoader { get ; private set ; }
2023-01-18 14:50:42 +01:00
private readonly ConcurrentDictionary < ulong , Stream > _romFsByPid ;
2021-08-12 23:56:24 +02:00
2020-01-24 17:01:21 +01:00
private static bool _isInitialized = false ;
2022-03-22 20:46:16 +01:00
public static VirtualFileSystem CreateInstance ( )
{
if ( _isInitialized )
{
throw new InvalidOperationException ( "VirtualFileSystem can only be instantiated once!" ) ;
}
2020-01-21 23:23:11 +01:00
2022-03-22 20:46:16 +01:00
_isInitialized = true ;
return new VirtualFileSystem ( ) ;
}
2020-07-09 06:31:15 +02:00
2020-01-24 17:01:21 +01:00
private VirtualFileSystem ( )
2020-01-21 23:23:11 +01:00
{
2021-08-12 23:56:24 +02:00
ReloadKeySet ( ) ;
2020-07-09 06:31:15 +02:00
ModLoader = new ModLoader ( ) ; // Should only be created once
2023-01-18 14:50:42 +01: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 23:23:11 +01:00
}
2023-01-18 14:50:42 +01:00
public void SetRomFs ( ulong pid , Stream romfsStream )
2018-02-05 00:08:20 +01:00
{
2023-01-18 14:50:42 +01:00
_romFsByPid . AddOrUpdate ( pid , romfsStream , ( pid , oldStream ) = >
{
oldStream . Close ( ) ;
return romfsStream ;
} ) ;
2018-02-05 00:08:20 +01:00
}
2023-01-18 14:50:42 +01:00
public Stream GetRomFs ( ulong pid )
2018-09-08 20:33:27 +02:00
{
2023-01-18 14:50:42 +01:00
return _romFsByPid [ pid ] ;
2018-09-08 20:33:27 +02:00
}
2023-07-16 19:31:14 +02:00
public static string GetFullPath ( string basePath , string fileName )
2018-02-05 00:08:20 +01:00
{
2018-12-06 12:16:24 +01:00
if ( fileName . StartsWith ( "//" ) )
2018-03-06 21:27:50 +01:00
{
2023-07-16 19:31:14 +02:00
fileName = fileName [ 2. . ] ;
2018-03-06 21:27:50 +01:00
}
2018-12-06 12:16:24 +01:00
else if ( fileName . StartsWith ( '/' ) )
2018-02-05 00:08:20 +01:00
{
2023-07-16 19:31:14 +02:00
fileName = fileName [ 1. . ] ;
2018-02-05 00:08:20 +01:00
}
2018-03-06 21:27:50 +01:00
else
{
return null ;
}
2018-02-05 00:08:20 +01:00
2018-12-06 12:16:24 +01:00
string fullPath = Path . GetFullPath ( Path . Combine ( basePath , fileName ) ) ;
2018-02-05 00:08:20 +01:00
2022-03-22 20:46:16 +01:00
if ( ! fullPath . StartsWith ( AppDataManager . BaseDirPath ) )
2018-02-05 00:08:20 +01:00
{
return null ;
}
2018-12-06 12:16:24 +01:00
return fullPath ;
2018-02-05 00:08:20 +01:00
}
2023-07-16 19:31:14 +02:00
internal static string GetSdCardPath ( ) = > MakeFullPath ( AppDataManager . DefaultSdcardDir ) ;
public static string GetNandPath ( ) = > MakeFullPath ( AppDataManager . DefaultNandDir ) ;
2018-11-18 20:37:41 +01:00
2023-07-16 19:31:14 +02:00
public static string SwitchPathToSystemPath ( string switchPath )
2018-07-17 21:14:27 +02:00
{
2018-12-06 12:16:24 +01:00
string [ ] parts = switchPath . Split ( ":" ) ;
2018-11-18 20:37:41 +01:00
2018-12-06 12:16:24 +01:00
if ( parts . Length ! = 2 )
2018-07-17 21:14:27 +02:00
{
return null ;
}
2018-11-19 01:20:17 +01:00
2019-09-08 23:33:40 +02:00
return GetFullPath ( MakeFullPath ( parts [ 0 ] ) , parts [ 1 ] ) ;
2018-07-17 21:14:27 +02:00
}
2023-07-16 19:31:14 +02:00
public static string SystemPathToSwitchPath ( string systemPath )
2018-07-17 21:14:27 +02:00
{
2022-03-22 20:46:16 +01:00
string baseSystemPath = AppDataManager . BaseDirPath + Path . DirectorySeparatorChar ;
2018-11-18 20:37:41 +01:00
2018-12-06 12:16:24 +01:00
if ( systemPath . StartsWith ( baseSystemPath ) )
2018-07-17 21:14:27 +02:00
{
2021-08-12 23:56:24 +02:00
string rawPath = systemPath . Replace ( baseSystemPath , "" ) ;
int firstSeparatorOffset = rawPath . IndexOf ( Path . DirectorySeparatorChar ) ;
2018-11-18 20:37:41 +01:00
2018-12-06 12:16:24 +01:00
if ( firstSeparatorOffset = = - 1 )
2018-07-17 21:14:27 +02:00
{
2018-12-06 12:16:24 +01:00
return $"{rawPath}:/" ;
2018-07-17 21:14:27 +02:00
}
2023-01-18 23:25:16 +01:00
var basePath = rawPath . AsSpan ( 0 , firstSeparatorOffset ) ;
var fileName = rawPath . AsSpan ( firstSeparatorOffset + 1 ) ;
2018-11-18 20:37:41 +01:00
2018-12-06 12:16:24 +01:00
return $"{basePath}:/{fileName}" ;
2018-07-17 21:14:27 +02:00
}
2023-03-31 21:16:46 +02:00
2018-07-17 21:14:27 +02:00
return null ;
}
2023-07-16 19:31:14 +02:00
private static string MakeFullPath ( string path , bool isDirectory = true )
2018-02-05 00:08:20 +01:00
{
2018-11-18 20:37:41 +01:00
// Handles Common Switch Content Paths
2019-09-08 23:33:40 +02:00
switch ( path )
2018-11-18 20:37:41 +01:00
{
case ContentPath . SdCard :
2022-03-22 20:46:16 +01:00
path = AppDataManager . DefaultSdcardDir ;
2018-11-18 20:37:41 +01:00
break ;
case ContentPath . User :
2019-09-08 23:33:40 +02:00
path = UserNandPath ;
2018-11-18 20:37:41 +01:00
break ;
case ContentPath . System :
2019-09-08 23:33:40 +02:00
path = SystemNandPath ;
2018-11-18 20:37:41 +01:00
break ;
case ContentPath . SdCardContent :
2022-03-22 20:46:16 +01:00
path = Path . Combine ( AppDataManager . DefaultSdcardDir , "Nintendo" , "Contents" ) ;
2018-11-18 20:37:41 +01:00
break ;
case ContentPath . UserContent :
2019-09-08 23:33:40 +02:00
path = Path . Combine ( UserNandPath , "Contents" ) ;
2018-11-18 20:37:41 +01:00
break ;
case ContentPath . SystemContent :
2019-09-08 23:33:40 +02:00
path = Path . Combine ( SystemNandPath , "Contents" ) ;
2018-11-18 20:37:41 +01:00
break ;
}
2022-03-22 20:46:16 +01:00
string fullPath = Path . Combine ( AppDataManager . BaseDirPath , path ) ;
2018-02-05 00:08:20 +01:00
2022-03-22 20:46:16 +01:00
if ( isDirectory & & ! Directory . Exists ( fullPath ) )
2018-02-05 00:08:20 +01:00
{
2022-03-22 20:46:16 +01:00
Directory . CreateDirectory ( fullPath ) ;
2018-02-05 00:08:20 +01:00
}
2018-12-06 12:16:24 +01:00
return fullPath ;
2018-02-05 00:08:20 +01:00
}
2021-08-12 23:56:24 +02:00
public void InitializeFsServer ( LibHac . Horizon horizon , out HorizonClient fsServerClient )
2020-01-21 23:23:11 +01:00
{
2023-07-16 19:31:14 +02:00
LocalFileSystem serverBaseFs = new ( AppDataManager . BaseDirPath ) ;
2021-07-13 10:19:28 +02:00
2021-08-12 23:56:24 +02:00
fsServerClient = horizon . CreatePrivilegedHorizonClient ( ) ;
var fsServer = new FileSystemServer ( fsServerClient ) ;
2023-03-31 21:16:46 +02:00
RandomDataGenerator randomGenerator = Random . Shared . NextBytes ;
2020-01-21 23:23:11 +01:00
2022-11-23 18:32:35 +01: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 19:46:52 +02:00
fsServerObjects . FsCreators . EncryptedFileSystemCreator = new EncryptedFileSystemCreator ( ) ;
2020-01-21 23:23:11 +01:00
GameCard = fsServerObjects . GameCard ;
2023-10-23 01:30:46 +02:00
SdCard = fsServerObjects . Sdmmc ;
2020-01-21 23:23:11 +01:00
2023-10-23 01:30:46 +02:00
SdCard . SetSdCardInserted ( true ) ;
2020-03-09 23:34:35 +01:00
2021-08-12 23:56:24 +02:00
var fsServerConfig = new FileSystemServerConfig
2020-01-21 23:23:11 +01:00
{
2021-08-12 23:56:24 +02:00
ExternalKeySet = KeySet . ExternalKeySet ,
2022-11-23 18:32:35 +01:00
FsCreators = fsServerObjects . FsCreators ,
2023-10-23 01:30:46 +02:00
StorageDeviceManagerFactory = fsServerObjects . StorageDeviceManagerFactory ,
2023-07-16 19:31:14 +02:00
RandomGenerator = randomGenerator ,
2020-01-21 23:23:11 +01:00
} ;
2021-08-12 23:56:24 +02:00
FileSystemServerInitializer . InitializeWithConfig ( fsServerClient , fsServer , fsServerConfig ) ;
2020-01-21 23:23:11 +01:00
}
2021-08-12 23:56:24 +02:00
public void ReloadKeySet ( )
2021-07-13 16:48:54 +02:00
{
2021-08-12 23:56:24 +02:00
KeySet ? ? = KeySet . CreateDefaultKeySet ( ) ;
string keyFile = null ;
string titleKeyFile = null ;
2020-01-21 23:23:11 +01:00
string consoleKeyFile = null ;
2021-03-15 22:10:36 +01:00
if ( AppDataManager . Mode = = AppDataManager . LaunchMode . UserProfile )
2020-08-30 18:51:53 +02:00
{
2021-03-15 22:10:36 +01:00
LoadSetAtPath ( AppDataManager . KeysDirPathUser ) ;
2020-08-30 18:51:53 +02:00
}
2020-01-21 23:23:11 +01:00
2020-08-30 18:51:53 +02:00
LoadSetAtPath ( AppDataManager . KeysDirPath ) ;
2020-01-21 23:23:11 +01:00
void LoadSetAtPath ( string basePath )
{
2021-08-12 23:56:24 +02:00
string localKeyFile = Path . Combine ( basePath , "prod.keys" ) ;
string localTitleKeyFile = Path . Combine ( basePath , "title.keys" ) ;
2020-01-21 23:23:11 +01: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 23:56:24 +02:00
ExternalKeyReader . ReadKeyFile ( KeySet , keyFile , titleKeyFile , consoleKeyFile , null ) ;
2020-01-21 23:23:11 +01:00
}
2020-05-15 08:16:46 +02:00
public void ImportTickets ( IFileSystem fs )
{
foreach ( DirectoryEntryEx ticketEntry in fs . EnumerateEntries ( "/" , "*.tik" ) )
{
2021-12-23 17:55:50 +01:00
using var ticketFile = new UniqueRef < IFile > ( ) ;
2023-03-02 03:42:27 +01:00
Result result = fs . OpenFile ( ref ticketFile . Ref , ticketEntry . FullPath . ToU8Span ( ) , OpenMode . Read ) ;
2020-05-15 08:16:46 +02:00
if ( result . IsSuccess ( ) )
{
2023-10-24 18:26:25 +02:00
// When reading a file from a Sha256PartitionFileSystem, you can't start a read in the middle
// of the hashed portion (usually the first 0x200 bytes) of the file and end the read after
// the end of the hashed portion, so we read the ticket file using a single read.
byte [ ] ticketData = new byte [ 0x2C0 ] ;
result = ticketFile . Get . Read ( out long bytesRead , 0 , ticketData ) ;
if ( result . IsFailure ( ) | | bytesRead ! = ticketData . Length )
continue ;
Ticket ticket = new ( new MemoryStream ( ticketData ) ) ;
2023-03-02 03:42:27 +01:00
var titleKey = ticket . GetTitleKey ( KeySet ) ;
2020-05-15 08:16:46 +02:00
2023-03-02 03:42:27 +01:00
if ( titleKey ! = null )
2021-01-11 04:47:13 +01:00
{
2023-03-02 03:42:27 +01:00
KeySet . ExternalKeySet . Add ( new RightsId ( ticket . RightsId ) , new AccessKey ( titleKey ) ) ;
2021-01-11 04:47:13 +01:00
}
2020-05-15 08:16:46 +02:00
}
}
}
2021-08-12 23:56:24 +02: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 ) ;
2023-07-16 19:31:14 +02:00
if ( rc . IsFailure ( ) )
{
return rc ;
}
2021-08-12 23:56:24 +02:00
rc = FixUnindexedSystemSaves ( hos , systemSaveIds ) ;
2023-07-16 19:31:14 +02:00
if ( rc . IsFailure ( ) )
{
return rc ;
}
2021-08-12 23:56:24 +02:00
rc = FixExtraDataInSpaceId ( hos , SaveDataSpaceId . System ) ;
2023-07-16 19:31:14 +02:00
if ( rc . IsFailure ( ) )
{
return rc ;
}
2021-08-12 23:56:24 +02:00
rc = FixExtraDataInSpaceId ( hos , SaveDataSpaceId . User ) ;
2023-07-16 19:31:14 +02:00
if ( rc . IsFailure ( ) )
{
return rc ;
}
2021-08-12 23:56:24 +02:00
return Result . Success ;
}
private static Result FixExtraDataInSpaceId ( HorizonClient hos , SaveDataSpaceId spaceId )
{
Span < SaveDataInfo > info = stackalloc SaveDataInfo [ 8 ] ;
2021-12-23 17:55:50 +01:00
using var iterator = new UniqueRef < SaveDataIterator > ( ) ;
2023-03-02 03:42:27 +01:00
Result rc = hos . Fs . OpenSaveDataIterator ( ref iterator . Ref , spaceId ) ;
2023-07-16 19:31:14 +02:00
if ( rc . IsFailure ( ) )
{
return rc ;
}
2021-08-12 23:56:24 +02:00
while ( true )
{
2021-12-23 17:55:50 +01:00
rc = iterator . Get . ReadSaveDataInfo ( out long count , info ) ;
2023-07-16 19:31:14 +02:00
if ( rc . IsFailure ( ) )
{
return rc ;
}
2021-08-12 23:56:24 +02:00
if ( count = = 0 )
2023-07-16 19:31:14 +02:00
{
2021-08-12 23:56:24 +02:00
return Result . Success ;
2023-07-16 19:31:14 +02:00
}
2021-08-12 23:56:24 +02:00
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 )
2023-07-16 19:31:14 +02:00
{
2021-08-12 23:56:24 +02:00
return Result . Success ;
2023-07-16 19:31:14 +02:00
}
2021-08-12 23:56:24 +02:00
2023-06-28 01:18:19 +02:00
const string MountName = "SaveDir" ;
var mountNameU8 = MountName . ToU8Span ( ) ;
2021-08-12 23:56:24 +02:00
BisPartitionId partitionId = info . SpaceId switch
{
SaveDataSpaceId . System = > BisPartitionId . System ,
SaveDataSpaceId . User = > BisPartitionId . User ,
2023-07-16 19:31:14 +02:00
_ = > throw new ArgumentOutOfRangeException ( nameof ( info ) , info . SpaceId , null ) ,
2021-08-12 23:56:24 +02:00
} ;
Result rc = hos . Fs . MountBis ( mountNameU8 , partitionId ) ;
2023-07-16 19:31:14 +02:00
if ( rc . IsFailure ( ) )
{
return rc ;
}
2021-08-12 23:56:24 +02:00
try
{
2023-06-28 01:18:19 +02:00
var path = $"{MountName}:/save/{info.SaveDataId:x16}" . ToU8Span ( ) ;
2021-08-12 23:56:24 +02:00
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 ;
2023-07-16 19:31:14 +02:00
List < ulong > localList = new ( ) ;
2021-08-12 23:56:24 +02:00
try
{
Result rc = hos . Fs . MountBis ( mountName , BisPartitionId . System ) ;
2023-07-16 19:31:14 +02:00
if ( rc . IsFailure ( ) )
{
return rc ;
}
2021-08-12 23:56:24 +02:00
rc = hos . Fs . OpenDirectory ( out handle , "system:/save" . ToU8Span ( ) , OpenDirectoryMode . All ) ;
2023-07-16 19:31:14 +02:00
if ( rc . IsFailure ( ) )
{
return rc ;
}
2021-08-12 23:56:24 +02:00
2023-07-16 19:31:14 +02:00
DirectoryEntry entry = new ( ) ;
2021-08-12 23:56:24 +02:00
while ( true )
{
rc = hos . Fs . ReadDirectory ( out long readCount , SpanHelpers . AsSpan ( ref entry ) , handle ) ;
2023-07-16 19:31:14 +02:00
if ( rc . IsFailure ( ) )
{
return rc ;
}
2021-08-12 23:56:24 +02:00
if ( readCount = = 0 )
2023-07-16 19:31:14 +02:00
{
2021-08-12 23:56:24 +02:00
break ;
2023-07-16 19:31:14 +02:00
}
2021-08-12 23:56:24 +02:00
2023-07-16 19:31:14 +02:00
if ( Utf8Parser . TryParse ( entry . Name , out ulong saveDataId , out int bytesRead , 'x' ) & & bytesRead = = 16 & & ( long ) saveDataId < 0 )
2021-08-12 23:56:24 +02:00
{
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 )
{
2023-07-16 19:31:14 +02:00
foreach ( var fixInfo in _systemExtraDataFixInfo )
2021-08-12 23:56:24 +02:00
{
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 ) )
2023-07-16 19:31:14 +02:00
{
2021-08-12 23:56:24 +02:00
return rc ;
2023-07-16 19:31:14 +02:00
}
2021-08-12 23:56:24 +02:00
// 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 ,
2023-07-16 19:31:14 +02:00
JournalSize = info . JournalSize ,
2021-08-12 23:56:24 +02:00
} ;
// 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 ;
2023-07-16 19:31:14 +02:00
Result rc = hos . Fs . Impl . ReadSaveDataFileSystemExtraData ( out SaveDataExtraData extraData , info . SpaceId , info . SaveDataId ) ;
if ( rc . IsFailure ( ) )
{
return rc ;
}
2021-08-12 23:56:24 +02:00
// 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 20:46:16 +01:00
bool hasEmptyOwnerId = extraData . OwnerId = = 0 & & info . Type ! = SaveDataType . System ;
2022-02-02 22:49:49 +01:00
if ( ! canFixByProgramId & & ! canFixBySaveDataId & & ! hasEmptyOwnerId )
2021-08-12 23:56:24 +02: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 20:46:16 +01:00
if ( info . Type ! = SaveDataType . System )
2021-08-12 23:56:24 +02:00
{
extraData . OwnerId = info . ProgramId . Value ;
}
else
{
// Try to match the system save with one of the known saves
2023-07-16 19:31:14 +02:00
foreach ( ExtraDataFixInfo fixInfo in _systemExtraDataFixInfo )
2021-08-12 23:56:24 +02:00
{
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 ;
}
2023-07-16 19:31:14 +02:00
private static readonly ExtraDataFixInfo [ ] _systemExtraDataFixInfo =
2021-08-12 23:56:24 +02:00
{
new ExtraDataFixInfo ( )
{
StaticSaveDataId = 0x8000000000000030 ,
OwnerId = 0x010000000000001F ,
Flags = SaveDataFlags . KeepAfterResettingSystemSaveDataWithoutUserSaveData ,
DataSize = 0x10000 ,
2023-07-16 19:31:14 +02:00
JournalSize = 0x10000 ,
2021-08-12 23:56:24 +02:00
} ,
new ExtraDataFixInfo ( )
{
StaticSaveDataId = 0x8000000000001040 ,
OwnerId = 0x0100000000001009 ,
Flags = SaveDataFlags . None ,
DataSize = 0xC000 ,
2023-07-16 19:31:14 +02:00
JournalSize = 0xC000 ,
} ,
2021-08-12 23:56:24 +02:00
} ;
2018-02-05 00:08:20 +01:00
public void Dispose ( )
{
2023-07-16 19:31:14 +02:00
GC . SuppressFinalize ( this ) ;
2018-02-05 00:08:20 +01:00
Dispose ( true ) ;
}
protected virtual void Dispose ( bool disposing )
{
2018-02-20 11:54:00 +01:00
if ( disposing )
2018-02-05 00:08:20 +01:00
{
2023-01-18 14:50:42 +01:00
foreach ( var stream in _romFsByPid . Values )
{
stream . Close ( ) ;
}
_romFsByPid . Clear ( ) ;
2018-02-05 00:08:20 +01:00
}
}
}
2023-06-28 01:18:19 +02:00
}