Update to LibHac 0.13.1 (#2475)
* Update to LibHac 0.13.1 * Recreate directories for indexed saves if they're missing on emulator start
This commit is contained in:
parent
3977d1f72b
commit
dadc0e59da
32 changed files with 1869 additions and 584 deletions
|
@ -1,40 +1,46 @@
|
|||
using LibHac;
|
||||
using LibHac.Common;
|
||||
using LibHac.Common.Keys;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
using LibHac.Fs.Shim;
|
||||
using LibHac.FsSrv;
|
||||
using LibHac.FsSystem;
|
||||
using LibHac.Ncm;
|
||||
using LibHac.Spl;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.FileSystem.Content;
|
||||
using Ryujinx.HLE.HOS;
|
||||
using System;
|
||||
using System.Buffers.Text;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using RightsId = LibHac.Fs.RightsId;
|
||||
|
||||
namespace Ryujinx.HLE.FileSystem
|
||||
{
|
||||
public class VirtualFileSystem : IDisposable
|
||||
{
|
||||
public const string NandPath = AppDataManager.DefaultNandDir;
|
||||
public const string NandPath = AppDataManager.DefaultNandDir;
|
||||
public const string SdCardPath = AppDataManager.DefaultSdcardDir;
|
||||
|
||||
public static string SafeNandPath = Path.Combine(NandPath, "safe");
|
||||
public static string SafeNandPath = Path.Combine(NandPath, "safe");
|
||||
public static string SystemNandPath = Path.Combine(NandPath, "system");
|
||||
public static string UserNandPath = Path.Combine(NandPath, "user");
|
||||
|
||||
public static string UserNandPath = Path.Combine(NandPath, "user");
|
||||
|
||||
private static bool _isInitialized = false;
|
||||
|
||||
public Keyset KeySet { get; private set; }
|
||||
public FileSystemServer FsServer { get; private set; }
|
||||
public FileSystemClient FsClient { get; private set; }
|
||||
public KeySet KeySet { get; private set; }
|
||||
public EmulatedGameCard GameCard { get; private set; }
|
||||
public EmulatedSdCard SdCard { get; private set; }
|
||||
public EmulatedSdCard SdCard { get; private set; }
|
||||
|
||||
public ModLoader ModLoader {get; private set;}
|
||||
public ModLoader ModLoader { get; private set; }
|
||||
|
||||
private VirtualFileSystem()
|
||||
{
|
||||
Reload();
|
||||
ReloadKeySet();
|
||||
ModLoader = new ModLoader(); // Should only be created once
|
||||
}
|
||||
|
||||
|
@ -80,39 +86,6 @@ namespace Ryujinx.HLE.FileSystem
|
|||
internal string GetSdCardPath() => MakeFullPath(SdCardPath);
|
||||
public string GetNandPath() => MakeFullPath(NandPath);
|
||||
|
||||
internal string GetSavePath(ServiceCtx context, SaveInfo saveInfo, bool isDirectory = true)
|
||||
{
|
||||
string saveUserPath = "";
|
||||
string baseSavePath = NandPath;
|
||||
ulong currentTitleId = saveInfo.TitleId;
|
||||
|
||||
switch (saveInfo.SaveSpaceId)
|
||||
{
|
||||
case SaveSpaceId.NandUser: baseSavePath = UserNandPath; break;
|
||||
case SaveSpaceId.NandSystem: baseSavePath = SystemNandPath; break;
|
||||
case SaveSpaceId.SdCard: baseSavePath = Path.Combine(SdCardPath, "Nintendo"); break;
|
||||
}
|
||||
|
||||
baseSavePath = Path.Combine(baseSavePath, "save");
|
||||
|
||||
if (saveInfo.TitleId == 0 && saveInfo.SaveDataType == SaveDataType.SaveData)
|
||||
{
|
||||
currentTitleId = context.Process.TitleId;
|
||||
}
|
||||
|
||||
if (saveInfo.SaveSpaceId == SaveSpaceId.NandUser)
|
||||
{
|
||||
saveUserPath = saveInfo.UserId.IsNull ? "savecommon" : saveInfo.UserId.ToString();
|
||||
}
|
||||
|
||||
string savePath = Path.Combine(baseSavePath,
|
||||
saveInfo.SaveId.ToString("x16"),
|
||||
saveUserPath,
|
||||
saveInfo.SaveDataType == SaveDataType.SaveData ? currentTitleId.ToString("x16") : string.Empty);
|
||||
|
||||
return MakeFullPath(savePath, isDirectory);
|
||||
}
|
||||
|
||||
public string GetFullPartitionPath(string partitionPath)
|
||||
{
|
||||
return MakeFullPath(partitionPath);
|
||||
|
@ -136,8 +109,8 @@ namespace Ryujinx.HLE.FileSystem
|
|||
|
||||
if (systemPath.StartsWith(baseSystemPath))
|
||||
{
|
||||
string rawPath = systemPath.Replace(baseSystemPath, "");
|
||||
int firstSeparatorOffset = rawPath.IndexOf(Path.DirectorySeparatorChar);
|
||||
string rawPath = systemPath.Replace(baseSystemPath, "");
|
||||
int firstSeparatorOffset = rawPath.IndexOf(Path.DirectorySeparatorChar);
|
||||
|
||||
if (firstSeparatorOffset == -1)
|
||||
{
|
||||
|
@ -196,35 +169,36 @@ namespace Ryujinx.HLE.FileSystem
|
|||
return new DriveInfo(Path.GetPathRoot(GetBasePath()));
|
||||
}
|
||||
|
||||
public void Reload()
|
||||
public void InitializeFsServer(LibHac.Horizon horizon, out HorizonClient fsServerClient)
|
||||
{
|
||||
ReloadKeySet();
|
||||
|
||||
LocalFileSystem serverBaseFs = new LocalFileSystem(GetBasePath());
|
||||
|
||||
DefaultFsServerObjects fsServerObjects = DefaultFsServerObjects.GetDefaultEmulatedCreators(serverBaseFs, KeySet);
|
||||
fsServerClient = horizon.CreatePrivilegedHorizonClient();
|
||||
var fsServer = new FileSystemServer(fsServerClient);
|
||||
|
||||
DefaultFsServerObjects fsServerObjects = DefaultFsServerObjects.GetDefaultEmulatedCreators(serverBaseFs, KeySet, fsServer);
|
||||
|
||||
GameCard = fsServerObjects.GameCard;
|
||||
SdCard = fsServerObjects.SdCard;
|
||||
SdCard = fsServerObjects.SdCard;
|
||||
|
||||
SdCard.SetSdCardInsertionStatus(true);
|
||||
|
||||
FileSystemServerConfig fsServerConfig = new FileSystemServerConfig
|
||||
var fsServerConfig = new FileSystemServerConfig
|
||||
{
|
||||
FsCreators = fsServerObjects.FsCreators,
|
||||
DeviceOperator = fsServerObjects.DeviceOperator,
|
||||
ExternalKeySet = KeySet.ExternalKeySet
|
||||
ExternalKeySet = KeySet.ExternalKeySet,
|
||||
FsCreators = fsServerObjects.FsCreators
|
||||
};
|
||||
|
||||
FsServer = new FileSystemServer(fsServerConfig);
|
||||
FsClient = FsServer.CreateFileSystemClient();
|
||||
FileSystemServerInitializer.InitializeWithConfig(fsServerClient, fsServer, fsServerConfig);
|
||||
}
|
||||
|
||||
|
||||
private void ReloadKeySet()
|
||||
public void ReloadKeySet()
|
||||
{
|
||||
string keyFile = null;
|
||||
string titleKeyFile = null;
|
||||
KeySet ??= KeySet.CreateDefaultKeySet();
|
||||
|
||||
string keyFile = null;
|
||||
string titleKeyFile = null;
|
||||
string consoleKeyFile = null;
|
||||
|
||||
if (AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile)
|
||||
|
@ -236,8 +210,8 @@ namespace Ryujinx.HLE.FileSystem
|
|||
|
||||
void LoadSetAtPath(string basePath)
|
||||
{
|
||||
string localKeyFile = Path.Combine(basePath, "prod.keys");
|
||||
string localTitleKeyFile = Path.Combine(basePath, "title.keys");
|
||||
string localKeyFile = Path.Combine(basePath, "prod.keys");
|
||||
string localTitleKeyFile = Path.Combine(basePath, "title.keys");
|
||||
string localConsoleKeyFile = Path.Combine(basePath, "console.keys");
|
||||
|
||||
if (File.Exists(localKeyFile))
|
||||
|
@ -256,7 +230,7 @@ namespace Ryujinx.HLE.FileSystem
|
|||
}
|
||||
}
|
||||
|
||||
KeySet = ExternalKeyReader.ReadKeyFile(keyFile, titleKeyFile, consoleKeyFile);
|
||||
ExternalKeyReader.ReadKeyFile(KeySet, keyFile, titleKeyFile, consoleKeyFile, null);
|
||||
}
|
||||
|
||||
public void ImportTickets(IFileSystem fs)
|
||||
|
@ -277,6 +251,324 @@ namespace Ryujinx.HLE.FileSystem
|
|||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
rc = FixExtraDataInSpaceId(hos, SaveDataSpaceId.SdCache);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
private static Result FixExtraDataInSpaceId(HorizonClient hos, SaveDataSpaceId spaceId)
|
||||
{
|
||||
Span<SaveDataInfo> info = stackalloc SaveDataInfo[8];
|
||||
|
||||
Result rc = hos.Fs.OpenSaveDataIterator(out var iterator, spaceId);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
while (true)
|
||||
{
|
||||
rc = iterator.ReadSaveDataInfo(out long count, info);
|
||||
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;
|
||||
|
||||
if (!canFixByProgramId && !canFixBySaveDataId)
|
||||
{
|
||||
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.
|
||||
if (info.Type != LibHac.Fs.SaveDataType.System)
|
||||
{
|
||||
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
|
||||
}
|
||||
};
|
||||
|
||||
public void Unload()
|
||||
{
|
||||
RomFs?.Dispose();
|
||||
|
@ -299,7 +591,7 @@ namespace Ryujinx.HLE.FileSystem
|
|||
{
|
||||
if (_isInitialized)
|
||||
{
|
||||
throw new InvalidOperationException($"VirtualFileSystem can only be instantiated once!");
|
||||
throw new InvalidOperationException("VirtualFileSystem can only be instantiated once!");
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue