mirror of
https://github.com/Thealexbarney/LibHac.git
synced 2025-02-09 13:14:46 +01:00
Add extra data handling to DirectorySaveDataFileSystem
This commit is contained in:
parent
3bf8826a5b
commit
649c72e5e6
@ -1,9 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace LibHac.Common
|
||||
{
|
||||
public interface ITimeStampGenerator
|
||||
{
|
||||
DateTimeOffset Generate();
|
||||
}
|
||||
}
|
@ -25,7 +25,7 @@ namespace LibHac.FsSrv
|
||||
creators.StorageOnNcaCreator = new StorageOnNcaCreator(keySet);
|
||||
creators.TargetManagerFileSystemCreator = new TargetManagerFileSystemCreator();
|
||||
creators.SubDirectoryFileSystemCreator = new SubDirectoryFileSystemCreator();
|
||||
creators.SaveDataFileSystemCreator = new SaveDataFileSystemCreator(keySet);
|
||||
creators.SaveDataFileSystemCreator = new SaveDataFileSystemCreator(keySet, null, null);
|
||||
creators.GameCardStorageCreator = gcStorageCreator;
|
||||
creators.GameCardFileSystemCreator = new EmulatedGameCardFsCreator(gcStorageCreator, gameCard);
|
||||
creators.EncryptedFileSystemCreator = new EncryptedFileSystemCreator(keySet);
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using LibHac.Common.Keys;
|
||||
using LibHac.Fs.Impl;
|
||||
using LibHac.Fs.Shim;
|
||||
using LibHac.FsSrv.FsCreator;
|
||||
@ -73,6 +74,10 @@ namespace LibHac.FsSrv
|
||||
Memory<byte> heapBuffer = GC.AllocateArray<byte>(BufferManagerHeapSize, true);
|
||||
bufferManager.Initialize(BufferManagerCacheSize, heapBuffer, BufferManagerBlockSize);
|
||||
|
||||
// Todo: A non-hacky way of initializing the save data creator
|
||||
config.FsCreators.SaveDataFileSystemCreator =
|
||||
new SaveDataFileSystemCreator(config.KeySet, bufferManager, randomGenerator);
|
||||
|
||||
var saveDataIndexerManager = new SaveDataIndexerManager(server.Hos.Fs, Fs.SaveData.SaveIndexerId,
|
||||
new ArrayPoolMemoryResource(), new SdHandleManager(), false);
|
||||
|
||||
@ -259,5 +264,10 @@ namespace LibHac.FsSrv
|
||||
/// If null, an empty set will be created.
|
||||
/// </summary>
|
||||
public ExternalKeySet ExternalKeySet { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A keyset used for decrypting content.
|
||||
/// </summary>
|
||||
public KeySet KeySet { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
using LibHac.FsSystem;
|
||||
@ -13,7 +12,7 @@ namespace LibHac.FsSrv.FsCreator
|
||||
Result Create(out IFileSystem fileSystem,
|
||||
out ReferenceCountedDisposable<ISaveDataExtraDataAccessor> extraDataAccessor, IFileSystem sourceFileSystem,
|
||||
ulong saveDataId, bool allowDirectorySaveData, bool useDeviceUniqueMac, SaveDataType type,
|
||||
ITimeStampGenerator timeStampGenerator);
|
||||
ISaveDataCommitTimeStampGetter timeStampGetter);
|
||||
|
||||
void SetSdCardEncryptionSeed(ReadOnlySpan<byte> seed);
|
||||
}
|
||||
|
@ -10,11 +10,17 @@ namespace LibHac.FsSrv.FsCreator
|
||||
{
|
||||
public class SaveDataFileSystemCreator : ISaveDataFileSystemCreator
|
||||
{
|
||||
private KeySet KeySet { get; }
|
||||
private IBufferManager _bufferManager;
|
||||
private RandomDataGenerator _randomGenerator;
|
||||
|
||||
public SaveDataFileSystemCreator(KeySet keySet)
|
||||
private KeySet _keySet;
|
||||
|
||||
public SaveDataFileSystemCreator(KeySet keySet, IBufferManager bufferManager,
|
||||
RandomDataGenerator randomGenerator)
|
||||
{
|
||||
KeySet = keySet;
|
||||
_bufferManager = bufferManager;
|
||||
_randomGenerator = randomGenerator;
|
||||
_keySet = keySet;
|
||||
}
|
||||
|
||||
public Result CreateFile(out IFile file, IFileSystem sourceFileSystem, ulong saveDataId, OpenMode openMode)
|
||||
@ -25,7 +31,7 @@ namespace LibHac.FsSrv.FsCreator
|
||||
public Result Create(out IFileSystem fileSystem,
|
||||
out ReferenceCountedDisposable<ISaveDataExtraDataAccessor> extraDataAccessor, IFileSystem sourceFileSystem,
|
||||
ulong saveDataId, bool allowDirectorySaveData, bool useDeviceUniqueMac, SaveDataType type,
|
||||
ITimeStampGenerator timeStampGenerator)
|
||||
ISaveDataCommitTimeStampGetter timeStampGetter)
|
||||
{
|
||||
UnsafeHelpers.SkipParamInit(out fileSystem, out extraDataAccessor);
|
||||
|
||||
@ -64,7 +70,7 @@ namespace LibHac.FsSrv.FsCreator
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
var saveDataStorage = new DisposingFileStorage(saveDataFile);
|
||||
fileSystem = new SaveDataFileSystem(KeySet, saveDataStorage, IntegrityCheckLevel.ErrorOnInvalid,
|
||||
fileSystem = new SaveDataFileSystem(_keySet, saveDataStorage, IntegrityCheckLevel.ErrorOnInvalid,
|
||||
false);
|
||||
|
||||
// Todo: ISaveDataExtraDataAccessor
|
||||
@ -80,4 +86,4 @@ namespace LibHac.FsSrv.FsCreator
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -22,13 +22,30 @@ namespace LibHac.FsSrv
|
||||
// Save data extra data cache
|
||||
// Save data porter manager
|
||||
private bool _isSdCardAccessible;
|
||||
// Timestamp getter
|
||||
private TimeStampGetter _timeStampGetter;
|
||||
|
||||
internal HorizonClient Hos => _config.FsServer.Hos;
|
||||
|
||||
private class TimeStampGetter : ISaveDataCommitTimeStampGetter
|
||||
{
|
||||
private SaveDataFileSystemServiceImpl _saveService;
|
||||
|
||||
public TimeStampGetter(SaveDataFileSystemServiceImpl saveService)
|
||||
{
|
||||
_saveService = saveService;
|
||||
}
|
||||
|
||||
public Result Get(out long timeStamp)
|
||||
{
|
||||
return _saveService.GetSaveDataCommitTimeStamp(out timeStamp);
|
||||
}
|
||||
}
|
||||
|
||||
public SaveDataFileSystemServiceImpl(in Configuration configuration)
|
||||
{
|
||||
_config = configuration;
|
||||
|
||||
_timeStampGetter = new TimeStampGetter(this);
|
||||
}
|
||||
|
||||
public struct Configuration
|
||||
@ -137,7 +154,7 @@ namespace LibHac.FsSrv
|
||||
|
||||
rc = _config.SaveFsCreator.Create(out IFileSystem saveFs,
|
||||
out extraDataAccessor, saveDirectoryFs.Target, saveDataId,
|
||||
allowDirectorySaveData, useDeviceUniqueMac, type, null);
|
||||
allowDirectorySaveData, useDeviceUniqueMac, type, _timeStampGetter);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
saveDataFs = new ReferenceCountedDisposable<IFileSystem>(saveFs);
|
||||
@ -402,6 +419,11 @@ namespace LibHac.FsSrv
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private Result GetSaveDataCommitTimeStamp(out long timeStamp)
|
||||
{
|
||||
return _config.TimeService.GetCurrentPosixTime(out timeStamp);
|
||||
}
|
||||
|
||||
private bool IsSaveEmulated(U8Span saveDataRootPath)
|
||||
{
|
||||
return !saveDataRootPath.IsEmpty();
|
||||
|
@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using LibHac.Common;
|
||||
using LibHac.Diag;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
using LibHac.FsSrv;
|
||||
using LibHac.Os;
|
||||
using LibHac.Util;
|
||||
|
||||
@ -26,7 +28,7 @@ namespace LibHac.FsSystem
|
||||
/// underlying <see cref="IFileSystem"/> is atomic.
|
||||
/// <br/>Based on FS 11.0.0 (nnSdk 11.4.0)
|
||||
/// </remarks>
|
||||
public class DirectorySaveDataFileSystem : IFileSystem
|
||||
public class DirectorySaveDataFileSystem : IFileSystem, ISaveDataExtraDataAccessor
|
||||
{
|
||||
private const int IdealWorkBufferSize = 0x100000; // 1 MiB
|
||||
|
||||
@ -40,13 +42,19 @@ namespace LibHac.FsSystem
|
||||
|
||||
private FileSystemClient _fsClient;
|
||||
private IFileSystem _baseFs;
|
||||
|
||||
private SdkMutexType _mutex;
|
||||
|
||||
// Todo: Unique file system for disposal
|
||||
private int _openWritableFileCount;
|
||||
private bool _isPersistentSaveData;
|
||||
private bool _canCommitProvisionally;
|
||||
private bool _useTransactions;
|
||||
|
||||
// Additions to support extra data
|
||||
private ISaveDataCommitTimeStampGetter _timeStampGetter;
|
||||
private RandomDataGenerator _randomGenerator;
|
||||
|
||||
private class DirectorySaveDataFile : IFile
|
||||
{
|
||||
private IFile _baseFile;
|
||||
@ -86,7 +94,8 @@ namespace LibHac.FsSystem
|
||||
return _baseFile.SetSize(size);
|
||||
}
|
||||
|
||||
protected override Result DoOperateRange(Span<byte> outBuffer, OperationId operationId, long offset, long size, ReadOnlySpan<byte> inBuffer)
|
||||
protected override Result DoOperateRange(Span<byte> outBuffer, OperationId operationId, long offset,
|
||||
long size, ReadOnlySpan<byte> inBuffer)
|
||||
{
|
||||
return _baseFile.OperateRange(outBuffer, operationId, offset, size, inBuffer);
|
||||
}
|
||||
@ -109,11 +118,13 @@ namespace LibHac.FsSystem
|
||||
}
|
||||
|
||||
public static Result CreateNew(out DirectorySaveDataFileSystem created, IFileSystem baseFileSystem,
|
||||
ISaveDataCommitTimeStampGetter timeStampGetter, RandomDataGenerator randomGenerator,
|
||||
bool isPersistentSaveData, bool canCommitProvisionally, bool useTransactions,
|
||||
FileSystemClient fsClient = null)
|
||||
FileSystemClient fsClient)
|
||||
{
|
||||
var obj = new DirectorySaveDataFileSystem(baseFileSystem, fsClient);
|
||||
Result rc = obj.Initialize(isPersistentSaveData, canCommitProvisionally, useTransactions);
|
||||
Result rc = obj.Initialize(timeStampGetter, randomGenerator, isPersistentSaveData, canCommitProvisionally,
|
||||
useTransactions);
|
||||
|
||||
if (rc.IsSuccess())
|
||||
{
|
||||
@ -126,6 +137,13 @@ namespace LibHac.FsSystem
|
||||
return rc;
|
||||
}
|
||||
|
||||
public static Result CreateNew(out DirectorySaveDataFileSystem created, IFileSystem baseFileSystem,
|
||||
bool isPersistentSaveData, bool canCommitProvisionally, bool useTransactions)
|
||||
{
|
||||
return CreateNew(out created, baseFileSystem, null, null, isPersistentSaveData, canCommitProvisionally,
|
||||
useTransactions, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an uninitialized <see cref="DirectorySaveDataFileSystem"/>.
|
||||
/// </summary>
|
||||
@ -158,10 +176,18 @@ namespace LibHac.FsSystem
|
||||
}
|
||||
|
||||
private Result Initialize(bool isPersistentSaveData, bool canCommitProvisionally, bool useTransactions)
|
||||
{
|
||||
return Initialize(null, null, isPersistentSaveData, canCommitProvisionally, useTransactions);
|
||||
}
|
||||
|
||||
private Result Initialize(ISaveDataCommitTimeStampGetter timeStampGetter, RandomDataGenerator randomGenerator,
|
||||
bool isPersistentSaveData, bool canCommitProvisionally, bool useTransactions)
|
||||
{
|
||||
_isPersistentSaveData = isPersistentSaveData;
|
||||
_canCommitProvisionally = canCommitProvisionally;
|
||||
_useTransactions = useTransactions;
|
||||
_timeStampGetter = timeStampGetter ?? _timeStampGetter;
|
||||
_randomGenerator = randomGenerator ?? _randomGenerator;
|
||||
|
||||
// Ensure the working directory exists
|
||||
Result rc = _baseFs.GetEntryType(out _, WorkingDirectoryPath);
|
||||
@ -176,12 +202,11 @@ namespace LibHac.FsSystem
|
||||
if (_isPersistentSaveData)
|
||||
{
|
||||
rc = _baseFs.CreateDirectory(CommittedDirectoryPath);
|
||||
if (rc.IsFailure()) return rc;
|
||||
}
|
||||
|
||||
// Nintendo returns on all failures, but we'll keep going if committed already exists
|
||||
// to avoid confusing people manually creating savedata in emulators
|
||||
if (rc.IsFailure() && !ResultFs.PathAlreadyExists.Includes(rc)) return rc;
|
||||
// Nintendo returns on all failures, but we'll keep going if committed already exists
|
||||
// to avoid confusing people manually creating savedata in emulators
|
||||
if (rc.IsFailure() && !ResultFs.PathAlreadyExists.Includes(rc)) return rc;
|
||||
}
|
||||
}
|
||||
|
||||
// Only the working directory is needed for temporary savedata
|
||||
@ -195,7 +220,10 @@ namespace LibHac.FsSystem
|
||||
if (!_useTransactions)
|
||||
return Result.Success;
|
||||
|
||||
return SynchronizeDirectory(WorkingDirectoryPath, CommittedDirectoryPath);
|
||||
rc = SynchronizeDirectory(WorkingDirectoryPath, CommittedDirectoryPath);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
return InitializeExtraData();
|
||||
}
|
||||
|
||||
if (!ResultFs.PathNotFound.Includes(rc)) return rc;
|
||||
@ -206,7 +234,13 @@ namespace LibHac.FsSystem
|
||||
rc = SynchronizeDirectory(SynchronizingDirectoryPath, WorkingDirectoryPath);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
return _baseFs.RenameDirectory(SynchronizingDirectoryPath, CommittedDirectoryPath);
|
||||
rc = _baseFs.RenameDirectory(SynchronizingDirectoryPath, CommittedDirectoryPath);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
rc = InitializeExtraData();
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
private Result ResolveFullPath(Span<byte> outPath, U8Span relativePath)
|
||||
@ -403,7 +437,7 @@ namespace LibHac.FsSystem
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
// Lock only if initialized with a client
|
||||
if(_fsClient is not null)
|
||||
if (_fsClient is not null)
|
||||
{
|
||||
using ScopedLock<SdkMutexType> lk =
|
||||
ScopedLock.Lock(ref _fsClient.Globals.DirectorySaveDataFileSystem.SynchronizeDirectoryMutex);
|
||||
@ -437,7 +471,9 @@ namespace LibHac.FsSystem
|
||||
|
||||
Result RenameCommittedDir() => _baseFs.RenameDirectory(CommittedDirectoryPath, SynchronizingDirectoryPath);
|
||||
Result SynchronizeWorkingDir() => SynchronizeDirectory(SynchronizingDirectoryPath, WorkingDirectoryPath);
|
||||
Result RenameSynchronizingDir() => _baseFs.RenameDirectory(SynchronizingDirectoryPath, CommittedDirectoryPath);
|
||||
|
||||
Result RenameSynchronizingDir() =>
|
||||
_baseFs.RenameDirectory(SynchronizingDirectoryPath, CommittedDirectoryPath);
|
||||
|
||||
// Get rid of the previous commit by renaming the folder
|
||||
Result rc = Utility.RetryFinitelyForTargetLocked(RenameCommittedDir);
|
||||
@ -506,5 +542,262 @@ namespace LibHac.FsSystem
|
||||
|
||||
_openWritableFileCount--;
|
||||
}
|
||||
|
||||
// The original class doesn't support extra data.
|
||||
// Everything below this point is a LibHac extension.
|
||||
|
||||
private static ReadOnlySpan<byte> CommittedExtraDataBytes => // "/ExtraData0"
|
||||
new[]
|
||||
{
|
||||
(byte)'/', (byte)'E', (byte)'x', (byte)'t', (byte)'r', (byte)'a', (byte)'D', (byte)'a',
|
||||
(byte)'t', (byte)'a', (byte)'0'
|
||||
};
|
||||
|
||||
private static ReadOnlySpan<byte> WorkingExtraDataBytes => // "/ExtraData1"
|
||||
new[]
|
||||
{
|
||||
(byte)'/', (byte)'E', (byte)'x', (byte)'t', (byte)'r', (byte)'a', (byte)'D', (byte)'a',
|
||||
(byte)'t', (byte)'a', (byte)'1'
|
||||
};
|
||||
|
||||
private static ReadOnlySpan<byte> SynchronizingExtraDataBytes => // "/ExtraData_"
|
||||
new[]
|
||||
{
|
||||
(byte)'/', (byte)'E', (byte)'x', (byte)'t', (byte)'r', (byte)'a', (byte)'D', (byte)'a',
|
||||
(byte)'t', (byte)'a', (byte)'_'
|
||||
};
|
||||
|
||||
private U8Span CommittedExtraDataPath => new U8Span(CommittedExtraDataBytes);
|
||||
private U8Span WorkingExtraDataPath => new U8Span(WorkingExtraDataBytes);
|
||||
private U8Span SynchronizingExtraDataPath => new U8Span(SynchronizingExtraDataBytes);
|
||||
|
||||
private Result InitializeExtraData()
|
||||
{
|
||||
// Ensure the extra data files exist
|
||||
Result rc = _baseFs.GetEntryType(out _, WorkingExtraDataPath);
|
||||
|
||||
if (rc.IsFailure())
|
||||
{
|
||||
if (!ResultFs.PathNotFound.Includes(rc)) return rc;
|
||||
|
||||
rc = _baseFs.CreateFile(WorkingExtraDataPath, Unsafe.SizeOf<SaveDataExtraData>());
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
if (_isPersistentSaveData)
|
||||
{
|
||||
rc = _baseFs.CreateFile(CommittedExtraDataPath, Unsafe.SizeOf<SaveDataExtraData>());
|
||||
if (rc.IsFailure() && !ResultFs.PathAlreadyExists.Includes(rc)) return rc;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the working file exists make sure it's the right size
|
||||
rc = EnsureExtraDataSize(WorkingExtraDataPath);
|
||||
if (rc.IsFailure()) return rc;
|
||||
}
|
||||
|
||||
// Only the working extra data is needed for temporary savedata
|
||||
if (!_isPersistentSaveData)
|
||||
return Result.Success;
|
||||
|
||||
rc = _baseFs.GetEntryType(out _, CommittedExtraDataPath);
|
||||
|
||||
if (rc.IsSuccess())
|
||||
{
|
||||
rc = EnsureExtraDataSize(CommittedExtraDataPath);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
if (!_useTransactions)
|
||||
return Result.Success;
|
||||
|
||||
return SynchronizeExtraData(WorkingExtraDataPath, CommittedExtraDataPath);
|
||||
}
|
||||
|
||||
if (!ResultFs.PathNotFound.Includes(rc)) return rc;
|
||||
|
||||
// If a previous commit failed, the committed extra data may be missing.
|
||||
// Finish that commit by copying the working extra data to the committed extra data
|
||||
|
||||
rc = SynchronizeExtraData(SynchronizingExtraDataPath, WorkingExtraDataPath);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
rc = _baseFs.RenameFile(SynchronizingExtraDataPath, CommittedExtraDataPath);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
private Result EnsureExtraDataSize(U8Span path)
|
||||
{
|
||||
IFile file = null;
|
||||
try
|
||||
{
|
||||
Result rc = _baseFs.OpenFile(out file, path, OpenMode.ReadWrite);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
rc = file.GetSize(out long fileSize);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
if (fileSize == Unsafe.SizeOf<SaveDataExtraData>())
|
||||
return Result.Success;
|
||||
|
||||
return file.SetSize(Unsafe.SizeOf<SaveDataExtraData>());
|
||||
}
|
||||
finally
|
||||
{
|
||||
file?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private Result SynchronizeExtraData(U8Span destPath, U8Span sourcePath)
|
||||
{
|
||||
Span<byte> workBuffer = stackalloc byte[Unsafe.SizeOf<SaveDataExtraData>()];
|
||||
|
||||
Result rc = _baseFs.OpenFile(out IFile sourceFile, sourcePath, OpenMode.Read);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
using (sourceFile)
|
||||
{
|
||||
rc = sourceFile.Read(out long bytesRead, 0, workBuffer);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
Assert.SdkEqual(bytesRead, Unsafe.SizeOf<SaveDataExtraData>());
|
||||
}
|
||||
|
||||
rc = _baseFs.OpenFile(out IFile destFile, destPath, OpenMode.Write);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
using (destFile)
|
||||
{
|
||||
rc = destFile.Write(0, workBuffer, WriteOption.Flush);
|
||||
if (rc.IsFailure()) return rc;
|
||||
}
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
private U8Span GetExtraDataPath()
|
||||
{
|
||||
return !_useTransactions && _isPersistentSaveData
|
||||
? CommittedExtraDataPath
|
||||
: WorkingExtraDataPath;
|
||||
}
|
||||
|
||||
public Result WriteExtraData(in SaveDataExtraData extraData)
|
||||
{
|
||||
using ScopedLock<SdkMutexType> lk = ScopedLock.Lock(ref _mutex);
|
||||
|
||||
return WriteExtraDataImpl(in extraData);
|
||||
}
|
||||
|
||||
public Result CommitExtraData(bool updateTimeStamp)
|
||||
{
|
||||
using ScopedLock<SdkMutexType> lk = ScopedLock.Lock(ref _mutex);
|
||||
|
||||
if (updateTimeStamp && _timeStampGetter is not null && _randomGenerator is not null)
|
||||
{
|
||||
Result rc = UpdateExtraDataTimeStamp();
|
||||
if (rc.IsFailure()) return rc;
|
||||
}
|
||||
|
||||
return CommitExtraDataImpl();
|
||||
}
|
||||
|
||||
public Result ReadExtraData(out SaveDataExtraData extraData)
|
||||
{
|
||||
using ScopedLock<SdkMutexType> lk = ScopedLock.Lock(ref _mutex);
|
||||
|
||||
return ReadExtraDataImpl(out extraData);
|
||||
}
|
||||
|
||||
private Result UpdateExtraDataTimeStamp()
|
||||
{
|
||||
Assert.SdkRequires(_mutex.IsLockedByCurrentThread());
|
||||
|
||||
Result rc = ReadExtraDataImpl(out SaveDataExtraData extraData);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
if (_timeStampGetter.Get(out long timeStamp).IsSuccess())
|
||||
{
|
||||
extraData.TimeStamp = (ulong)timeStamp;
|
||||
}
|
||||
|
||||
long commitId = 0;
|
||||
|
||||
do
|
||||
{
|
||||
_randomGenerator(SpanHelpers.AsByteSpan(ref commitId));
|
||||
} while (commitId == 0 || commitId == extraData.CommitId);
|
||||
|
||||
extraData.CommitId = commitId;
|
||||
|
||||
return WriteExtraDataImpl(in extraData);
|
||||
}
|
||||
|
||||
private Result WriteExtraDataImpl(in SaveDataExtraData extraData)
|
||||
{
|
||||
Assert.SdkRequires(_mutex.IsLockedByCurrentThread());
|
||||
|
||||
Result rc = _baseFs.OpenFile(out IFile file, GetExtraDataPath(), OpenMode.Write);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
using (file)
|
||||
{
|
||||
return file.Write(0, SpanHelpers.AsReadOnlyByteSpan(in extraData), WriteOption.Flush);
|
||||
}
|
||||
}
|
||||
|
||||
private Result CommitExtraDataImpl()
|
||||
{
|
||||
Assert.SdkRequires(_mutex.IsLockedByCurrentThread());
|
||||
|
||||
if (!_useTransactions || !_isPersistentSaveData)
|
||||
return Result.Success;
|
||||
|
||||
Result RenameCommittedFile() => _baseFs.RenameFile(CommittedExtraDataPath, SynchronizingExtraDataPath);
|
||||
Result SynchronizeWorkingFile() => SynchronizeExtraData(SynchronizingExtraDataPath, WorkingExtraDataPath);
|
||||
Result RenameSynchronizingFile() => _baseFs.RenameFile(SynchronizingExtraDataPath, CommittedExtraDataPath);
|
||||
|
||||
// Get rid of the previous commit by renaming the folder
|
||||
Result rc = Utility.RetryFinitelyForTargetLocked(RenameCommittedFile);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
// If something goes wrong beyond this point, the commit will be
|
||||
// completed the next time the savedata is opened
|
||||
|
||||
rc = Utility.RetryFinitelyForTargetLocked(SynchronizeWorkingFile);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
rc = Utility.RetryFinitelyForTargetLocked(RenameSynchronizingFile);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
private Result ReadExtraDataImpl(out SaveDataExtraData extraData)
|
||||
{
|
||||
Assert.SdkRequires(_mutex.IsLockedByCurrentThread());
|
||||
|
||||
UnsafeHelpers.SkipParamInit(out extraData);
|
||||
|
||||
Result rc = _baseFs.OpenFile(out IFile file, GetExtraDataPath(), OpenMode.Read);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
using (file)
|
||||
{
|
||||
rc = file.Read(out long bytesRead, 0, SpanHelpers.AsByteSpan(ref extraData));
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
Assert.SdkEqual(bytesRead, Unsafe.SizeOf<SaveDataExtraData>());
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterCacheObserver(ISaveDataExtraDataAccessorCacheObserver observer, SaveDataSpaceId spaceId,
|
||||
ulong saveDataId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
7
src/LibHac/FsSystem/ISaveDataCommitTimeStampGetter.cs
Normal file
7
src/LibHac/FsSystem/ISaveDataCommitTimeStampGetter.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace LibHac.FsSystem
|
||||
{
|
||||
public interface ISaveDataCommitTimeStampGetter
|
||||
{
|
||||
Result Get(out long timeStamp);
|
||||
}
|
||||
}
|
@ -35,7 +35,8 @@ namespace LibHac
|
||||
{
|
||||
DeviceOperator = defaultObjects.DeviceOperator,
|
||||
ExternalKeySet = keySet.ExternalKeySet,
|
||||
FsCreators = defaultObjects.FsCreators
|
||||
FsCreators = defaultObjects.FsCreators,
|
||||
KeySet = keySet
|
||||
};
|
||||
|
||||
FileSystemServerInitializer.InitializeWithConfig(fsServerClient, fsServer, fsServerConfig);
|
||||
|
@ -1,6 +1,10 @@
|
||||
using LibHac.Common;
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
using LibHac.FsSrv;
|
||||
using LibHac.FsSystem;
|
||||
using LibHac.Tests.Fs.IFileSystemTestBase;
|
||||
using Xunit;
|
||||
@ -38,7 +42,7 @@ namespace LibHac.Tests.Fs
|
||||
}
|
||||
}
|
||||
|
||||
private (IFileSystem baseFs, IFileSystem saveFs) CreateFileSystemInternal()
|
||||
private (IFileSystem baseFs, DirectorySaveDataFileSystem saveFs) CreateFileSystemInternal()
|
||||
{
|
||||
var baseFs = new InMemoryFileSystem();
|
||||
|
||||
@ -170,5 +174,263 @@ namespace LibHac.Tests.Fs
|
||||
Assert.Result(ResultFs.PathNotFound, saveFs.GetEntryType(out _, "/file1".ToU8Span()));
|
||||
Assert.Success(saveFs.GetEntryType(out _, "/file2".ToU8Span()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_InitialExtraDataIsEmpty()
|
||||
{
|
||||
(IFileSystem _, DirectorySaveDataFileSystem saveFs) = CreateFileSystemInternal();
|
||||
|
||||
Assert.Success(saveFs.ReadExtraData(out SaveDataExtraData extraData));
|
||||
Assert.True(SpanHelpers.AsByteSpan(ref extraData).IsZeros());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteExtraData_CanReadBackExtraData()
|
||||
{
|
||||
(IFileSystem _, DirectorySaveDataFileSystem saveFs) = CreateFileSystemInternal();
|
||||
|
||||
var originalExtraData = new SaveDataExtraData();
|
||||
originalExtraData.DataSize = 0x12345;
|
||||
|
||||
Assert.Success(saveFs.WriteExtraData(in originalExtraData));
|
||||
Assert.Success(saveFs.ReadExtraData(out SaveDataExtraData extraData));
|
||||
Assert.Equal(originalExtraData, extraData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Commit_AfterSuccessfulCommit_CanReadCommittedExtraData()
|
||||
{
|
||||
var baseFs = new InMemoryFileSystem();
|
||||
|
||||
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, true, true, true)
|
||||
.ThrowIfFailure();
|
||||
|
||||
var originalExtraData = new SaveDataExtraData();
|
||||
originalExtraData.DataSize = 0x12345;
|
||||
|
||||
saveFs.WriteExtraData(in originalExtraData).ThrowIfFailure();
|
||||
Assert.Success(saveFs.CommitExtraData(false));
|
||||
|
||||
saveFs.Dispose();
|
||||
DirectorySaveDataFileSystem.CreateNew(out saveFs, baseFs, true, true, true).ThrowIfFailure();
|
||||
|
||||
Assert.Success(saveFs.ReadExtraData(out SaveDataExtraData extraData));
|
||||
Assert.Equal(originalExtraData, extraData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rollback_WriteExtraDataThenRollback_ExtraDataIsRolledBack()
|
||||
{
|
||||
var baseFs = new InMemoryFileSystem();
|
||||
|
||||
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, true, true, true)
|
||||
.ThrowIfFailure();
|
||||
|
||||
var originalExtraData = new SaveDataExtraData();
|
||||
originalExtraData.DataSize = 0x12345;
|
||||
|
||||
saveFs.WriteExtraData(in originalExtraData).ThrowIfFailure();
|
||||
saveFs.CommitExtraData(false).ThrowIfFailure();
|
||||
|
||||
saveFs.Dispose();
|
||||
DirectorySaveDataFileSystem.CreateNew(out saveFs, baseFs, true, true, true).ThrowIfFailure();
|
||||
|
||||
var newExtraData = new SaveDataExtraData();
|
||||
newExtraData.DataSize = 0x67890;
|
||||
|
||||
saveFs.WriteExtraData(in newExtraData).ThrowIfFailure();
|
||||
|
||||
Assert.Success(saveFs.Rollback());
|
||||
Assert.Success(saveFs.ReadExtraData(out SaveDataExtraData extraData));
|
||||
|
||||
Assert.Equal(originalExtraData, extraData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rollback_WriteExtraDataThenCloseFs_ExtraDataIsRolledBack()
|
||||
{
|
||||
var baseFs = new InMemoryFileSystem();
|
||||
|
||||
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, true, true, true)
|
||||
.ThrowIfFailure();
|
||||
|
||||
// Write extra data and close with committing
|
||||
var originalExtraData = new SaveDataExtraData();
|
||||
originalExtraData.DataSize = 0x12345;
|
||||
|
||||
saveFs.WriteExtraData(in originalExtraData).ThrowIfFailure();
|
||||
saveFs.CommitExtraData(false).ThrowIfFailure();
|
||||
|
||||
saveFs.Dispose();
|
||||
DirectorySaveDataFileSystem.CreateNew(out saveFs, baseFs, true, true, true).ThrowIfFailure();
|
||||
|
||||
// Write a new extra data and close without committing
|
||||
var newExtraData = new SaveDataExtraData();
|
||||
newExtraData.DataSize = 0x67890;
|
||||
|
||||
saveFs.WriteExtraData(in newExtraData).ThrowIfFailure();
|
||||
saveFs.Dispose();
|
||||
|
||||
// Read extra data should match the first one
|
||||
DirectorySaveDataFileSystem.CreateNew(out saveFs, baseFs, true, true, true).ThrowIfFailure();
|
||||
Assert.Success(saveFs.ReadExtraData(out SaveDataExtraData extraData));
|
||||
|
||||
Assert.Equal(originalExtraData, extraData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_InterruptedAfterCommitPart1_UsesWorkingExtraData()
|
||||
{
|
||||
var baseFs = new InMemoryFileSystem();
|
||||
|
||||
CreateExtraDataForTest(baseFs, "/ExtraData_".ToU8Span(), 0x12345).ThrowIfFailure();
|
||||
CreateExtraDataForTest(baseFs, "/ExtraData1".ToU8Span(), 0x67890).ThrowIfFailure();
|
||||
|
||||
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, true, true, true)
|
||||
.ThrowIfFailure();
|
||||
|
||||
saveFs.ReadExtraData(out SaveDataExtraData extraData).ThrowIfFailure();
|
||||
|
||||
Assert.Equal(0x67890, extraData.DataSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CommitSaveData_MultipleCommits_CommitIdIsUpdatedSkippingInvalidIds()
|
||||
{
|
||||
var random = new RandomGenerator();
|
||||
RandomDataGenerator randomGeneratorFunc = buffer => random.GenerateRandom(buffer);
|
||||
var timeStampGetter = new TimeStampGetter();
|
||||
|
||||
var baseFs = new InMemoryFileSystem();
|
||||
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, timeStampGetter,
|
||||
randomGeneratorFunc, true, true, true, null).ThrowIfFailure();
|
||||
|
||||
saveFs.CommitExtraData(true).ThrowIfFailure();
|
||||
saveFs.ReadExtraData(out SaveDataExtraData extraData).ThrowIfFailure();
|
||||
Assert.Equal(2, extraData.CommitId);
|
||||
|
||||
saveFs.CommitExtraData(true).ThrowIfFailure();
|
||||
saveFs.ReadExtraData(out extraData).ThrowIfFailure();
|
||||
Assert.Equal(3, extraData.CommitId);
|
||||
|
||||
saveFs.CommitExtraData(true).ThrowIfFailure();
|
||||
saveFs.ReadExtraData(out extraData).ThrowIfFailure();
|
||||
Assert.Equal(6, extraData.CommitId);
|
||||
|
||||
saveFs.CommitExtraData(true).ThrowIfFailure();
|
||||
saveFs.ReadExtraData(out extraData).ThrowIfFailure();
|
||||
Assert.Equal(2, extraData.CommitId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CommitSaveData_MultipleCommits_TimeStampUpdated()
|
||||
{
|
||||
var random = new RandomGenerator();
|
||||
RandomDataGenerator randomGeneratorFunc = buffer => random.GenerateRandom(buffer);
|
||||
var timeStampGetter = new TimeStampGetter();
|
||||
|
||||
var baseFs = new InMemoryFileSystem();
|
||||
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, timeStampGetter,
|
||||
randomGeneratorFunc, true, true, true, null).ThrowIfFailure();
|
||||
|
||||
saveFs.CommitExtraData(true).ThrowIfFailure();
|
||||
saveFs.ReadExtraData(out SaveDataExtraData extraData).ThrowIfFailure();
|
||||
Assert.Equal(1u, extraData.TimeStamp);
|
||||
|
||||
saveFs.CommitExtraData(true).ThrowIfFailure();
|
||||
saveFs.ReadExtraData(out extraData).ThrowIfFailure();
|
||||
Assert.Equal(2u, extraData.TimeStamp);
|
||||
|
||||
saveFs.CommitExtraData(true).ThrowIfFailure();
|
||||
saveFs.ReadExtraData(out extraData).ThrowIfFailure();
|
||||
Assert.Equal(3u, extraData.TimeStamp);
|
||||
|
||||
saveFs.CommitExtraData(true).ThrowIfFailure();
|
||||
saveFs.ReadExtraData(out extraData).ThrowIfFailure();
|
||||
Assert.Equal(4u, extraData.TimeStamp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CommitSaveData_UpdateTimeStampIsFalse_TimeStampAndCommitIdAreNotUpdated()
|
||||
{
|
||||
var random = new RandomGenerator();
|
||||
RandomDataGenerator randomGeneratorFunc = buffer => random.GenerateRandom(buffer);
|
||||
var timeStampGetter = new TimeStampGetter();
|
||||
|
||||
var baseFs = new InMemoryFileSystem();
|
||||
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, timeStampGetter,
|
||||
randomGeneratorFunc, true, true, true, null).ThrowIfFailure();
|
||||
|
||||
saveFs.CommitExtraData(true).ThrowIfFailure();
|
||||
saveFs.ReadExtraData(out SaveDataExtraData extraData).ThrowIfFailure();
|
||||
Assert.Equal(1u, extraData.TimeStamp);
|
||||
Assert.Equal(2, extraData.CommitId);
|
||||
|
||||
saveFs.CommitExtraData(false).ThrowIfFailure();
|
||||
saveFs.ReadExtraData(out extraData).ThrowIfFailure();
|
||||
Assert.Equal(1u, extraData.TimeStamp);
|
||||
Assert.Equal(2, extraData.CommitId);
|
||||
|
||||
saveFs.CommitExtraData(true).ThrowIfFailure();
|
||||
saveFs.ReadExtraData(out extraData).ThrowIfFailure();
|
||||
Assert.Equal(2u, extraData.TimeStamp);
|
||||
Assert.Equal(3, extraData.CommitId);
|
||||
|
||||
saveFs.CommitExtraData(false).ThrowIfFailure();
|
||||
saveFs.ReadExtraData(out extraData).ThrowIfFailure();
|
||||
Assert.Equal(2u, extraData.TimeStamp);
|
||||
Assert.Equal(3, extraData.CommitId);
|
||||
}
|
||||
|
||||
private class TimeStampGetter : ISaveDataCommitTimeStampGetter
|
||||
{
|
||||
private long _currentTimeStamp = 1;
|
||||
|
||||
public Result Get(out long timeStamp)
|
||||
{
|
||||
timeStamp = _currentTimeStamp++;
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
|
||||
private class RandomGenerator
|
||||
{
|
||||
private static readonly int[] Values = { 2, 0, 3, 3, 6, 0 };
|
||||
|
||||
private int _index;
|
||||
|
||||
public Result GenerateRandom(Span<byte> output)
|
||||
{
|
||||
if (output.Length != 8)
|
||||
throw new ArgumentException();
|
||||
|
||||
Unsafe.As<byte, long>(ref MemoryMarshal.GetReference(output)) = Values[_index];
|
||||
|
||||
_index = (_index + 1) % Values.Length;
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
|
||||
private Result CreateExtraDataForTest(IFileSystem fileSystem, U8Span path, int saveDataSize)
|
||||
{
|
||||
fileSystem.DeleteFile(path).IgnoreResult();
|
||||
|
||||
Result rc = fileSystem.CreateFile(path, Unsafe.SizeOf<SaveDataExtraData>());
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
var extraData = new SaveDataExtraData();
|
||||
extraData.DataSize = saveDataSize;
|
||||
|
||||
rc = fileSystem.OpenFile(out IFile file, path, OpenMode.ReadWrite);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
using (file)
|
||||
{
|
||||
rc = file.Write(0, SpanHelpers.AsByteSpan(ref extraData), WriteOption.Flush);
|
||||
if (rc.IsFailure()) return rc;
|
||||
}
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -10,8 +10,9 @@ namespace LibHac.Tests.Fs.FileSystemClientTests
|
||||
private static FileSystemClient CreateClientImpl(bool sdCardInserted, out IFileSystem rootFs)
|
||||
{
|
||||
rootFs = new InMemoryFileSystem();
|
||||
|
||||
var defaultObjects = DefaultFsServerObjects.GetDefaultEmulatedCreators(rootFs, new KeySet());
|
||||
var keySet = new KeySet();
|
||||
|
||||
var defaultObjects = DefaultFsServerObjects.GetDefaultEmulatedCreators(rootFs, keySet);
|
||||
|
||||
defaultObjects.SdCard.SetSdCardInsertionStatus(sdCardInserted);
|
||||
|
||||
@ -19,6 +20,7 @@ namespace LibHac.Tests.Fs.FileSystemClientTests
|
||||
config.FsCreators = defaultObjects.FsCreators;
|
||||
config.DeviceOperator = defaultObjects.DeviceOperator;
|
||||
config.ExternalKeySet = new ExternalKeySet();
|
||||
config.KeySet = keySet;
|
||||
|
||||
Horizon horizon = LibHac.HorizonFactory.CreateWithFsConfig(new HorizonConfiguration(), config);
|
||||
|
||||
|
@ -10,13 +10,15 @@ namespace LibHac.Tests
|
||||
public static Horizon CreateBasicHorizon()
|
||||
{
|
||||
IFileSystem rootFs = new InMemoryFileSystem();
|
||||
var keySet = new KeySet();
|
||||
|
||||
var defaultObjects = DefaultFsServerObjects.GetDefaultEmulatedCreators(rootFs, new KeySet());
|
||||
var defaultObjects = DefaultFsServerObjects.GetDefaultEmulatedCreators(rootFs, keySet);
|
||||
|
||||
var config = new FileSystemServerConfig();
|
||||
config.FsCreators = defaultObjects.FsCreators;
|
||||
config.DeviceOperator = defaultObjects.DeviceOperator;
|
||||
config.ExternalKeySet = new ExternalKeySet();
|
||||
config.KeySet = keySet;
|
||||
|
||||
Horizon horizon = LibHac.HorizonFactory.CreateWithFsConfig(new HorizonConfiguration(), config);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user