From d1a49b989af18f78d4f13007ad8c5c645e724dd6 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Sun, 16 May 2021 18:16:36 -0700 Subject: [PATCH] Add SaveDataSharedFileStorage --- src/LibHac/Fs/FileStorageBasedFileSystem.cs | 5 +- src/LibHac/FsSrv/FileSystemServer.cs | 7 +- src/LibHac/FsSrv/NcaFileSystemServiceImpl.cs | 4 +- src/LibHac/FsSrv/SaveDataSharedFileStorage.cs | 451 ++++++++++++++++++ src/LibHac/Os/UniqueLock.cs | 86 ++++ 5 files changed, 546 insertions(+), 7 deletions(-) create mode 100644 src/LibHac/FsSrv/SaveDataSharedFileStorage.cs create mode 100644 src/LibHac/Os/UniqueLock.cs diff --git a/src/LibHac/Fs/FileStorageBasedFileSystem.cs b/src/LibHac/Fs/FileStorageBasedFileSystem.cs index 35d9efd3..5ae1534c 100644 --- a/src/LibHac/Fs/FileStorageBasedFileSystem.cs +++ b/src/LibHac/Fs/FileStorageBasedFileSystem.cs @@ -5,7 +5,6 @@ namespace LibHac.Fs { public class FileStorageBasedFileSystem : FileStorage2 { - // ReSharper disable once UnusedAutoPropertyAccessor.Local private ReferenceCountedDisposable BaseFileSystem { get; set; } private IFile BaseFile { get; set; } @@ -14,14 +13,14 @@ namespace LibHac.Fs FileSize = SizeNotInitialized; } - public Result Initialize(ReferenceCountedDisposable baseFileSystem, U8Span path, OpenMode mode) + public Result Initialize(ref ReferenceCountedDisposable baseFileSystem, U8Span path, OpenMode mode) { Result rc = baseFileSystem.Target.OpenFile(out IFile file, path, mode); if (rc.IsFailure()) return rc; SetFile(file); BaseFile = file; - BaseFileSystem = baseFileSystem; + BaseFileSystem = Shared.Move(ref baseFileSystem); return Result.Success; } diff --git a/src/LibHac/FsSrv/FileSystemServer.cs b/src/LibHac/FsSrv/FileSystemServer.cs index d44ca125..8a6ed7cb 100644 --- a/src/LibHac/FsSrv/FileSystemServer.cs +++ b/src/LibHac/FsSrv/FileSystemServer.cs @@ -17,7 +17,7 @@ namespace LibHac.FsSrv /// The that will be used by this server. public FileSystemServer(HorizonClient horizonClient) { - Globals.Initialize(horizonClient); + Globals.Initialize(horizonClient, this); } } @@ -30,11 +30,14 @@ namespace LibHac.FsSrv public DeviceEventSimulatorGlobals DeviceEventSimulator; public AccessControlGlobals AccessControl; public StorageDeviceManagerFactoryGlobals StorageDeviceManagerFactory; + public SaveDataSharedFileStorageGlobals SaveDataSharedFileStorage; - public void Initialize(HorizonClient horizonClient) + public void Initialize(HorizonClient horizonClient, FileSystemServer fsServer) { Hos = horizonClient; InitMutex = new object(); + + SaveDataSharedFileStorage.Initialize(fsServer); } } diff --git a/src/LibHac/FsSrv/NcaFileSystemServiceImpl.cs b/src/LibHac/FsSrv/NcaFileSystemServiceImpl.cs index a08c4963..fd271b9e 100644 --- a/src/LibHac/FsSrv/NcaFileSystemServiceImpl.cs +++ b/src/LibHac/FsSrv/NcaFileSystemServiceImpl.cs @@ -635,7 +635,7 @@ namespace LibHac.FsSrv var storage = new FileStorageBasedFileSystem(); using var nspFileStorage = new ReferenceCountedDisposable(storage); - rc = nspFileStorage.Target.Initialize(baseFileSystem, new U8Span(nspPath.Str), OpenMode.Read); + rc = nspFileStorage.Target.Initialize(ref baseFileSystem, new U8Span(nspPath.Str), OpenMode.Read); if (rc.IsFailure()) return rc; rc = _config.PartitionFsCreator.Create(out fileSystem, nspFileStorage.AddReference()); @@ -656,7 +656,7 @@ namespace LibHac.FsSrv // Todo: Create ref-counted storage var ncaFileStorage = new FileStorageBasedFileSystem(); - Result rc = ncaFileStorage.Initialize(baseFileSystem, path, OpenMode.Read); + Result rc = ncaFileStorage.Initialize(ref baseFileSystem, path, OpenMode.Read); if (rc.IsFailure()) return rc; rc = _config.StorageOnNcaCreator.OpenNca(out Nca ncaTemp, ncaFileStorage); diff --git a/src/LibHac/FsSrv/SaveDataSharedFileStorage.cs b/src/LibHac/FsSrv/SaveDataSharedFileStorage.cs new file mode 100644 index 00000000..76358950 --- /dev/null +++ b/src/LibHac/FsSrv/SaveDataSharedFileStorage.cs @@ -0,0 +1,451 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using LibHac.Common; +using LibHac.Diag; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.Os; +using LibHac.Util; + +namespace LibHac.FsSrv +{ + public static class SaveDataSharedFileStorageGlobalMethods + { + public static Result OpenSaveDataStorage(this FileSystemServer fsSrv, + out ReferenceCountedDisposable saveDataStorage, + ref ReferenceCountedDisposable baseFileSystem, SaveDataSpaceId spaceId, ulong saveDataId, + OpenMode mode, Optional type) + { + return fsSrv.Globals.SaveDataSharedFileStorage.SaveDataFileStorageHolder.OpenSaveDataStorage( + out saveDataStorage, ref baseFileSystem, spaceId, saveDataId, mode, type); + } + } + + internal struct SaveDataSharedFileStorageGlobals + { + public SdkMutexType Mutex; + public SaveDataFileStorageHolder SaveDataFileStorageHolder; + + public void Initialize(FileSystemServer fsServer) + { + Mutex.Initialize(); + SaveDataFileStorageHolder = new SaveDataFileStorageHolder(fsServer); + } + } + + /// + /// Provides access to a save data file from the provided + /// via an interface. + /// This class keeps track of which types of save data file systems have been opened from the save data file. + /// Only one of each file system type can be opened at the same time. + /// + public class SaveDataOpenTypeSetFileStorage : FileStorageBasedFileSystem + { + public enum OpenType + { + None, + Normal, + Internal + } + + private bool _isNormalStorageOpened; + private bool _isInternalStorageOpened; + private bool _isInternalStorageInvalidated; + private SaveDataSpaceId _spaceId; + private ulong _saveDataId; + private SdkMutexType _mutex; + + // LibHac addition + private FileSystemServer _fsServer; + private ref SaveDataSharedFileStorageGlobals Globals => ref _fsServer.Globals.SaveDataSharedFileStorage; + + public SaveDataOpenTypeSetFileStorage(FileSystemServer fsServer, SaveDataSpaceId spaceId, ulong saveDataId) + { + _fsServer = fsServer; + _spaceId = spaceId; + _saveDataId = saveDataId; + _mutex.Initialize(); + } + + public Result Initialize(ref ReferenceCountedDisposable baseFileSystem, U8Span path, OpenMode mode, + OpenType type) + { + Result rc = Initialize(ref baseFileSystem, path, mode); + if (rc.IsFailure()) return rc; + + return SetOpenType(type); + } + + public Result SetOpenType(OpenType type) + { + Assert.SdkRequires(type == OpenType.Normal || type == OpenType.Internal); + + switch (type) + { + case OpenType.Normal: + if (_isNormalStorageOpened) + return ResultFs.TargetLocked.Log(); + + _isNormalStorageOpened = true; + return Result.Success; + + case OpenType.Internal: + if (_isInternalStorageOpened) + return ResultFs.TargetLocked.Log(); + + _isInternalStorageOpened = true; + _isInternalStorageInvalidated = false; + return Result.Success; + + default: + Abort.UnexpectedDefault(); + return Result.Success; + } + } + + public void UnsetOpenType(OpenType type) + { + using ScopedLock scopedLock = + ScopedLock.Lock(ref Globals.Mutex); + + if (type == OpenType.Normal) + { + _isNormalStorageOpened = false; + } + else if (type == OpenType.Internal) + { + _isInternalStorageOpened = false; + } + + if (!IsOpened()) + { + Globals.SaveDataFileStorageHolder.Unregister(_spaceId, _saveDataId); + } + } + + public void InvalidateInternalStorage() + { + _isInternalStorageInvalidated = true; + } + + public bool IsInternalStorageInvalidated() + { + return _isInternalStorageInvalidated; + } + + public bool IsOpened() + { + return _isNormalStorageOpened || _isInternalStorageOpened; + } + + public UniqueLock GetLock() + { + return new UniqueLock(ref _mutex); + } + } + + /// + /// Handles sharing a save data file storage between an internal save data file system + /// and a normal save data file system. + /// + /// + /// During save data import/export a save data image is opened as an "internal file system". + /// This file system allows access to portions of a save data image via an emulated file system + /// with different portions being represented as individual files. This class allows simultaneous + /// read-only access to a save data image via a normal save data file system and an internal file system. + /// Once an internal file system is opened, it will be considered valid until the save data image is + /// written to via the normal file system, at which point any accesses via the internal file system will + /// return . + /// + public class SaveDataSharedFileStorage : IStorage + { + private ReferenceCountedDisposable _baseStorage; + private SaveDataOpenTypeSetFileStorage.OpenType _type; + + public SaveDataSharedFileStorage(ref ReferenceCountedDisposable baseStorage, + SaveDataOpenTypeSetFileStorage.OpenType type) + { + _baseStorage = Shared.Move(ref baseStorage); + _type = type; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _baseStorage?.Target.UnsetOpenType(_type); + _baseStorage?.Dispose(); + } + + base.Dispose(disposing); + } + + private Result AccessCheck(bool isWriteAccess) + { + if (_type == SaveDataOpenTypeSetFileStorage.OpenType.Internal) + { + if (_baseStorage.Target.IsInternalStorageInvalidated()) + return ResultFs.SaveDataPorterInvalidated.Log(); + } + else if (_type == SaveDataOpenTypeSetFileStorage.OpenType.Normal && isWriteAccess) + { + // Any opened internal file system will be invalid after a write to the normal file system + _baseStorage.Target.InvalidateInternalStorage(); + } + + return Result.Success; + } + + protected override Result DoRead(long offset, Span destination) + { + using UniqueLock scopedLock = _baseStorage.Target.GetLock(); + + Result rc = AccessCheck(isWriteAccess: false); + if (rc.IsFailure()) return rc; + + return _baseStorage.Target.Read(offset, destination); + } + + protected override Result DoWrite(long offset, ReadOnlySpan source) + { + using UniqueLock scopedLock = _baseStorage.Target.GetLock(); + + Result rc = AccessCheck(isWriteAccess: true); + if (rc.IsFailure()) return rc; + + return _baseStorage.Target.Write(offset, source); + } + + protected override Result DoFlush() + { + using UniqueLock scopedLock = _baseStorage.Target.GetLock(); + + Result rc = AccessCheck(isWriteAccess: true); + if (rc.IsFailure()) return rc; + + return _baseStorage.Target.Flush(); + } + + protected override Result DoSetSize(long size) + { + using UniqueLock scopedLock = _baseStorage.Target.GetLock(); + + Result rc = AccessCheck(isWriteAccess: true); + if (rc.IsFailure()) return rc; + + return _baseStorage.Target.SetSize(size); + } + + protected override Result DoGetSize(out long size) + { + Unsafe.SkipInit(out size); + + using UniqueLock scopedLock = _baseStorage.Target.GetLock(); + + Result rc = AccessCheck(isWriteAccess: false); + if (rc.IsFailure()) return rc; + + return _baseStorage.Target.GetSize(out size); + } + + protected override Result DoOperateRange(Span outBuffer, OperationId operationId, long offset, long size, + ReadOnlySpan inBuffer) + { + using UniqueLock scopedLock = _baseStorage.Target.GetLock(); + + Result rc = AccessCheck(isWriteAccess: true); + if (rc.IsFailure()) return rc; + + return _baseStorage.Target.OperateRange(outBuffer, operationId, offset, size, inBuffer); + } + } + + /// + /// Holds references to any open shared save data image files. + /// + public class SaveDataFileStorageHolder + { + private struct Entry + { + private ReferenceCountedDisposable _storage; + private SaveDataSpaceId _spaceId; + private ulong _saveDataId; + + public Entry(ref ReferenceCountedDisposable storage, + SaveDataSpaceId spaceId, ulong saveDataId) + { + _storage = Shared.Move(ref storage); + _spaceId = spaceId; + _saveDataId = saveDataId; + } + + public void Dispose() + { + _storage?.Dispose(); + } + + public bool Contains(SaveDataSpaceId spaceId, ulong saveDataId) + { + return _spaceId == spaceId && _saveDataId == saveDataId; + } + + public ReferenceCountedDisposable GetStorage() + { + return _storage.AddReference(); + } + } + + private LinkedList _entryList; + + // LibHac additions + private FileSystemServer _fsServer; + private ref SaveDataSharedFileStorageGlobals Globals => ref _fsServer.Globals.SaveDataSharedFileStorage; + + public SaveDataFileStorageHolder(FileSystemServer fsServer) + { + _fsServer = fsServer; + _entryList = new LinkedList(); + } + + public void Dispose() + { + using ScopedLock scopedLock = ScopedLock.Lock(ref Globals.Mutex); + + LinkedListNode currentEntry = _entryList.First; + + while (currentEntry is not null) + { + ref Entry entry = ref currentEntry.ValueRef; + _entryList.Remove(currentEntry); + entry.Dispose(); + + currentEntry = _entryList.First; + } + } + + public Result OpenSaveDataStorage(out ReferenceCountedDisposable saveDataStorage, + ref ReferenceCountedDisposable baseFileSystem, SaveDataSpaceId spaceId, ulong saveDataId, + OpenMode mode, Optional type) + { + Result rc; + UnsafeHelpers.SkipParamInit(out saveDataStorage); + + Span saveImageName = stackalloc byte[0x30]; + var sb = new U8StringBuilder(saveImageName); + sb.Append((byte)'/').AppendFormat(saveDataId, 'x', 16); + + // If an open type isn't specified, open the save without the shared file storage layer + if (!type.HasValue) + { + ReferenceCountedDisposable fileStorage = null; + try + { + fileStorage = + new ReferenceCountedDisposable(new FileStorageBasedFileSystem()); + + rc = fileStorage.Target.Initialize(ref baseFileSystem, new U8Span(saveImageName), mode); + if (rc.IsFailure()) return rc; + + saveDataStorage = fileStorage.AddReference(); + return Result.Success; + } + finally + { + fileStorage?.Dispose(); + } + } + + using ScopedLock scopedLock = ScopedLock.Lock(ref Globals.Mutex); + + ReferenceCountedDisposable baseFileStorage = null; + ReferenceCountedDisposable tempBaseFileStorage = null; + try + { + baseFileStorage = GetStorage(spaceId, saveDataId); + + if (baseFileStorage is not null) + { + rc = baseFileStorage.Target.SetOpenType(type.ValueRo); + if (rc.IsFailure()) return rc; + } + else + { + baseFileStorage = + new ReferenceCountedDisposable( + new SaveDataOpenTypeSetFileStorage(_fsServer, spaceId, saveDataId)); + + rc = baseFileStorage.Target.Initialize(ref baseFileSystem, new U8Span(saveImageName), mode, + type.ValueRo); + if (rc.IsFailure()) return rc; + + tempBaseFileStorage = baseFileStorage.AddReference(); + rc = Register(ref tempBaseFileStorage, spaceId, saveDataId); + if (rc.IsFailure()) return rc; + } + + saveDataStorage = + new ReferenceCountedDisposable( + new SaveDataSharedFileStorage(ref baseFileStorage, type.ValueRo)); + } + finally + { + baseFileStorage?.Dispose(); + tempBaseFileStorage?.Dispose(); + } + + return Result.Success; + } + + public Result Register(ref ReferenceCountedDisposable storage, + SaveDataSpaceId spaceId, ulong saveDataId) + { + Assert.SdkRequires(Globals.Mutex.IsLockedByCurrentThread()); + + var entry = new Entry(ref storage, spaceId, saveDataId); + _entryList.AddLast(entry); + + return Result.Success; + } + + public ReferenceCountedDisposable GetStorage(SaveDataSpaceId spaceId, + ulong saveDataId) + { + Assert.SdkRequires(Globals.Mutex.IsLockedByCurrentThread()); + + LinkedListNode currentEntry = _entryList.First; + + while (currentEntry is not null) + { + if (currentEntry.ValueRef.Contains(spaceId, saveDataId)) + { + return currentEntry.ValueRef.GetStorage(); + } + + currentEntry = currentEntry.Next; + } + + return null; + } + + public void Unregister(SaveDataSpaceId spaceId, ulong saveDataId) + { + Assert.SdkRequires(Globals.Mutex.IsLockedByCurrentThread()); + + LinkedListNode currentEntry = _entryList.First; + + while (currentEntry is not null) + { + if (currentEntry.ValueRef.Contains(spaceId, saveDataId)) + { + ref Entry entry = ref currentEntry.ValueRef; + _entryList.Remove(currentEntry); + entry.Dispose(); + + } + + currentEntry = currentEntry.Next; + } + } + } +} diff --git a/src/LibHac/Os/UniqueLock.cs b/src/LibHac/Os/UniqueLock.cs new file mode 100644 index 00000000..47ce3352 --- /dev/null +++ b/src/LibHac/Os/UniqueLock.cs @@ -0,0 +1,86 @@ +using System.Runtime.CompilerServices; +using System.Threading; +using LibHac.Common; + +namespace LibHac.Os +{ + public static class UniqueLock + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static UniqueLock Lock(ref TMutex lockable) where TMutex : ILockable + { + return new UniqueLock(ref lockable); + } + } + + public ref struct UniqueLock where TMutex : ILockable + { + private Ref _mutex; + private bool _ownsLock; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public UniqueLock(ref TMutex mutex) + { + _mutex = new Ref(ref mutex); + mutex.Lock(); + _ownsLock = true; + } + + public UniqueLock(ref UniqueLock other) + { + this = other; + other = default; + } + + public void Set(ref UniqueLock other) + { + if (_ownsLock) + _mutex.Value.Unlock(); + + this = other; + other = default; + } + + public void Lock() + { + if (_mutex.IsNull) + throw new SynchronizationLockException("UniqueLock.Lock: References null mutex"); + + if (_ownsLock) + throw new SynchronizationLockException("UniqueLock.Lock: Already locked"); + + _mutex.Value.Lock(); + _ownsLock = true; + } + + public bool TryLock() + { + if (_mutex.IsNull) + throw new SynchronizationLockException("UniqueLock.TryLock: References null mutex"); + + if (_ownsLock) + throw new SynchronizationLockException("UniqueLock.TryLock: Already locked"); + + _ownsLock = _mutex.Value.TryLock(); + return _ownsLock; + } + + public void Unlock() + { + if (_ownsLock) + throw new SynchronizationLockException("UniqueLock.Unlock: Not locked"); + + _mutex.Value.Unlock(); + _ownsLock = false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() + { + if (_ownsLock) + _mutex.Value.Unlock(); + + this = default; + } + } +} \ No newline at end of file