Add SaveDataIndexer

This commit is contained in:
Alex Barney 2019-10-17 10:52:35 -05:00
parent dee7c93285
commit d330d11de2
13 changed files with 533 additions and 16 deletions

View file

@ -137,5 +137,6 @@
public static Result SubStorageNotInitialized => new Result(ModuleFs, 6902);
public static Result MountNameNotFound => new Result(ModuleFs, 6905);
public static Result Result6906 => new Result(ModuleFs, 6906);
}
}

View file

@ -12,6 +12,7 @@
public static ResultRange FatFsCorrupted => new ResultRange(ResultFs.ModuleFs, 4681, 4699);
public static ResultRange HostFsCorrupted => new ResultRange(ResultFs.ModuleFs, 4701, 4719);
public static ResultRange FileTableCorrupted => new ResultRange(ResultFs.ModuleFs, 4721, 4739);
public static ResultRange Range4771To4779 => new ResultRange(ResultFs.ModuleFs, 4771, 4779);
public static ResultRange Range4811To4819 => new ResultRange(ResultFs.ModuleFs, 4811, 4819);
public static ResultRange UnexpectedFailure => new ResultRange(ResultFs.ModuleFs, 5000, 5999);

View file

@ -1,10 +1,68 @@
using LibHac.Common;
using System;
using LibHac.Common;
using LibHac.FsService;
using LibHac.FsSystem.Save;
using LibHac.Ncm;
namespace LibHac.Fs
{
public static class SaveData
{
public static Result MountSaveData(this FileSystemClient fs, U8Span mountName, TitleId titleId, UserId userId)
{
Result rc;
if (fs.IsEnabledAccessLog(LocalAccessLogMode.Application))
{
TimeSpan startTime = fs.Time.GetCurrent();
rc = MountSaveDataImpl(fs, mountName, SaveDataSpaceId.User, titleId, userId, SaveDataType.SaveData, false, 0);
TimeSpan endTime = fs.Time.GetCurrent();
fs.OutputAccessLog(rc, startTime, endTime, $", name: \"{mountName.ToString()}\", applicationid: 0x{titleId}, userid: {userId}");
}
else
{
rc = MountSaveDataImpl(fs, mountName, SaveDataSpaceId.User, titleId, userId, SaveDataType.SaveData, false, 0);
}
if (rc.IsSuccess() && fs.IsEnabledAccessLog(LocalAccessLogMode.Application))
{
fs.EnableFileSystemAccessorAccessLog(mountName);
}
return rc;
}
private static Result MountSaveDataImpl(this FileSystemClient fs, U8Span mountName, SaveDataSpaceId spaceId,
TitleId titleId, UserId userId, SaveDataType type, bool openReadOnly, short index)
{
Result rc = MountHelpers.CheckMountName(mountName);
if (rc.IsFailure()) return rc;
IFileSystemProxy fsProxy = fs.GetFileSystemProxyServiceObject();
SaveDataAttribute attribute = default;
attribute.TitleId = titleId;
attribute.UserId = userId;
attribute.Type = type;
attribute.Index = index;
IFileSystem saveFs;
if (openReadOnly)
{
rc = fsProxy.OpenReadOnlySaveDataFileSystem(out saveFs, spaceId, ref attribute);
}
else
{
rc = fsProxy.OpenSaveDataFileSystem(out saveFs, spaceId, ref attribute);
}
if (rc.IsFailure()) return rc;
return fs.Register(mountName, saveFs);
}
public static Result MountSystemSaveData(this FileSystemClient fs, U8Span mountName, SaveDataSpaceId spaceId, ulong saveDataId)
{
return MountSystemSaveData(fs, mountName, spaceId, saveDataId, UserId.Zero);
@ -98,6 +156,17 @@ namespace LibHac.Fs
() => $", savedataid: 0x{saveDataId:X}");
}
public static Result DeleteSaveData(this FileSystemClient fs, SaveDataSpaceId spaceId, ulong saveDataId)
{
return fs.RunOperationWithAccessLog(LocalAccessLogMode.System,
() =>
{
IFileSystemProxy fsProxy = fs.GetFileSystemProxyServiceObject();
return fsProxy.DeleteSaveDataFileSystemBySaveDataSpaceId(spaceId, saveDataId);
},
() => $", savedataspaceid: {spaceId}, savedataid: 0x{saveDataId:X}");
}
public static Result DisableAutoSaveDataCreation(this FileSystemClient fsClient)
{
IFileSystemProxy fsProxy = fsClient.GetFileSystemProxyServiceObject();

View file

@ -9,7 +9,7 @@ namespace LibHac.Fs
[StructLayout(LayoutKind.Explicit, Size = 0x40)]
public struct SaveDataAttribute : IEquatable<SaveDataAttribute>, IComparable<SaveDataAttribute>
{
[FieldOffset(0x00)] public ulong TitleId;
[FieldOffset(0x00)] public TitleId TitleId;
[FieldOffset(0x08)] public UserId UserId;
[FieldOffset(0x18)] public ulong SaveDataId;
[FieldOffset(0x20)] public SaveDataType Type;
@ -71,7 +71,7 @@ namespace LibHac.Fs
[FieldOffset(0x04)] public bool FilterByIndex;
[FieldOffset(0x05)] public byte Rank;
[FieldOffset(0x08)] public TitleId TitleID;
[FieldOffset(0x08)] public TitleId TitleId;
[FieldOffset(0x10)] public UserId UserId;
[FieldOffset(0x20)] public ulong SaveDataId;
[FieldOffset(0x28)] public SaveDataType SaveDataType;

View file

@ -25,7 +25,7 @@ namespace LibHac.Fs
public override string ToString()
{
return $"0x{Id.High:x8}{Id.Low:x8}";
return $"0x{Id.High:X16}{Id.Low:X16}";
}
public bool Equals(UserId other) => Id == other.Id;

View file

@ -2,6 +2,7 @@
using LibHac.Common;
using LibHac.Fs;
using LibHac.FsSystem;
using LibHac.FsSystem.Save;
using LibHac.Ncm;
using LibHac.Spl;
@ -14,6 +15,7 @@ namespace LibHac.FsService
/// <summary>The client instance to be used for internal operations like save indexer access.</summary>
// ReSharper disable once UnusedAutoPropertyAccessor.Local
private FileSystemClient FsClient { get; }
private FileSystemServer FsServer { get; }
public long CurrentProcess { get; private set; }
@ -24,10 +26,11 @@ namespace LibHac.FsService
private const ulong SaveIndexerId = 0x8000000000000000;
internal FileSystemProxy(FileSystemProxyCore fsProxyCore, FileSystemClient fsClient)
internal FileSystemProxy(FileSystemProxyCore fsProxyCore, FileSystemClient fsClient, FileSystemServer fsServer)
{
FsProxyCore = fsProxyCore;
FsClient = fsClient;
FsServer = fsServer;
CurrentProcess = -1;
SaveDataSize = 0x2000000;
@ -147,6 +150,9 @@ namespace LibHac.FsService
private Result OpenSaveDataFileSystemImpl(out IFileSystem fileSystem, out ulong saveDataId,
SaveDataSpaceId spaceId, ref SaveDataAttribute attribute, bool openReadOnly, bool cacheExtraData)
{
fileSystem = default;
saveDataId = default;
bool hasFixedId = attribute.SaveDataId != 0 && attribute.UserId == UserId.Zero;
if (hasFixedId)
@ -155,7 +161,25 @@ namespace LibHac.FsService
}
else
{
throw new NotImplementedException();
SaveDataAttribute indexerKey = attribute;
Result rc = FsServer.SaveDataIndexerManager.GetSaveDataIndexer(out SaveDataIndexerReader reader, spaceId);
using SaveDataIndexerReader c = reader;
if (rc.IsFailure()) return rc;
c.Indexer.Get(out SaveDataIndexerValue indexerValue, ref indexerKey);
SaveDataSpaceId indexerSpaceId = spaceId == SaveDataSpaceId.ProperSystem || spaceId == SaveDataSpaceId.Safe
? SaveDataSpaceId.System
: spaceId;
if (indexerValue.SpaceId != indexerSpaceId)
return ResultFs.TargetNotFound.Log();
if (indexerValue.State == 4)
return ResultFs.Result6906.Log();
saveDataId = indexerValue.SaveDataId;
}
Result saveFsResult = FsProxyCore.OpenSaveDataFileSystem(out fileSystem, spaceId, saveDataId,
@ -167,7 +191,6 @@ namespace LibHac.FsService
if (saveDataId != SaveIndexerId)
{
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
if (hasFixedId)
{
// todo: remove save indexer entry
@ -177,15 +200,61 @@ namespace LibHac.FsService
return ResultFs.TargetNotFound;
}
private Result OpenSaveDataFileSystem3(out IFileSystem fileSystem, SaveDataSpaceId spaceId,
ref SaveDataAttribute attribute, bool openReadOnly)
{
// Missing check if the open limit has been hit
Result rc = OpenSaveDataFileSystemImpl(out fileSystem, out _, spaceId, ref attribute, openReadOnly, true);
// Missing permission check based on the save's owner ID,
// speed emulation storage type wrapper, and FileSystemInterfaceAdapter
return rc;
}
private Result OpenSaveDataFileSystem2(out IFileSystem fileSystem, SaveDataSpaceId spaceId,
ref SaveDataAttribute attribute, bool openReadOnly)
{
fileSystem = default;
// Missing permission checks
SaveDataAttribute attributeCopy;
if (attribute.TitleId == TitleId.Zero)
{
throw new NotImplementedException();
}
else
{
attributeCopy = attribute;
}
SaveDataSpaceId actualSpaceId;
if (attributeCopy.Type == SaveDataType.CacheStorage)
{
// Check whether the save is on the SD card or the BIS
throw new NotImplementedException();
}
else
{
actualSpaceId = spaceId;
}
return OpenSaveDataFileSystem3(out fileSystem, actualSpaceId, ref attributeCopy, openReadOnly);
}
public Result OpenSaveDataFileSystem(out IFileSystem fileSystem, SaveDataSpaceId spaceId, ref SaveDataAttribute attribute)
{
throw new NotImplementedException();
return OpenSaveDataFileSystem2(out fileSystem, spaceId, ref attribute, false);
}
public Result OpenReadOnlySaveDataFileSystem(out IFileSystem fileSystem, SaveDataSpaceId spaceId,
ref SaveDataAttribute attribute)
{
throw new NotImplementedException();
return OpenSaveDataFileSystem2(out fileSystem, spaceId, ref attribute, true);
}
public Result OpenSaveDataFileSystemBySystemSaveDataId(out IFileSystem fileSystem, SaveDataSpaceId spaceId,

View file

@ -6,22 +6,26 @@ namespace LibHac.FsService
{
public class FileSystemServer
{
internal const ulong SaveDataIndexerSaveId = 0x8000000000000000;
private FileSystemProxyCore FsProxyCore { get; }
/// <summary>The client instance to be used for internal operations like save indexer access.</summary>
private FileSystemClient FsClient { get; }
private ITimeSpanGenerator Timer { get; }
internal SaveDataIndexerManager SaveDataIndexerManager { get; }
/// <summary>
/// Creates a new <see cref="FileSystemServer"/>.
/// </summary>
/// <param name="config">The configuration for the created <see cref="FileSystemServer"/>.</param>
public FileSystemServer(FileSystemServerConfig config)
{
if(config.FsCreators == null)
if (config.FsCreators == null)
throw new ArgumentException("FsCreators must not be null");
if(config.DeviceOperator == null)
if (config.DeviceOperator == null)
throw new ArgumentException("DeviceOperator must not be null");
ExternalKeySet externalKeySet = config.ExternalKeySet ?? new ExternalKeySet();
@ -30,6 +34,8 @@ namespace LibHac.FsService
FsProxyCore = new FileSystemProxyCore(config.FsCreators, externalKeySet, config.DeviceOperator);
FsClient = new FileSystemClient(this, timer);
Timer = timer;
SaveDataIndexerManager = new SaveDataIndexerManager(FsClient, SaveDataIndexerSaveId);
}
/// <summary>
@ -52,7 +58,7 @@ namespace LibHac.FsService
public IFileSystemProxy CreateFileSystemProxyService()
{
return new FileSystemProxy(FsProxyCore, FsClient);
return new FileSystemProxy(FsProxyCore, FsClient, this);
}
}

View file

@ -0,0 +1,9 @@
using LibHac.Fs;
namespace LibHac.FsService
{
public interface ISaveDataIndexer
{
Result Get(out SaveDataIndexerValue value, ref SaveDataAttribute key);
}
}

View file

@ -0,0 +1,219 @@
using System.Diagnostics;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Kvdb;
namespace LibHac.FsService
{
public class SaveDataIndexer : ISaveDataIndexer
{
private const string LastIdFileName = "lastPublishedId";
private const long LastIdFileSize = 8;
private FileSystemClient FsClient { get; }
private string MountName { get; }
private ulong SaveDataId { get; }
private SaveDataSpaceId SpaceId { get; }
private KeyValueDatabase<SaveDataAttribute> KvDatabase { get; set; }
private object Locker { get; } = new object();
private bool IsInitialized { get; set; }
private bool IsKvdbLoaded { get; set; }
private long LastPublishedId { get; set; }
public SaveDataIndexer(FileSystemClient fsClient, string mountName, SaveDataSpaceId spaceId, ulong saveDataId)
{
FsClient = fsClient;
MountName = mountName;
SaveDataId = saveDataId;
SpaceId = spaceId;
}
public Result Get(out SaveDataIndexerValue value, ref SaveDataAttribute key)
{
value = default;
lock (Locker)
{
Result rc = Initialize();
if (rc.IsFailure()) return rc;
rc = EnsureKvDatabaseLoaded(false);
if (rc.IsFailure()) return rc;
rc = KvDatabase.Get(ref key, SpanHelpers.AsByteSpan(ref value));
if (rc.IsFailure())
{
return ResultFs.TargetNotFound.LogConverted(rc);
}
return Result.Success;
}
}
private Result Initialize()
{
if (IsInitialized) return Result.Success;
var mount = new Mounter();
try
{
Result rc = mount.Initialize(FsClient, MountName, SpaceId, SaveDataId);
if (rc.IsFailure()) return rc;
string dbDirectory = $"{MountName}:/";
rc = FsClient.GetEntryType(out DirectoryEntryType entryType, dbDirectory);
if (rc.IsFailure()) return rc;
if (entryType == DirectoryEntryType.File)
return ResultFs.PathNotFound.Log();
string dbArchiveFile = $"{dbDirectory}imkvdb.arc";
KvDatabase = new KeyValueDatabase<SaveDataAttribute>(FsClient, dbArchiveFile);
IsInitialized = true;
return Result.Success;
}
finally
{
mount.Dispose();
}
}
private Result EnsureKvDatabaseLoaded(bool forceLoad)
{
Debug.Assert(KvDatabase != null);
if (forceLoad)
{
IsKvdbLoaded = false;
}
else if (IsKvdbLoaded)
{
return Result.Success;
}
var mount = new Mounter();
try
{
Result rc = mount.Initialize(FsClient, MountName, SpaceId, SaveDataId);
if (rc.IsFailure()) return rc;
rc = KvDatabase.ReadDatabaseFromFile();
if (rc.IsFailure()) return rc;
bool createdNewFile = false;
string idFilePath = $"{MountName}:/{LastIdFileName}";
rc = FsClient.OpenFile(out FileHandle handle, idFilePath, OpenMode.Read);
if (rc.IsFailure())
{
if (rc != ResultFs.PathNotFound) return rc;
rc = FsClient.CreateFile(idFilePath, LastIdFileSize);
if (rc.IsFailure()) return rc;
rc = FsClient.OpenFile(out handle, idFilePath, OpenMode.Read);
if (rc.IsFailure()) return rc;
createdNewFile = true;
LastPublishedId = 0;
IsKvdbLoaded = true;
}
try
{
if (!createdNewFile)
{
long lastId = default;
rc = FsClient.ReadFile(handle, 0, SpanHelpers.AsByteSpan(ref lastId));
if (rc.IsFailure()) return rc;
LastPublishedId = lastId;
IsKvdbLoaded = true;
}
return Result.Success;
}
finally
{
FsClient.CloseFile(handle);
if (createdNewFile)
{
FsClient.Commit(MountName);
}
}
}
finally
{
mount.Dispose();
}
}
private ref struct Mounter
{
private FileSystemClient FsClient { get; set; }
private string MountName { get; set; }
private bool IsMounted { get; set; }
public Result Initialize(FileSystemClient fsClient, string mountName, SaveDataSpaceId spaceId,
ulong saveDataId)
{
FsClient = fsClient;
MountName = mountName;
FsClient.DisableAutoSaveDataCreation();
Result rc = FsClient.MountSystemSaveData(MountName.ToU8Span(), spaceId, saveDataId);
if (rc.IsFailure())
{
if (rc == ResultFs.TargetNotFound)
{
rc = FsClient.CreateSystemSaveData(spaceId, saveDataId, 0, 0xC0000, 0xC0000, 0);
if (rc.IsFailure()) return rc;
rc = FsClient.MountSystemSaveData(MountName.ToU8Span(), spaceId, saveDataId);
if (rc.IsFailure()) return rc;
}
else
{
if (ResultRangeFs.Range4771To4779.Contains(rc)) return rc;
if (!ResultRangeFs.DataCorrupted.Contains(rc)) return rc;
if (spaceId == SaveDataSpaceId.SdSystem) return rc;
rc = FsClient.DeleteSaveData(spaceId, saveDataId);
if (rc.IsFailure()) return rc;
rc = FsClient.CreateSystemSaveData(spaceId, saveDataId, 0, 0xC0000, 0xC0000, 0);
if (rc.IsFailure()) return rc;
rc = FsClient.MountSystemSaveData(MountName.ToU8Span(), spaceId, saveDataId);
if (rc.IsFailure()) return rc;
}
}
IsMounted = true;
return Result.Success;
}
public void Dispose()
{
if (IsMounted)
{
FsClient.Unmount(MountName);
}
}
}
}
}

View file

@ -0,0 +1,115 @@
using System;
using System.Threading;
using LibHac.Fs;
namespace LibHac.FsService
{
internal class SaveDataIndexerManager
{
private FileSystemClient FsClient { get; }
private ulong SaveDataId { get; }
private IndexerHolder _bisIndexer = new IndexerHolder(new object());
private IndexerHolder _sdCardIndexer = new IndexerHolder(new object());
private IndexerHolder _safeIndexer = new IndexerHolder(new object());
private IndexerHolder _properSystemIndexer = new IndexerHolder(new object());
public SaveDataIndexerManager(FileSystemClient fsClient, ulong saveDataId)
{
FsClient = fsClient;
SaveDataId = saveDataId;
}
public Result GetSaveDataIndexer(out SaveDataIndexerReader reader, SaveDataSpaceId spaceId)
{
switch (spaceId)
{
case SaveDataSpaceId.System:
case SaveDataSpaceId.User:
Monitor.Enter(_bisIndexer.Locker);
if (!_bisIndexer.IsInitialized)
{
_bisIndexer.Indexer = new SaveDataIndexer(FsClient, "saveDataIxrDb", SaveDataSpaceId.System, SaveDataId);
}
reader = new SaveDataIndexerReader(_bisIndexer.Indexer, _bisIndexer.Locker);
return Result.Success;
case SaveDataSpaceId.SdSystem:
case SaveDataSpaceId.SdCache:
Monitor.Enter(_sdCardIndexer.Locker);
// Missing reinitialize if SD handle is old
if (!_sdCardIndexer.IsInitialized)
{
_sdCardIndexer.Indexer = new SaveDataIndexer(FsClient, "saveDataIxrDbSd", SaveDataSpaceId.SdSystem, SaveDataId);
}
reader = new SaveDataIndexerReader(_bisIndexer.Indexer, _bisIndexer.Locker);
return Result.Success;
case SaveDataSpaceId.TemporaryStorage:
throw new NotImplementedException();
case SaveDataSpaceId.ProperSystem:
Monitor.Enter(_safeIndexer.Locker);
if (!_safeIndexer.IsInitialized)
{
_safeIndexer.Indexer = new SaveDataIndexer(FsClient, "saveDataIxrDbPr", SaveDataSpaceId.ProperSystem, SaveDataId);
}
reader = new SaveDataIndexerReader(_safeIndexer.Indexer, _safeIndexer.Locker);
return Result.Success;
case SaveDataSpaceId.Safe:
Monitor.Enter(_properSystemIndexer.Locker);
if (!_properSystemIndexer.IsInitialized)
{
_properSystemIndexer.Indexer = new SaveDataIndexer(FsClient, "saveDataIxrDbSf", SaveDataSpaceId.Safe, SaveDataId);
}
reader = new SaveDataIndexerReader(_properSystemIndexer.Indexer, _properSystemIndexer.Locker);
return Result.Success;
default:
reader = default;
return ResultFs.InvalidArgument.Log();
}
}
private struct IndexerHolder
{
public object Locker { get; }
public ISaveDataIndexer Indexer { get; set; }
public IndexerHolder(object locker)
{
Locker = locker;
Indexer = null;
}
public bool IsInitialized => Indexer != null;
}
}
public ref struct SaveDataIndexerReader
{
private object Locker;
public ISaveDataIndexer Indexer;
internal SaveDataIndexerReader(ISaveDataIndexer indexer, object locker)
{
Locker = locker;
Indexer = indexer;
}
public void Dispose()
{
Monitor.Exit(Locker);
}
}
}

View file

@ -4,7 +4,7 @@ using LibHac.Fs;
namespace LibHac.FsService
{
[StructLayout(LayoutKind.Explicit, Size = 0x40)]
public struct SaveDataIndexerEntry
public struct SaveDataIndexerValue
{
[FieldOffset(0x00)] public ulong SaveDataId;
[FieldOffset(0x08)] public ulong Size;

View file

@ -1,10 +1,13 @@
using System.Diagnostics;
using System;
using System.Diagnostics;
namespace LibHac.Ncm
{
[DebuggerDisplay("{" + nameof(Value) + "}")]
public struct TitleId
public struct TitleId : IEquatable<TitleId>, IComparable<TitleId>, IComparable
{
public static TitleId Zero => default;
public readonly ulong Value;
public TitleId(ulong value)
@ -13,5 +16,20 @@ namespace LibHac.Ncm
}
public static explicit operator ulong(TitleId titleId) => titleId.Value;
public override string ToString() => $"{Value:X16}";
public int CompareTo(object obj)
{
if (ReferenceEquals(null, obj)) return 1;
return obj is TitleId other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(TitleId)}");
}
public int CompareTo(TitleId other) => Value.CompareTo(other.Value);
public bool Equals(TitleId other) => Value == other.Value;
public override bool Equals(object obj) => obj is TitleId other && Equals(other);
public override int GetHashCode() => Value.GetHashCode();
public static bool operator ==(TitleId left, TitleId right) => left.Equals(right);
public static bool operator !=(TitleId left, TitleId right) => !left.Equals(right);
}
}

View file

@ -45,6 +45,16 @@ namespace LibHac
return this;
}
/// <summary>
/// Same as <see cref="Log"/>, but for when one result is converted to another.
/// </summary>
/// <param name="originalResult">The original <see cref="Result"/> value.</param>
/// <returns>The called <see cref="Result"/> value.</returns>
public Result LogConverted(Result originalResult)
{
return this;
}
public override string ToString()
{
return IsSuccess() ? "Success" : ErrorCode;