Add extra data handling to DirectorySaveDataFileSystem

This commit is contained in:
Alex Barney 2021-04-28 18:24:59 -07:00
parent 3bf8826a5b
commit 649c72e5e6
12 changed files with 636 additions and 41 deletions

View File

@ -1,9 +0,0 @@
using System;
namespace LibHac.Common
{
public interface ITimeStampGenerator
{
DateTimeOffset Generate();
}
}

View File

@ -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);

View File

@ -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; }
}
}

View File

@ -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);
}

View File

@ -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();
}
}
}
}

View File

@ -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();

View File

@ -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();
}
}
}
}

View File

@ -0,0 +1,7 @@
namespace LibHac.FsSystem
{
public interface ISaveDataCommitTimeStampGetter
{
Result Get(out long timeStamp);
}
}

View File

@ -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);

View File

@ -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;
}
}
}
}

View File

@ -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);

View File

@ -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);