From 649c72e5e65c430cc818f7f25ccc6ab83d71d52e Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Wed, 28 Apr 2021 18:24:59 -0700 Subject: [PATCH] Add extra data handling to DirectorySaveDataFileSystem --- src/LibHac/Common/ITimeStampGenerator.cs | 9 - src/LibHac/FsSrv/DefaultFsServerObjects.cs | 2 +- .../FsSrv/FileSystemServerInitializer.cs | 10 + .../FsCreator/ISaveDataFileSystemCreator.cs | 3 +- .../FsCreator/SaveDataFileSystemCreator.cs | 18 +- .../FsSrv/SaveDataFileSystemServiceImpl.cs | 26 +- .../FsSystem/DirectorySaveDataFileSystem.cs | 321 +++++++++++++++++- .../ISaveDataCommitTimeStampGetter.cs | 7 + src/LibHac/HorizonFactory.cs | 3 +- .../Fs/DirectorySaveDataFileSystemTests.cs | 268 ++++++++++++++- .../FileSystemServerFactory.cs | 6 +- tests/LibHac.Tests/HorizonFactory.cs | 4 +- 12 files changed, 636 insertions(+), 41 deletions(-) delete mode 100644 src/LibHac/Common/ITimeStampGenerator.cs create mode 100644 src/LibHac/FsSystem/ISaveDataCommitTimeStampGetter.cs diff --git a/src/LibHac/Common/ITimeStampGenerator.cs b/src/LibHac/Common/ITimeStampGenerator.cs deleted file mode 100644 index 3d0c528b..00000000 --- a/src/LibHac/Common/ITimeStampGenerator.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace LibHac.Common -{ - public interface ITimeStampGenerator - { - DateTimeOffset Generate(); - } -} \ No newline at end of file diff --git a/src/LibHac/FsSrv/DefaultFsServerObjects.cs b/src/LibHac/FsSrv/DefaultFsServerObjects.cs index 7d35453f..49c11a38 100644 --- a/src/LibHac/FsSrv/DefaultFsServerObjects.cs +++ b/src/LibHac/FsSrv/DefaultFsServerObjects.cs @@ -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); diff --git a/src/LibHac/FsSrv/FileSystemServerInitializer.cs b/src/LibHac/FsSrv/FileSystemServerInitializer.cs index ee0efb0f..8b348f17 100644 --- a/src/LibHac/FsSrv/FileSystemServerInitializer.cs +++ b/src/LibHac/FsSrv/FileSystemServerInitializer.cs @@ -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 heapBuffer = GC.AllocateArray(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. /// public ExternalKeySet ExternalKeySet { get; set; } + + /// + /// A keyset used for decrypting content. + /// + public KeySet KeySet { get; set; } } } diff --git a/src/LibHac/FsSrv/FsCreator/ISaveDataFileSystemCreator.cs b/src/LibHac/FsSrv/FsCreator/ISaveDataFileSystemCreator.cs index 0f82429b..25b94cec 100644 --- a/src/LibHac/FsSrv/FsCreator/ISaveDataFileSystemCreator.cs +++ b/src/LibHac/FsSrv/FsCreator/ISaveDataFileSystemCreator.cs @@ -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 extraDataAccessor, IFileSystem sourceFileSystem, ulong saveDataId, bool allowDirectorySaveData, bool useDeviceUniqueMac, SaveDataType type, - ITimeStampGenerator timeStampGenerator); + ISaveDataCommitTimeStampGetter timeStampGetter); void SetSdCardEncryptionSeed(ReadOnlySpan seed); } diff --git a/src/LibHac/FsSrv/FsCreator/SaveDataFileSystemCreator.cs b/src/LibHac/FsSrv/FsCreator/SaveDataFileSystemCreator.cs index 51a176d6..c5a03bfd 100644 --- a/src/LibHac/FsSrv/FsCreator/SaveDataFileSystemCreator.cs +++ b/src/LibHac/FsSrv/FsCreator/SaveDataFileSystemCreator.cs @@ -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 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(); } } -} +} \ No newline at end of file diff --git a/src/LibHac/FsSrv/SaveDataFileSystemServiceImpl.cs b/src/LibHac/FsSrv/SaveDataFileSystemServiceImpl.cs index 9b960375..e3047a57 100644 --- a/src/LibHac/FsSrv/SaveDataFileSystemServiceImpl.cs +++ b/src/LibHac/FsSrv/SaveDataFileSystemServiceImpl.cs @@ -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(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(); diff --git a/src/LibHac/FsSystem/DirectorySaveDataFileSystem.cs b/src/LibHac/FsSystem/DirectorySaveDataFileSystem.cs index e0aea600..400f967a 100644 --- a/src/LibHac/FsSystem/DirectorySaveDataFileSystem.cs +++ b/src/LibHac/FsSystem/DirectorySaveDataFileSystem.cs @@ -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 is atomic. ///
Based on FS 11.0.0 (nnSdk 11.4.0) /// - 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 outBuffer, OperationId operationId, long offset, long size, ReadOnlySpan inBuffer) + protected override Result DoOperateRange(Span outBuffer, OperationId operationId, long offset, + long size, ReadOnlySpan 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); + } + /// /// Create an uninitialized . /// @@ -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 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 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 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 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 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()); + if (rc.IsFailure()) return rc; + + if (_isPersistentSaveData) + { + rc = _baseFs.CreateFile(CommittedExtraDataPath, Unsafe.SizeOf()); + 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()) + return Result.Success; + + return file.SetSize(Unsafe.SizeOf()); + } + finally + { + file?.Dispose(); + } + } + + private Result SynchronizeExtraData(U8Span destPath, U8Span sourcePath) + { + Span workBuffer = stackalloc byte[Unsafe.SizeOf()]; + + 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()); + } + + 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 lk = ScopedLock.Lock(ref _mutex); + + return WriteExtraDataImpl(in extraData); + } + + public Result CommitExtraData(bool updateTimeStamp) + { + using ScopedLock 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 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()); + + return Result.Success; + } + } + + public void RegisterCacheObserver(ISaveDataExtraDataAccessorCacheObserver observer, SaveDataSpaceId spaceId, + ulong saveDataId) + { + throw new NotImplementedException(); + } } -} +} \ No newline at end of file diff --git a/src/LibHac/FsSystem/ISaveDataCommitTimeStampGetter.cs b/src/LibHac/FsSystem/ISaveDataCommitTimeStampGetter.cs new file mode 100644 index 00000000..0738177c --- /dev/null +++ b/src/LibHac/FsSystem/ISaveDataCommitTimeStampGetter.cs @@ -0,0 +1,7 @@ +namespace LibHac.FsSystem +{ + public interface ISaveDataCommitTimeStampGetter + { + Result Get(out long timeStamp); + } +} \ No newline at end of file diff --git a/src/LibHac/HorizonFactory.cs b/src/LibHac/HorizonFactory.cs index f7a5f3bb..35003670 100644 --- a/src/LibHac/HorizonFactory.cs +++ b/src/LibHac/HorizonFactory.cs @@ -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); diff --git a/tests/LibHac.Tests/Fs/DirectorySaveDataFileSystemTests.cs b/tests/LibHac.Tests/Fs/DirectorySaveDataFileSystemTests.cs index 38bb0f28..ec460090 100644 --- a/tests/LibHac.Tests/Fs/DirectorySaveDataFileSystemTests.cs +++ b/tests/LibHac.Tests/Fs/DirectorySaveDataFileSystemTests.cs @@ -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 output) + { + if (output.Length != 8) + throw new ArgumentException(); + + Unsafe.As(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()); + 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; + } } -} +} \ No newline at end of file diff --git a/tests/LibHac.Tests/Fs/FileSystemClientTests/FileSystemServerFactory.cs b/tests/LibHac.Tests/Fs/FileSystemClientTests/FileSystemServerFactory.cs index 0042d795..5056bbb7 100644 --- a/tests/LibHac.Tests/Fs/FileSystemClientTests/FileSystemServerFactory.cs +++ b/tests/LibHac.Tests/Fs/FileSystemClientTests/FileSystemServerFactory.cs @@ -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); diff --git a/tests/LibHac.Tests/HorizonFactory.cs b/tests/LibHac.Tests/HorizonFactory.cs index a0ccc3ab..5562db7b 100644 --- a/tests/LibHac.Tests/HorizonFactory.cs +++ b/tests/LibHac.Tests/HorizonFactory.cs @@ -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);