diff --git a/src/LibHac/Fs/Common/DirectoryPathParser.cs b/src/LibHac/Fs/Common/DirectoryPathParser.cs index 50639999..d0c0bc0f 100644 --- a/src/LibHac/Fs/Common/DirectoryPathParser.cs +++ b/src/LibHac/Fs/Common/DirectoryPathParser.cs @@ -88,6 +88,7 @@ namespace LibHac.Fs.Common { _replacedChar = _buffer[1]; _buffer[1] = 0; + _position = 1; return _buffer; } diff --git a/src/LibHac/Fs/Common/Path.cs b/src/LibHac/Fs/Common/Path.cs index a316d36c..90d66074 100644 --- a/src/LibHac/Fs/Common/Path.cs +++ b/src/LibHac/Fs/Common/Path.cs @@ -538,12 +538,12 @@ namespace LibHac.Fs int parentBytesCopied = StringUtils.Copy(destBuffer, parent, parentLength + SeparatorLength); // Make sure we copied the expected number of parent bytes. - if (parentHasTrailingSlash) + if (!parentHasTrailingSlash) { - if (parentBytesCopied != parentLength + SeparatorLength) + if (parentBytesCopied != parentLength) return ResultFs.UnexpectedInPathA.Log(); } - else if (parentBytesCopied != parentLength) + else if (parentBytesCopied != parentLength + SeparatorLength) { return ResultFs.UnexpectedInPathA.Log(); } @@ -602,7 +602,7 @@ namespace LibHac.Fs if (_string[parentLength - 1] == DirectorySeparator || _string[parentLength - 1] == AltDirectorySeparator) parentLength--; - int childLength = StringUtils.GetLength(child); + int childLength = StringUtils.GetLength(trimmedChild); byte[] parentBuffer = null; try @@ -630,15 +630,14 @@ namespace LibHac.Fs if (childBytesCopied != childLength) return ResultFs.UnexpectedInPathA.Log(); + + return Result.Success; } finally { if (parentBuffer is not null) ArrayPool.Shared.Return(parentBuffer); } - - _isNormalized = false; - return Result.Success; } public Result AppendChild(in Path child) @@ -737,7 +736,6 @@ namespace LibHac.Fs if (currentPos <= 0) return ResultFs.NotImplemented.Log(); - _isNormalized = false; return Result.Success; } @@ -809,7 +807,7 @@ namespace LibHac.Fs } // Only a small number of format strings are used with these functions, so we can hard code them all easily. - + // /%s internal static Result SetUpFixedPathSingleEntry(ref Path path, Span pathBuffer, ReadOnlySpan entryName) diff --git a/src/LibHac/Fs/Fsa/IDirectory.cs b/src/LibHac/Fs/Fsa/IDirectory.cs index 3d5012fa..dd90bc37 100644 --- a/src/LibHac/Fs/Fsa/IDirectory.cs +++ b/src/LibHac/Fs/Fsa/IDirectory.cs @@ -44,12 +44,6 @@ namespace LibHac.Fs.Fsa protected abstract Result DoRead(out long entriesRead, Span entryBuffer); protected abstract Result DoGetEntryCount(out long entryCount); - protected virtual void Dispose(bool disposing) { } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + public virtual void Dispose() { } } } diff --git a/src/LibHac/Fs/Fsa/IFile.cs b/src/LibHac/Fs/Fsa/IFile.cs index af908b79..2660128c 100644 --- a/src/LibHac/Fs/Fsa/IFile.cs +++ b/src/LibHac/Fs/Fsa/IFile.cs @@ -219,12 +219,6 @@ namespace LibHac.Fs.Fsa protected abstract Result DoOperateRange(Span outBuffer, OperationId operationId, long offset, long size, ReadOnlySpan inBuffer); - protected virtual void Dispose(bool disposing) { } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + public virtual void Dispose() { } } } diff --git a/src/LibHac/Fs/Shim/FileSystemServiceObjectAdapter.cs b/src/LibHac/Fs/Shim/FileSystemServiceObjectAdapter.cs index 1286d837..d127aec4 100644 --- a/src/LibHac/Fs/Shim/FileSystemServiceObjectAdapter.cs +++ b/src/LibHac/Fs/Shim/FileSystemServiceObjectAdapter.cs @@ -31,14 +31,11 @@ namespace LibHac.Fs.Impl BaseFile = baseFile.AddReference(); } - protected override void Dispose(bool disposing) + public override void Dispose() { - if (disposing) - { - BaseFile?.Dispose(); - } + BaseFile?.Dispose(); - base.Dispose(disposing); + base.Dispose(); } protected override Result DoRead(out long bytesRead, long offset, Span destination, in ReadOption option) @@ -101,14 +98,11 @@ namespace LibHac.Fs.Impl BaseDirectory = baseDirectory.AddReference(); } - protected override void Dispose(bool disposing) + public override void Dispose() { - if (disposing) - { - BaseDirectory?.Dispose(); - } + BaseDirectory?.Dispose(); - base.Dispose(disposing); + base.Dispose(); } protected override Result DoRead(out long entriesRead, Span entryBuffer) diff --git a/src/LibHac/FsSrv/BaseFileSystemService.cs b/src/LibHac/FsSrv/BaseFileSystemService.cs index eaea33c3..0b0e5fe0 100644 --- a/src/LibHac/FsSrv/BaseFileSystemService.cs +++ b/src/LibHac/FsSrv/BaseFileSystemService.cs @@ -7,7 +7,7 @@ using LibHac.FsSystem; using LibHac.Sf; using IFileSystem = LibHac.Fs.Fsa.IFileSystem; using IFileSystemSf = LibHac.FsSrv.Sf.IFileSystem; -using PathNormalizer = LibHac.FsSrv.Impl.PathNormalizer; +using Path = LibHac.Fs.Path; namespace LibHac.FsSrv { @@ -117,7 +117,7 @@ namespace LibHac.FsSrv using var scopedContext = new ScopedStorageLayoutTypeSetter(storageFlag); // Normalize the path - var pathNormalized = new Fs.Path(); + var pathNormalized = new Path(); rc = pathNormalized.Initialize(rootPath.Str); if (rc.IsFailure()) return rc; @@ -135,7 +135,7 @@ namespace LibHac.FsSrv rc = _serviceImpl.OpenBisFileSystem(out baseFileSystem, partitionId, false); if (rc.IsFailure()) return rc; - rc = Impl.Utility.CreateSubDirectoryFileSystem(out fileSystem, ref baseFileSystem, in pathNormalized); + rc = Utility.CreateSubDirectoryFileSystem(out fileSystem, ref baseFileSystem, in pathNormalized); if (rc.IsFailure()) return rc; // Add all the file system wrappers diff --git a/src/LibHac/FsSrv/BaseFileSystemServiceImpl.cs b/src/LibHac/FsSrv/BaseFileSystemServiceImpl.cs index edc11658..265a441a 100644 --- a/src/LibHac/FsSrv/BaseFileSystemServiceImpl.cs +++ b/src/LibHac/FsSrv/BaseFileSystemServiceImpl.cs @@ -1,5 +1,4 @@ using System; -using LibHac.Common; using LibHac.Fs; using LibHac.FsSrv.FsCreator; using LibHac.FsSrv.Impl; diff --git a/src/LibHac/FsSrv/FsCreator/EmulatedBisFileSystemCreator.cs b/src/LibHac/FsSrv/FsCreator/EmulatedBisFileSystemCreator.cs index 62dcbb99..6b77cb0b 100644 --- a/src/LibHac/FsSrv/FsCreator/EmulatedBisFileSystemCreator.cs +++ b/src/LibHac/FsSrv/FsCreator/EmulatedBisFileSystemCreator.cs @@ -77,6 +77,11 @@ namespace LibHac.FsSrv.FsCreator Result rc = bisRootPath.Initialize(GetPartitionPath(partitionId).ToU8String()); if (rc.IsFailure()) return rc; + var pathFlags = new PathFlags(); + pathFlags.AllowEmptyPath(); + rc = bisRootPath.Normalize(pathFlags); + if (rc.IsFailure()) return rc; + ReferenceCountedDisposable partitionFileSystem = null; ReferenceCountedDisposable sharedRootFs = null; try diff --git a/src/LibHac/FsSrv/FsCreator/EmulatedSdCardFileSystemCreator.cs b/src/LibHac/FsSrv/FsCreator/EmulatedSdCardFileSystemCreator.cs index 6fab0289..1c2150e6 100644 --- a/src/LibHac/FsSrv/FsCreator/EmulatedSdCardFileSystemCreator.cs +++ b/src/LibHac/FsSrv/FsCreator/EmulatedSdCardFileSystemCreator.cs @@ -83,7 +83,7 @@ namespace LibHac.FsSrv.FsCreator try { tempFs = _rootFileSystem.AddReference(); - rc = Utility.CreateSubDirectoryFileSystem(out _sdCardFileSystem, ref tempFs, in sdCardPath); + rc = Utility.WrapSubDirectory(out _sdCardFileSystem, ref tempFs, in sdCardPath, true); if (rc.IsFailure()) return rc; outFileSystem = _sdCardFileSystem.AddReference(); diff --git a/src/LibHac/FsSrv/FsCreator/ITargetManagerFileSystemCreator.cs b/src/LibHac/FsSrv/FsCreator/ITargetManagerFileSystemCreator.cs index f1570bc4..9e0b6102 100644 --- a/src/LibHac/FsSrv/FsCreator/ITargetManagerFileSystemCreator.cs +++ b/src/LibHac/FsSrv/FsCreator/ITargetManagerFileSystemCreator.cs @@ -1,13 +1,10 @@ -using System; -using LibHac.Fs; +using LibHac.Fs; using LibHac.Fs.Fsa; namespace LibHac.FsSrv.FsCreator { public interface ITargetManagerFileSystemCreator { - // Todo: Remove raw IFilesystem function - Result Create(out IFileSystem fileSystem, bool openCaseSensitive); Result Create(out ReferenceCountedDisposable fileSystem, in Path rootPath, bool openCaseSensitive, bool ensureRootPathExists, Result pathNotFoundResult); Result NormalizeCaseOfPath(out bool isSupported, ref Path path); } diff --git a/src/LibHac/FsSrv/FsCreator/TargetManagerFileSystemCreator.cs b/src/LibHac/FsSrv/FsCreator/TargetManagerFileSystemCreator.cs index a8da0cf1..beef48c8 100644 --- a/src/LibHac/FsSrv/FsCreator/TargetManagerFileSystemCreator.cs +++ b/src/LibHac/FsSrv/FsCreator/TargetManagerFileSystemCreator.cs @@ -6,11 +6,6 @@ namespace LibHac.FsSrv.FsCreator { public class TargetManagerFileSystemCreator : ITargetManagerFileSystemCreator { - public Result Create(out IFileSystem fileSystem, bool openCaseSensitive) - { - throw new NotImplementedException(); - } - public Result Create(out ReferenceCountedDisposable fileSystem, in Path rootPath, bool openCaseSensitive, bool ensureRootPathExists, Result pathNotFoundResult) { throw new NotImplementedException(); diff --git a/src/LibHac/FsSrv/Impl/IResultConvertFileSystem.cs b/src/LibHac/FsSrv/Impl/IResultConvertFileSystem.cs index 93b44a44..2900f20c 100644 --- a/src/LibHac/FsSrv/Impl/IResultConvertFileSystem.cs +++ b/src/LibHac/FsSrv/Impl/IResultConvertFileSystem.cs @@ -15,15 +15,12 @@ namespace LibHac.FsSrv.Impl BaseFile = baseFile; } - protected override void Dispose(bool disposing) + public override void Dispose() { - if (disposing) - { - BaseFile?.Dispose(); - BaseFile = null; - } + BaseFile?.Dispose(); + BaseFile = null; - base.Dispose(disposing); + base.Dispose(); } protected override Result DoRead(out long bytesRead, long offset, Span destination, in ReadOption option) @@ -70,15 +67,12 @@ namespace LibHac.FsSrv.Impl BaseDirectory = baseDirectory; } - protected override void Dispose(bool disposing) + public override void Dispose() { - if (disposing) - { - BaseDirectory?.Dispose(); - BaseDirectory = null; - } + BaseDirectory?.Dispose(); + BaseDirectory = null; - base.Dispose(disposing); + base.Dispose(); } protected override Result DoRead(out long entriesRead, Span entryBuffer) diff --git a/src/LibHac/FsSrv/NcaFileSystemService.cs b/src/LibHac/FsSrv/NcaFileSystemService.cs index 4e306fa1..54cf8ff1 100644 --- a/src/LibHac/FsSrv/NcaFileSystemService.cs +++ b/src/LibHac/FsSrv/NcaFileSystemService.cs @@ -8,13 +8,11 @@ using LibHac.FsSystem; using LibHac.Lr; using LibHac.Ncm; using LibHac.Spl; -using LibHac.Util; using IFileSystem = LibHac.Fs.Fsa.IFileSystem; using IStorage = LibHac.Fs.IStorage; using IFileSystemSf = LibHac.FsSrv.Sf.IFileSystem; using IStorageSf = LibHac.FsSrv.Sf.IStorage; using Path = LibHac.Fs.Path; -using PathNormalizer = LibHac.FsSrv.Impl.PathNormalizer; using Utility = LibHac.FsSrv.Impl.Utility; namespace LibHac.FsSrv @@ -96,7 +94,7 @@ namespace LibHac.FsSrv if (rc.IsFailure()) return rc; // Try to find the path to the original version of the file system - var originalPath = new Fs.Path(); + var originalPath = new Path(); Result originalResult = ServiceImpl.ResolveApplicationHtmlDocumentPath(out bool isDirectory, ref originalPath, new Ncm.ApplicationId(programId.Value), ownerProgramInfo.StorageId); @@ -105,7 +103,7 @@ namespace LibHac.FsSrv return originalResult; // Try to find the path to the patch file system - var patchPath = new Fs.Path(); + var patchPath = new Path(); Result patchResult = ServiceImpl.ResolveRegisteredHtmlDocumentPath(ref patchPath, programId.Value); ReferenceCountedDisposable tempFileSystem = null; @@ -128,11 +126,11 @@ namespace LibHac.FsSrv if (patchResult.IsFailure()) return patchResult; - var emptyPath = new Fs.Path(); + var emptyPath = new Path(); rc = emptyPath.InitializeAsEmpty(); if (rc.IsFailure()) return rc; - ref Fs.Path originalNcaPath = ref originalResult.IsSuccess() ? ref originalPath : ref emptyPath; + ref Path originalNcaPath = ref originalResult.IsSuccess() ? ref originalPath : ref emptyPath; // Open the file system using both the original and patch versions rc = ServiceImpl.OpenFileSystemWithPatch(out tempFileSystem, in originalNcaPath, in patchPath, @@ -235,7 +233,7 @@ namespace LibHac.FsSrv bool canMountSystemDataPrivate = ac.GetAccessibilityFor(AccessibilityType.MountSystemDataPrivate).CanRead; - var pathNormalized = new Fs.Path(); + var pathNormalized = new Path(); rc = pathNormalized.InitializeWithReplaceUnc(path.Str); if (rc.IsFailure()) return rc; diff --git a/src/LibHac/FsSrv/SaveDataFileSystemService.cs b/src/LibHac/FsSrv/SaveDataFileSystemService.cs index 66baff43..39a23bfd 100644 --- a/src/LibHac/FsSrv/SaveDataFileSystemService.cs +++ b/src/LibHac/FsSrv/SaveDataFileSystemService.cs @@ -17,7 +17,6 @@ using IFile = LibHac.Fs.Fsa.IFile; using IFileSf = LibHac.FsSrv.Sf.IFile; using Path = LibHac.Fs.Path; using SaveData = LibHac.Fs.SaveData; -using Utility = LibHac.FsSystem.Utility; using static LibHac.Fs.StringTraits; namespace LibHac.FsSrv @@ -1801,7 +1800,7 @@ namespace LibHac.FsSrv saveDataId); if (rc.IsFailure()) return rc; - commitId = Impl.Utility.ConvertZeroCommitId(in extraData); + commitId = Utility.ConvertZeroCommitId(in extraData); return Result.Success; } @@ -2228,7 +2227,7 @@ namespace LibHac.FsSrv { saveService = _selfReference.AddReference(); - Result rc = Utility.MakeUniqueLockWithPin(out uniqueLock, _openEntryCountSemaphore, + Result rc = Utility12.MakeUniqueLockWithPin(out uniqueLock, _openEntryCountSemaphore, ref saveService); if (rc.IsFailure()) return rc; @@ -2252,7 +2251,7 @@ namespace LibHac.FsSrv { saveService = _selfReference.AddReference(); - Result rc = Utility.MakeUniqueLockWithPin(out uniqueLock, _saveDataMountCountSemaphore, + Result rc = Utility12.MakeUniqueLockWithPin(out uniqueLock, _saveDataMountCountSemaphore, ref saveService); if (rc.IsFailure()) return rc; diff --git a/src/LibHac/FsSrv/SaveDataFileSystemServiceImpl.cs b/src/LibHac/FsSrv/SaveDataFileSystemServiceImpl.cs index 7c2edef7..8e105c63 100644 --- a/src/LibHac/FsSrv/SaveDataFileSystemServiceImpl.cs +++ b/src/LibHac/FsSrv/SaveDataFileSystemServiceImpl.cs @@ -218,7 +218,7 @@ namespace LibHac.FsSrv UnsafeHelpers.SkipParamInit(out fileSystem); // Hack around error CS8350. - const int bufferLength = 0xF; + const int bufferLength = 0x1B; Span buffer = stackalloc byte[bufferLength]; ref byte bufferRef = ref MemoryMarshal.GetReference(buffer); Span saveDataMetaIdDirectoryNameBuffer = MemoryMarshal.CreateSpan(ref bufferRef, bufferLength); diff --git a/src/LibHac/FsSystem/AesXtsFile.cs b/src/LibHac/FsSystem/AesXtsFile.cs index 55395261..3480d9e7 100644 --- a/src/LibHac/FsSystem/AesXtsFile.cs +++ b/src/LibHac/FsSystem/AesXtsFile.cs @@ -122,14 +122,13 @@ namespace LibHac.FsSystem return BaseStorage.SetSize(Alignment.AlignUp(size, 0x10)); } - protected override void Dispose(bool disposing) + public override void Dispose() { - if (disposing) - { - BaseStorage.Flush(); - BaseStorage.Dispose(); - BaseFile?.Dispose(); - } + BaseStorage.Flush(); + BaseStorage.Dispose(); + BaseFile?.Dispose(); + + base.Dispose(); } } } diff --git a/src/LibHac/FsSystem/AesXtsFileSystem.cs b/src/LibHac/FsSystem/AesXtsFileSystem.cs index 513a9c67..4e535655 100644 --- a/src/LibHac/FsSystem/AesXtsFileSystem.cs +++ b/src/LibHac/FsSystem/AesXtsFileSystem.cs @@ -57,16 +57,16 @@ namespace LibHac.FsSystem /// Flags to control how the file is created. /// Should usually be /// The 256-bit key containing a 128-bit data key followed by a 128-bit tweak key. - public Result CreateFile(U8Span path, long size, CreateFileOptions options, byte[] key) + public Result CreateFile(in Path path, long size, CreateFileOptions options, byte[] key) { long containerSize = AesXtsFile.HeaderLength + Alignment.AlignUp(size, 0x10); - Result rc = BaseFileSystem.CreateFile(path, containerSize, options); + Result rc = BaseFileSystem.CreateFile(in path, containerSize, options); if (rc.IsFailure()) return rc; var header = new AesXtsFileHeader(key, size, path.ToString(), KekSource, ValidationKey); - rc = BaseFileSystem.OpenFile(out IFile baseFile, path, OpenMode.Write); + rc = BaseFileSystem.OpenFile(out IFile baseFile, in path, OpenMode.Write); if (rc.IsFailure()) return rc; using (baseFile) @@ -105,7 +105,7 @@ namespace LibHac.FsSystem Result rc = BaseFileSystem.OpenDirectory(out IDirectory baseDir, path, mode); if (rc.IsFailure()) return rc; - directory = new AesXtsDirectory(BaseFileSystem, baseDir, path.ToU8String(), mode); + directory = new AesXtsDirectory(BaseFileSystem, baseDir, new U8String(path.GetString().ToArray()), mode); return Result.Success; } @@ -116,7 +116,8 @@ namespace LibHac.FsSystem Result rc = BaseFileSystem.OpenFile(out IFile baseFile, path, mode | OpenMode.Read); if (rc.IsFailure()) return rc; - var xtsFile = new AesXtsFile(mode, baseFile, path.ToU8String(), KekSource, ValidationKey, BlockSize); + var xtsFile = new AesXtsFile(mode, baseFile, new U8String(path.GetString().ToArray()), KekSource, + ValidationKey, BlockSize); file = xtsFile; return Result.Success; diff --git a/src/LibHac/FsSystem/ConcatenationFileSystem.cs b/src/LibHac/FsSystem/ConcatenationFileSystem.cs index abd80351..aa2d281d 100644 --- a/src/LibHac/FsSystem/ConcatenationFileSystem.cs +++ b/src/LibHac/FsSystem/ConcatenationFileSystem.cs @@ -46,19 +46,18 @@ namespace LibHac.FsSystem _path = new Path.Stored(); } - protected override void Dispose(bool disposing) + public override void Dispose() { _path.Dispose(); - if (disposing) + foreach (IFile file in _files) { - foreach (IFile file in _files) - { - file?.Dispose(); - } - - _files.Clear(); + file?.Dispose(); } + + _files.Clear(); + + base.Dispose(); } public Result Initialize(in Path path) @@ -391,15 +390,12 @@ namespace LibHac.FsSystem _concatenationFileSystem = concatFileSystem; } - protected override void Dispose(bool disposing) + public override void Dispose() { - if (disposing) - { - _path.Dispose(); - _baseDirectory.Dispose(); - } + _path.Dispose(); + _baseDirectory.Dispose(); - base.Dispose(disposing); + base.Dispose(); } public Result Initialize(in Path path) diff --git a/src/LibHac/FsSystem/DirectorySaveDataFileSystem.cs b/src/LibHac/FsSystem/DirectorySaveDataFileSystem.cs index eff324c9..22bf00d9 100644 --- a/src/LibHac/FsSystem/DirectorySaveDataFileSystem.cs +++ b/src/LibHac/FsSystem/DirectorySaveDataFileSystem.cs @@ -6,7 +6,6 @@ using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.FsSrv; using LibHac.Os; -using LibHac.Util; namespace LibHac.FsSystem { @@ -26,21 +25,16 @@ namespace LibHac.FsSystem /// /// Transactional commits should be atomic as long as the function of the /// underlying is atomic. - ///
Based on FS 11.0.0 (nnSdk 11.4.0) + ///
Based on FS 12.0.3 (nnSdk 12.3.1) ///
public class DirectorySaveDataFileSystem : IFileSystem, ISaveDataExtraDataAccessor { private const int IdealWorkBufferSize = 0x100000; // 1 MiB - private static ReadOnlySpan CommittedDirectoryBytes => new[] { (byte)'/', (byte)'0', (byte)'/' }; - private static ReadOnlySpan WorkingDirectoryBytes => new[] { (byte)'/', (byte)'1', (byte)'/' }; - private static ReadOnlySpan SynchronizingDirectoryBytes => new[] { (byte)'/', (byte)'_', (byte)'/' }; - private static ReadOnlySpan LockFileBytes => new[] { (byte)'/', (byte)'.', (byte)'l', (byte)'o', (byte)'c', (byte)'k' }; - - private static U8Span CommittedDirectoryPath => new U8Span(CommittedDirectoryBytes); - private static U8Span WorkingDirectoryPath => new U8Span(WorkingDirectoryBytes); - private static U8Span SynchronizingDirectoryPath => new U8Span(SynchronizingDirectoryBytes); - private static U8Span LockFilePath => new U8Span(LockFileBytes); + private static ReadOnlySpan CommittedDirectoryName => new[] { (byte)'/', (byte)'0' }; + private static ReadOnlySpan ModifiedDirectoryName => new[] { (byte)'/', (byte)'1' }; + private static ReadOnlySpan SynchronizingDirectoryName => new[] { (byte)'/', (byte)'_' }; + private static ReadOnlySpan LockFileName => new[] { (byte)'/', (byte)'.', (byte)'l', (byte)'o', (byte)'c', (byte)'k' }; private FileSystemClient _fsClient; private IFileSystem _baseFs; @@ -71,13 +65,26 @@ namespace LibHac.FsSystem private DirectorySaveDataFileSystem _parentFs; private OpenMode _mode; - public DirectorySaveDataFile(DirectorySaveDataFileSystem parentFs, IFile baseFile, OpenMode mode) + public DirectorySaveDataFile(IFile baseFile, DirectorySaveDataFileSystem parentFs, OpenMode mode) { - _parentFs = parentFs; _baseFile = baseFile; + _parentFs = parentFs; _mode = mode; } + public override void Dispose() + { + _baseFile?.Dispose(); + + if (_mode.HasFlag(OpenMode.Write)) + { + _parentFs.DecrementWriteOpenFileCount(); + _mode = default; + } + + base.Dispose(); + } + protected override Result DoRead(out long bytesRead, long offset, Span destination, in ReadOption option) { @@ -109,22 +116,6 @@ namespace LibHac.FsSystem { return _baseFile.OperateRange(outBuffer, operationId, offset, size, inBuffer); } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - _baseFile?.Dispose(); - - if (_mode.HasFlag(OpenMode.Write)) - { - _parentFs.DecrementWriteOpenFileCount(); - _mode = default; - } - } - - base.Dispose(disposing); - } } public static Result CreateNew(out DirectorySaveDataFileSystem created, IFileSystem baseFileSystem, @@ -196,7 +187,24 @@ namespace LibHac.FsSystem base.Dispose(); } - private Result RetryFinitelyForTargetLocked(Func function) + private ref struct RetryClosure + { + public DirectorySaveDataFileSystem This; + public Path CommittedPath; + public Path ModifiedPath; + public Path SynchronizingPath; + + public void Dispose() + { + CommittedPath.Dispose(); + ModifiedPath.Dispose(); + SynchronizingPath.Dispose(); + } + } + + private delegate Result RetryDelegate(in RetryClosure closure); + + private Result RetryFinitelyForTargetLocked(RetryDelegate function, in RetryClosure closure) { const int maxRetryCount = 10; const int retryWaitTimeMs = 100; @@ -205,7 +213,7 @@ namespace LibHac.FsSystem while (true) { - Result rc = function(); + Result rc = function(in closure); if (rc.IsSuccess()) return rc; @@ -237,8 +245,6 @@ namespace LibHac.FsSystem public Result Initialize(ISaveDataCommitTimeStampGetter timeStampGetter, RandomDataGenerator randomGenerator, bool isJournalingSupported, bool isMultiCommitSupported, bool isJournalingEnabled) { - Result rc; - _isJournalingSupported = isJournalingSupported; _isMultiCommitSupported = isMultiCommitSupported; _isJournalingEnabled = isJournalingEnabled; @@ -246,233 +252,304 @@ namespace LibHac.FsSystem _randomGenerator = randomGenerator ?? _randomGenerator; // Open the lock file - if (_lockFile is null) - { - rc = _baseFs.OpenFile(out _lockFile, LockFilePath, OpenMode.ReadWrite); + Result rc = GetFileSystemLock(); + if (rc.IsFailure()) return rc; - if (rc.IsFailure()) - { - if (!ResultFs.PathNotFound.Includes(rc)) return rc; + var pathModifiedDirectory = new Path(); + rc = PathFunctions.SetUpFixedPath(ref pathModifiedDirectory, ModifiedDirectoryName); + if (rc.IsFailure()) return rc; - rc = _baseFs.CreateFile(LockFilePath, 0); - if (rc.IsFailure()) return rc; + var pathCommittedDirectory = new Path(); + rc = PathFunctions.SetUpFixedPath(ref pathCommittedDirectory, CommittedDirectoryName); + if (rc.IsFailure()) return rc; - rc = _baseFs.OpenFile(out _lockFile, LockFilePath, OpenMode.ReadWrite); - if (rc.IsFailure()) return rc; - } - } + var pathSynchronizingDirectory = new Path(); + rc = PathFunctions.SetUpFixedPath(ref pathSynchronizingDirectory, SynchronizingDirectoryName); + if (rc.IsFailure()) return rc; // Ensure the working directory exists - rc = _baseFs.GetEntryType(out _, WorkingDirectoryPath); + rc = _baseFs.GetEntryType(out _, in pathModifiedDirectory); if (rc.IsFailure()) { - if (!ResultFs.PathNotFound.Includes(rc)) return rc; + if (!ResultFs.PathNotFound.Includes(rc)) + return rc; - rc = _baseFs.CreateDirectory(WorkingDirectoryPath); + rc = _baseFs.CreateDirectory(in pathModifiedDirectory); if (rc.IsFailure()) return rc; if (_isJournalingSupported) { - rc = _baseFs.CreateDirectory(CommittedDirectoryPath); + rc = _baseFs.CreateDirectory(in pathCommittedDirectory); // 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; + if (rc.IsFailure() && !ResultFs.PathAlreadyExists.Includes(rc)) + return rc; } } // Only the working directory is needed for non-journaling savedata - if (!_isJournalingSupported) - return InitializeExtraData(); - - rc = _baseFs.GetEntryType(out _, CommittedDirectoryPath); - - if (rc.IsSuccess()) + if (_isJournalingSupported) { - if (!_isJournalingEnabled) - return InitializeExtraData(); + rc = _baseFs.GetEntryType(out _, in pathCommittedDirectory); - rc = SynchronizeDirectory(WorkingDirectoryPath, CommittedDirectoryPath); - if (rc.IsFailure()) return rc; + if (rc.IsSuccess()) + { + // The previous commit successfully completed. Copy the committed dir to the working dir. + if (_isJournalingEnabled) + { + rc = SynchronizeDirectory(in pathModifiedDirectory, in pathCommittedDirectory); + if (rc.IsFailure()) return rc; + } + } + else if (ResultFs.PathNotFound.Includes(rc)) + { + // If a previous commit failed, the committed dir may be missing. + // Finish that commit by copying the working dir to the committed dir + rc = SynchronizeDirectory(in pathSynchronizingDirectory, in pathModifiedDirectory); + if (rc.IsFailure()) return rc; - return InitializeExtraData(); + rc = _baseFs.RenameDirectory(in pathSynchronizingDirectory, in pathCommittedDirectory); + if (rc.IsFailure()) return rc; + } + else + { + return rc; + } } - if (!ResultFs.PathNotFound.Includes(rc)) return rc; - - // If a previous commit failed, the committed dir may be missing. - // Finish that commit by copying the working dir to the committed dir - - rc = SynchronizeDirectory(SynchronizingDirectoryPath, WorkingDirectoryPath); - if (rc.IsFailure()) return rc; - - rc = _baseFs.RenameDirectory(SynchronizingDirectoryPath, CommittedDirectoryPath); - if (rc.IsFailure()) return rc; - rc = InitializeExtraData(); if (rc.IsFailure()) return rc; + pathModifiedDirectory.Dispose(); + pathCommittedDirectory.Dispose(); + pathSynchronizingDirectory.Dispose(); + return Result.Success; } - private Result ResolveFullPath(Span outPath, U8Span relativePath) + private Result GetFileSystemLock() { - if (StringUtils.GetLength(relativePath, PathTools.MaxPathLength + 1) > PathTools.MaxPathLength) - return ResultFs.TooLongPath.Log(); + // Having an open lock file means we already have the lock for the file system. + if (_lockFile is not null) + return Result.Success; + + var pathLockFile = new Path(); + Result rc = PathFunctions.SetUpFixedPath(ref pathLockFile, LockFileName); + if (rc.IsFailure()) return rc; + + rc = _baseFs.OpenFile(out _lockFile, in pathLockFile, OpenMode.ReadWrite); + + if (rc.IsFailure()) + { + if (ResultFs.PathNotFound.Includes(rc)) + { + rc = _baseFs.CreateFile(in pathLockFile, 0); + if (rc.IsFailure()) return rc; + + rc = _baseFs.OpenFile(out _lockFile, in pathLockFile, OpenMode.ReadWrite); + if (rc.IsFailure()) return rc; + } + else + { + return rc; + } + } + + pathLockFile.Dispose(); + return Result.Success; + } + + private Result ResolvePath(ref Path outFullPath, in Path path) + { + var pathDirectoryName = new Path(); // Use the committed directory directly if journaling is supported but not enabled - U8Span workingPath = _isJournalingSupported && !_isJournalingEnabled - ? CommittedDirectoryPath - : WorkingDirectoryPath; + ReadOnlySpan directoryName = _isJournalingSupported && !_isJournalingEnabled + ? CommittedDirectoryName + : ModifiedDirectoryName; - StringUtils.Copy(outPath, workingPath); - outPath[outPath.Length - 1] = StringTraits.NullTerminator; + Result rc = PathFunctions.SetUpFixedPath(ref pathDirectoryName, directoryName); + if (rc.IsFailure()) return rc; - return PathNormalizer.Normalize(outPath.Slice(2), out _, relativePath, false, false); + rc = outFullPath.Combine(in pathDirectoryName, in path); + if (rc.IsFailure()) return rc; + + pathDirectoryName.Dispose(); + return Result.Success; } protected override Result DoCreateFile(in Path path, long size, CreateFileOptions option) { - Unsafe.SkipInit(out FsPath fullPath); - - Result rc = ResolveFullPath(fullPath.Str, path); + var fullPath = new Path(); + Result rc = ResolvePath(ref fullPath, in path); if (rc.IsFailure()) return rc; - using ScopedLock lk = ScopedLock.Lock(ref _mutex); + using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex); - return _baseFs.CreateFile(fullPath, size, option); + rc = _baseFs.CreateFile(in fullPath, size, option); + if (rc.IsFailure()) return rc; + + fullPath.Dispose(); + return Result.Success; } protected override Result DoDeleteFile(in Path path) { - Unsafe.SkipInit(out FsPath fullPath); - - Result rc = ResolveFullPath(fullPath.Str, path); + var fullPath = new Path(); + Result rc = ResolvePath(ref fullPath, in path); if (rc.IsFailure()) return rc; - using ScopedLock lk = ScopedLock.Lock(ref _mutex); + using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex); - return _baseFs.DeleteFile(fullPath); + rc = _baseFs.DeleteFile(in fullPath); + if (rc.IsFailure()) return rc; + + fullPath.Dispose(); + return Result.Success; } protected override Result DoCreateDirectory(in Path path) { - Unsafe.SkipInit(out FsPath fullPath); - - Result rc = ResolveFullPath(fullPath.Str, path); + var fullPath = new Path(); + Result rc = ResolvePath(ref fullPath, in path); if (rc.IsFailure()) return rc; - using ScopedLock lk = ScopedLock.Lock(ref _mutex); + using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex); - return _baseFs.CreateDirectory(fullPath); + rc = _baseFs.CreateDirectory(in fullPath); + if (rc.IsFailure()) return rc; + + fullPath.Dispose(); + return Result.Success; } protected override Result DoDeleteDirectory(in Path path) { - Unsafe.SkipInit(out FsPath fullPath); - - Result rc = ResolveFullPath(fullPath.Str, path); + var fullPath = new Path(); + Result rc = ResolvePath(ref fullPath, in path); if (rc.IsFailure()) return rc; - using ScopedLock lk = ScopedLock.Lock(ref _mutex); + using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex); - return _baseFs.DeleteDirectory(fullPath); + rc = _baseFs.DeleteDirectory(in fullPath); + if (rc.IsFailure()) return rc; + + fullPath.Dispose(); + return Result.Success; } protected override Result DoDeleteDirectoryRecursively(in Path path) { - Unsafe.SkipInit(out FsPath fullPath); - - Result rc = ResolveFullPath(fullPath.Str, path); + var fullPath = new Path(); + Result rc = ResolvePath(ref fullPath, in path); if (rc.IsFailure()) return rc; - using ScopedLock lk = ScopedLock.Lock(ref _mutex); + using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex); - return _baseFs.DeleteDirectoryRecursively(fullPath); + rc = _baseFs.DeleteDirectoryRecursively(in fullPath); + if (rc.IsFailure()) return rc; + + fullPath.Dispose(); + return Result.Success; } protected override Result DoCleanDirectoryRecursively(in Path path) { - Unsafe.SkipInit(out FsPath fullPath); - - Result rc = ResolveFullPath(fullPath.Str, path); + var fullPath = new Path(); + Result rc = ResolvePath(ref fullPath, in path); if (rc.IsFailure()) return rc; - using ScopedLock lk = ScopedLock.Lock(ref _mutex); + using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex); - return _baseFs.CleanDirectoryRecursively(fullPath); + rc = _baseFs.CleanDirectoryRecursively(in fullPath); + if (rc.IsFailure()) return rc; + + fullPath.Dispose(); + return Result.Success; } protected override Result DoRenameFile(in Path currentPath, in Path newPath) { - Unsafe.SkipInit(out FsPath fullCurrentPath); - Unsafe.SkipInit(out FsPath fullNewPath); + var currentFullPath = new Path(); + var newFullPath = new Path(); - Result rc = ResolveFullPath(fullCurrentPath.Str, currentPath); + Result rc = ResolvePath(ref currentFullPath, in currentPath); if (rc.IsFailure()) return rc; - rc = ResolveFullPath(fullNewPath.Str, newPath); + rc = ResolvePath(ref newFullPath, in newPath); if (rc.IsFailure()) return rc; - using ScopedLock lk = ScopedLock.Lock(ref _mutex); + using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex); - return _baseFs.RenameFile(fullCurrentPath, fullNewPath); + rc = _baseFs.RenameFile(in currentFullPath, in newFullPath); + if (rc.IsFailure()) return rc; + + currentFullPath.Dispose(); + newFullPath.Dispose(); + return Result.Success; } protected override Result DoRenameDirectory(in Path currentPath, in Path newPath) { - Unsafe.SkipInit(out FsPath fullCurrentPath); - Unsafe.SkipInit(out FsPath fullNewPath); + var currentFullPath = new Path(); + var newFullPath = new Path(); - Result rc = ResolveFullPath(fullCurrentPath.Str, currentPath); + Result rc = ResolvePath(ref currentFullPath, in currentPath); if (rc.IsFailure()) return rc; - rc = ResolveFullPath(fullNewPath.Str, newPath); + rc = ResolvePath(ref newFullPath, in newPath); if (rc.IsFailure()) return rc; - using ScopedLock lk = ScopedLock.Lock(ref _mutex); + using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex); - return _baseFs.RenameDirectory(fullCurrentPath, fullNewPath); + rc = _baseFs.RenameDirectory(in currentFullPath, in newFullPath); + if (rc.IsFailure()) return rc; + + currentFullPath.Dispose(); + newFullPath.Dispose(); + return Result.Success; } protected override Result DoGetEntryType(out DirectoryEntryType entryType, in Path path) { - Unsafe.SkipInit(out FsPath fullPath); + UnsafeHelpers.SkipParamInit(out entryType); - Result rc = ResolveFullPath(fullPath.Str, path); - if (rc.IsFailure()) - { - UnsafeHelpers.SkipParamInit(out entryType); - return rc; - } + var fullPath = new Path(); + Result rc = ResolvePath(ref fullPath, in path); + if (rc.IsFailure()) return rc; - using ScopedLock lk = ScopedLock.Lock(ref _mutex); + using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex); - return _baseFs.GetEntryType(out entryType, fullPath); + rc = _baseFs.GetEntryType(out entryType, in fullPath); + if (rc.IsFailure()) return rc; + + fullPath.Dispose(); + return Result.Success; } protected override Result DoOpenFile(out IFile file, in Path path, OpenMode mode) { UnsafeHelpers.SkipParamInit(out file); - Unsafe.SkipInit(out FsPath fullPath); - - Result rc = ResolveFullPath(fullPath.Str, path); + var fullPath = new Path(); + Result rc = ResolvePath(ref fullPath, in path); if (rc.IsFailure()) return rc; - using ScopedLock lk = ScopedLock.Lock(ref _mutex); + using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex); - rc = _baseFs.OpenFile(out IFile baseFile, fullPath, mode); + rc = _baseFs.OpenFile(out IFile baseFile, in fullPath, mode); if (rc.IsFailure()) return rc; - file = new DirectorySaveDataFile(this, baseFile, mode); + file = new DirectorySaveDataFile(baseFile, this, mode); if (mode.HasFlag(OpenMode.Write)) { _openWritableFileCount++; } + fullPath.Dispose(); return Result.Success; } @@ -480,14 +557,17 @@ namespace LibHac.FsSystem { UnsafeHelpers.SkipParamInit(out directory); - Unsafe.SkipInit(out FsPath fullPath); - - Result rc = ResolveFullPath(fullPath.Str, path); + var fullPath = new Path(); + Result rc = ResolvePath(ref fullPath, in path); if (rc.IsFailure()) return rc; - using ScopedLock lk = ScopedLock.Lock(ref _mutex); + using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex); - return _baseFs.OpenDirectory(out directory, fullPath, mode); + rc = _baseFs.OpenDirectory(out directory, in fullPath, mode); + if (rc.IsFailure()) return rc; + + fullPath.Dispose(); + return Result.Success; } /// @@ -496,12 +576,12 @@ namespace LibHac.FsSystem /// The path of the destination directory. /// The path of the source directory. /// The of the operation. - private Result SynchronizeDirectory(U8Span destPath, U8Span sourcePath) + private Result SynchronizeDirectory(in Path destPath, in Path sourcePath) { // Delete destination dir and recreate it. Result rc = _baseFs.DeleteDirectoryRecursively(destPath); - // Nintendo returns error unconditionally because SynchronizeDirectory is always called in situations + // Nintendo returns all errors unconditionally because SynchronizeDirectory is always called in situations // where a PathNotFound error would mean the save directory was in an invalid state. // We'll ignore PathNotFound errors to be more user-friendly to users who might accidentally // put the save directory in an invalid state. @@ -510,22 +590,26 @@ namespace LibHac.FsSystem rc = _baseFs.CreateDirectory(destPath); if (rc.IsFailure()) return rc; + var directoryEntry = new DirectoryEntry(); + // Lock only if initialized with a client if (_fsClient is not null) { - using ScopedLock lk = + using ScopedLock scopedLock = ScopedLock.Lock(ref _fsClient.Globals.DirectorySaveDataFileSystem.SynchronizeDirectoryMutex); using (var buffer = new RentedArray(IdealWorkBufferSize)) { - return Utility.CopyDirectoryRecursively(_baseFs, destPath, sourcePath, buffer.Span); + return Utility12.CopyDirectoryRecursively(_baseFs, in destPath, in sourcePath, ref directoryEntry, + buffer.Span); } } else { using (var buffer = new RentedArray(IdealWorkBufferSize)) { - return Utility.CopyDirectoryRecursively(_baseFs, destPath, sourcePath, buffer.Span); + return Utility12.CopyDirectoryRecursively(_baseFs, in destPath, in sourcePath, ref directoryEntry, + buffer.Span); } } } @@ -537,31 +621,56 @@ namespace LibHac.FsSystem if (!_isJournalingEnabled || !_isJournalingSupported) return Result.Success; + var closure = new RetryClosure(); + closure.This = this; + + Result rc = PathFunctions.SetUpFixedPath(ref closure.ModifiedPath, ModifiedDirectoryName); + if (rc.IsFailure()) return rc; + + rc = PathFunctions.SetUpFixedPath(ref closure.CommittedPath, CommittedDirectoryName); + if (rc.IsFailure()) return rc; + + rc = PathFunctions.SetUpFixedPath(ref closure.SynchronizingPath, SynchronizingDirectoryName); + if (rc.IsFailure()) return rc; + if (_openWritableFileCount > 0) { // All files must be closed before commiting save data. return ResultFs.WriteModeFileNotClosed.Log(); } - Result RenameCommittedDir() => _baseFs.RenameDirectory(CommittedDirectoryPath, SynchronizingDirectoryPath); - Result SynchronizeWorkingDir() => SynchronizeDirectory(SynchronizingDirectoryPath, WorkingDirectoryPath); + static Result RenameCommittedDir(in RetryClosure closure) + { + return closure.This._baseFs.RenameDirectory(in closure.CommittedPath, + in closure.SynchronizingPath); + } - Result RenameSynchronizingDir() => - _baseFs.RenameDirectory(SynchronizingDirectoryPath, CommittedDirectoryPath); + static Result SynchronizeWorkingDir(in RetryClosure closure) + { + return closure.This.SynchronizeDirectory(in closure.SynchronizingPath, + in closure.ModifiedPath); + } - // Get rid of the previous commit by renaming the folder - Result rc = RetryFinitelyForTargetLocked(RenameCommittedDir); + static Result RenameSynchronizingDir(in RetryClosure closure) + { + return closure.This._baseFs.RenameDirectory(in closure.SynchronizingPath, + in closure.CommittedPath); + } + + // Get rid of the previous commit by renaming the folder. + rc = RetryFinitelyForTargetLocked(RenameCommittedDir, in closure); if (rc.IsFailure()) return rc; // If something goes wrong beyond this point, the commit will be - // completed the next time the savedata is opened + // completed the next time the savedata is opened. - rc = RetryFinitelyForTargetLocked(SynchronizeWorkingDir); + rc = RetryFinitelyForTargetLocked(SynchronizeWorkingDir, in closure); if (rc.IsFailure()) return rc; - rc = RetryFinitelyForTargetLocked(RenameSynchronizingDir); + rc = RetryFinitelyForTargetLocked(RenameSynchronizingDir, in closure); if (rc.IsFailure()) return rc; + closure.Dispose(); return Result.Success; } @@ -586,33 +695,39 @@ namespace LibHac.FsSystem { UnsafeHelpers.SkipParamInit(out freeSpace); - Unsafe.SkipInit(out FsPath fullPath); + using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex); - Result rc = ResolveFullPath(fullPath.Str, path); + var pathModifiedDirectory = new Path(); + Result rc = PathFunctions.SetUpFixedPath(ref pathModifiedDirectory, ModifiedDirectoryName); if (rc.IsFailure()) return rc; - using ScopedLock lk = ScopedLock.Lock(ref _mutex); + rc = _baseFs.GetFreeSpaceSize(out freeSpace, in pathModifiedDirectory); + if (rc.IsFailure()) return rc; - return _baseFs.GetFreeSpaceSize(out freeSpace, fullPath); + pathModifiedDirectory.Dispose(); + return Result.Success; } protected override Result DoGetTotalSpaceSize(out long totalSpace, in Path path) { UnsafeHelpers.SkipParamInit(out totalSpace); - Unsafe.SkipInit(out FsPath fullPath); + using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex); - Result rc = ResolveFullPath(fullPath.Str, path); + var pathModifiedDirectory = new Path(); + Result rc = PathFunctions.SetUpFixedPath(ref pathModifiedDirectory, ModifiedDirectoryName); if (rc.IsFailure()) return rc; - using ScopedLock lk = ScopedLock.Lock(ref _mutex); + rc = _baseFs.GetTotalSpaceSize(out totalSpace, in pathModifiedDirectory); + if (rc.IsFailure()) return rc; - return _baseFs.GetTotalSpaceSize(out totalSpace, fullPath); + pathModifiedDirectory.Dispose(); + return Result.Success; } internal void DecrementWriteOpenFileCount() { - using ScopedLock lk = ScopedLock.Lock(ref _mutex); + using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex); _openWritableFileCount--; } @@ -620,93 +735,111 @@ namespace LibHac.FsSystem // The original class doesn't support extra data. // Everything below this point is a LibHac extension. - private static ReadOnlySpan CommittedExtraDataBytes => // "/ExtraData0" + private static ReadOnlySpan CommittedExtraDataName => // "/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" + private static ReadOnlySpan ModifiedExtraDataName => // "/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_" + private static ReadOnlySpan SynchronizingExtraDataName => // "/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() { + var pathModifiedExtraData = new Path(); + Result rc = PathFunctions.SetUpFixedPath(ref pathModifiedExtraData, ModifiedExtraDataName); + if (rc.IsFailure()) return rc; + + var pathCommittedExtraData = new Path(); + rc = PathFunctions.SetUpFixedPath(ref pathCommittedExtraData, CommittedExtraDataName); + if (rc.IsFailure()) return rc; + + var pathSynchronizingExtraData = new Path(); + rc = PathFunctions.SetUpFixedPath(ref pathSynchronizingExtraData, SynchronizingExtraDataName); + if (rc.IsFailure()) return rc; + // Ensure the extra data files exist - Result rc = _baseFs.GetEntryType(out _, WorkingExtraDataPath); + rc = _baseFs.GetEntryType(out _, in pathModifiedExtraData); if (rc.IsFailure()) { - if (!ResultFs.PathNotFound.Includes(rc)) return rc; + if (!ResultFs.PathNotFound.Includes(rc)) + return rc; - rc = _baseFs.CreateFile(WorkingExtraDataPath, Unsafe.SizeOf()); + rc = _baseFs.CreateFile(in pathModifiedExtraData, Unsafe.SizeOf()); if (rc.IsFailure()) return rc; if (_isJournalingSupported) { - rc = _baseFs.CreateFile(CommittedExtraDataPath, Unsafe.SizeOf()); - if (rc.IsFailure() && !ResultFs.PathAlreadyExists.Includes(rc)) return rc; + rc = _baseFs.CreateFile(in pathCommittedExtraData, 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); + rc = EnsureExtraDataSize(in pathModifiedExtraData); if (rc.IsFailure()) return rc; } // Only the working extra data is needed for non-journaling savedata - if (!_isJournalingSupported) - return Result.Success; - - rc = _baseFs.GetEntryType(out _, CommittedExtraDataPath); - - if (rc.IsSuccess()) + if (_isJournalingSupported) { - rc = EnsureExtraDataSize(CommittedExtraDataPath); - if (rc.IsFailure()) return rc; + rc = _baseFs.GetEntryType(out _, in pathCommittedExtraData); - if (!_isJournalingEnabled) - return Result.Success; + if (rc.IsSuccess()) + { + rc = EnsureExtraDataSize(in pathCommittedExtraData); + if (rc.IsFailure()) return rc; - return SynchronizeExtraData(WorkingExtraDataPath, CommittedExtraDataPath); + if (_isJournalingEnabled) + { + rc = SynchronizeExtraData(in pathModifiedExtraData, in pathCommittedExtraData); + if (rc.IsFailure()) return rc; + } + } + else if (ResultFs.PathNotFound.Includes(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(in pathSynchronizingExtraData, in pathModifiedExtraData); + if (rc.IsFailure()) return rc; + + rc = _baseFs.RenameFile(in pathSynchronizingExtraData, in pathCommittedExtraData); + if (rc.IsFailure()) return rc; + } + else + { + return rc; + } } - 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; + pathModifiedExtraData.Dispose(); + pathCommittedExtraData.Dispose(); + pathSynchronizingExtraData.Dispose(); return Result.Success; } - private Result EnsureExtraDataSize(U8Span path) + private Result EnsureExtraDataSize(in Path path) { IFile file = null; try { - Result rc = _baseFs.OpenFile(out file, path, OpenMode.ReadWrite); + Result rc = _baseFs.OpenFile(out file, in path, OpenMode.ReadWrite); if (rc.IsFailure()) return rc; rc = file.GetSize(out long fileSize); @@ -723,11 +856,11 @@ namespace LibHac.FsSystem } } - private Result SynchronizeExtraData(U8Span destPath, U8Span sourcePath) + private Result SynchronizeExtraData(in Path destPath, in Path sourcePath) { Span workBuffer = stackalloc byte[Unsafe.SizeOf()]; - Result rc = _baseFs.OpenFile(out IFile sourceFile, sourcePath, OpenMode.Read); + Result rc = _baseFs.OpenFile(out IFile sourceFile, in sourcePath, OpenMode.Read); if (rc.IsFailure()) return rc; using (sourceFile) @@ -738,7 +871,7 @@ namespace LibHac.FsSystem Assert.SdkEqual(bytesRead, Unsafe.SizeOf()); } - rc = _baseFs.OpenFile(out IFile destFile, destPath, OpenMode.Write); + rc = _baseFs.OpenFile(out IFile destFile, in destPath, OpenMode.Write); if (rc.IsFailure()) return rc; using (destFile) @@ -750,23 +883,25 @@ namespace LibHac.FsSystem return Result.Success; } - private U8Span GetExtraDataPath() + private Result GetExtraDataPath(ref Path path) { - return _isJournalingSupported && !_isJournalingEnabled - ? CommittedExtraDataPath - : WorkingExtraDataPath; + ReadOnlySpan extraDataName = _isJournalingSupported && !_isJournalingEnabled + ? CommittedExtraDataName + : ModifiedExtraDataName; + + return PathFunctions.SetUpFixedPath(ref path, extraDataName); } public Result WriteExtraData(in SaveDataExtraData extraData) { - using ScopedLock lk = ScopedLock.Lock(ref _mutex); + using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex); return WriteExtraDataImpl(in extraData); } public Result CommitExtraData(bool updateTimeStamp) { - using ScopedLock lk = ScopedLock.Lock(ref _mutex); + using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex); if (updateTimeStamp && _timeStampGetter is not null && _randomGenerator is not null) { @@ -779,7 +914,7 @@ namespace LibHac.FsSystem public Result ReadExtraData(out SaveDataExtraData extraData) { - using ScopedLock lk = ScopedLock.Lock(ref _mutex); + using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex); return ReadExtraDataImpl(out extraData); } @@ -812,13 +947,21 @@ namespace LibHac.FsSystem { Assert.SdkRequires(_mutex.IsLockedByCurrentThread()); - Result rc = _baseFs.OpenFile(out IFile file, GetExtraDataPath(), OpenMode.Write); + var pathExtraData = new Path(); + Result rc = GetExtraDataPath(ref pathExtraData); + if (rc.IsFailure()) return rc; + + rc = _baseFs.OpenFile(out IFile file, in pathExtraData, OpenMode.Write); if (rc.IsFailure()) return rc; using (file) { - return file.Write(0, SpanHelpers.AsReadOnlyByteSpan(in extraData), WriteOption.Flush); + rc = file.Write(0, SpanHelpers.AsReadOnlyByteSpan(in extraData), WriteOption.Flush); + if (rc.IsFailure()) return rc; } + + pathExtraData.Dispose(); + return Result.Success; } private Result CommitExtraDataImpl() @@ -828,33 +971,64 @@ namespace LibHac.FsSystem if (!_isJournalingSupported || !_isJournalingEnabled) return Result.Success; - Result RenameCommittedFile() => _baseFs.RenameFile(CommittedExtraDataPath, SynchronizingExtraDataPath); - Result SynchronizeWorkingFile() => SynchronizeExtraData(SynchronizingExtraDataPath, WorkingExtraDataPath); - Result RenameSynchronizingFile() => _baseFs.RenameFile(SynchronizingExtraDataPath, CommittedExtraDataPath); + var closure = new RetryClosure(); + closure.This = this; - // Get rid of the previous commit by renaming the folder - Result rc = RetryFinitelyForTargetLocked(RenameCommittedFile); + Result rc = PathFunctions.SetUpFixedPath(ref closure.ModifiedPath, ModifiedExtraDataName); + if (rc.IsFailure()) return rc; + + rc = PathFunctions.SetUpFixedPath(ref closure.CommittedPath, CommittedExtraDataName); + if (rc.IsFailure()) return rc; + + rc = PathFunctions.SetUpFixedPath(ref closure.SynchronizingPath, SynchronizingExtraDataName); + if (rc.IsFailure()) return rc; + + static Result RenameCommittedFile(in RetryClosure closure) + { + return closure.This._baseFs.RenameFile(in closure.CommittedPath, + in closure.SynchronizingPath); + } + + static Result SynchronizeWorkingFile(in RetryClosure closure) + { + return closure.This.SynchronizeExtraData(in closure.SynchronizingPath, + in closure.ModifiedPath); + } + + static Result RenameSynchronizingFile(in RetryClosure closure) + { + return closure.This._baseFs.RenameFile(in closure.SynchronizingPath, + in closure.CommittedPath); + } + + // Get rid of the previous commit by renaming the file. + rc = RetryFinitelyForTargetLocked(RenameCommittedFile, in closure); if (rc.IsFailure()) return rc; // If something goes wrong beyond this point, the commit will be - // completed the next time the savedata is opened + // completed the next time the savedata is opened. - rc = RetryFinitelyForTargetLocked(SynchronizeWorkingFile); + rc = RetryFinitelyForTargetLocked(SynchronizeWorkingFile, in closure); if (rc.IsFailure()) return rc; - rc = RetryFinitelyForTargetLocked(RenameSynchronizingFile); + rc = RetryFinitelyForTargetLocked(RenameSynchronizingFile, in closure); if (rc.IsFailure()) return rc; + closure.Dispose(); 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); + Assert.SdkRequires(_mutex.IsLockedByCurrentThread()); + + var pathExtraData = new Path(); + Result rc = GetExtraDataPath(ref pathExtraData); + if (rc.IsFailure()) return rc; + + rc = _baseFs.OpenFile(out IFile file, in pathExtraData, OpenMode.Read); if (rc.IsFailure()) return rc; using (file) @@ -863,9 +1037,10 @@ namespace LibHac.FsSystem if (rc.IsFailure()) return rc; Assert.SdkEqual(bytesRead, Unsafe.SizeOf()); - - return Result.Success; } + + pathExtraData.Dispose(); + return Result.Success; } public void RegisterCacheObserver(ISaveDataExtraDataAccessorCacheObserver observer, SaveDataSpaceId spaceId, @@ -879,4 +1054,4 @@ namespace LibHac.FsSystem public SaveDataSpaceId GetSaveDataSpaceId() => _spaceId; public ulong GetSaveDataId() => _saveDataId; } -} \ No newline at end of file +} diff --git a/src/LibHac/FsSystem/DirectoryUtils.cs b/src/LibHac/FsSystem/DirectoryUtils.cs deleted file mode 100644 index 260eb9ae..00000000 --- a/src/LibHac/FsSystem/DirectoryUtils.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using LibHac.Common; -using LibHac.Fs; -using LibHac.Fs.Fsa; -using LibHac.Util; - -namespace LibHac.FsSystem -{ - public static class DirectoryUtils - { - public delegate Result Blah(ReadOnlySpan path, ref DirectoryEntry entry); - - public static Result IterateDirectoryRecursivelyInternal(IFileSystem fs, Span workPath, - ref DirectoryEntry entry, Blah onEnterDir, Blah onExitDir, Blah onFile) - { - Result rc = fs.OpenDirectory(out IDirectory _, new U8Span(workPath), OpenDirectoryMode.All); - if (rc.IsFailure()) return rc; - - onFile(workPath, ref entry); - - return Result.Success; - } - - public static Result IterateDirectoryRecursively(IFileSystem fs, ReadOnlySpan path, Blah onEnterDir, Blah onExitDir, Blah onFile) - { - return Result.Success; - } - - public static Result CopyDirectoryRecursively(IFileSystem sourceFs, IFileSystem destFs, string sourcePath, - string destPath) - { - return Result.Success; - } - - public static Result CopyFile(IFileSystem destFs, IFileSystem sourceFs, ReadOnlySpan destParentPath, - ReadOnlySpan sourcePath, ref DirectoryEntry dirEntry, Span copyBuffer) - { - IFile srcFile = null; - IFile dstFile = null; - - try - { - Result rc = sourceFs.OpenFile(out srcFile, new U8Span(sourcePath), OpenMode.Read); - if (rc.IsFailure()) return rc; - - Unsafe.SkipInit(out FsPath dstPath); - dstPath.Str[0] = 0; - int dstPathLen = StringUtils.Concat(dstPath.Str, destParentPath); - dstPathLen = StringUtils.Concat(dstPath.Str, dirEntry.Name, dstPathLen); - - if (dstPathLen > FsPath.MaxLength) - { - throw new ArgumentException(); - } - - rc = destFs.CreateFile(dstPath, dirEntry.Size, CreateFileOptions.None); - if (rc.IsFailure()) return rc; - - rc = destFs.OpenFile(out dstFile, dstPath, OpenMode.Write); - if (rc.IsFailure()) return rc; - - long fileSize = dirEntry.Size; - long offset = 0; - - while (offset < fileSize) - { - rc = srcFile.Read(out long bytesRead, offset, copyBuffer, ReadOption.None); - if (rc.IsFailure()) return rc; - - rc = dstFile.Write(offset, copyBuffer.Slice(0, (int)bytesRead), WriteOption.None); - if (rc.IsFailure()) return rc; - - offset += bytesRead; - } - - return Result.Success; - } - finally - { - srcFile?.Dispose(); - dstFile?.Dispose(); - } - } - } -} diff --git a/src/LibHac/FsSystem/FileSystemExtensions.cs b/src/LibHac/FsSystem/FileSystemExtensions.cs index 26db95f2..30c9e821 100644 --- a/src/LibHac/FsSystem/FileSystemExtensions.cs +++ b/src/LibHac/FsSystem/FileSystemExtensions.cs @@ -7,6 +7,7 @@ using LibHac.Common; using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.Util; +using Path = LibHac.Fs.Path; namespace LibHac.FsSystem { @@ -15,38 +16,119 @@ namespace LibHac.FsSystem public static Result CopyDirectory(this IFileSystem sourceFs, IFileSystem destFs, string sourcePath, string destPath, IProgressReport logger = null, CreateFileOptions options = CreateFileOptions.None) { - Result rc; + const int bufferSize = 0x100000; - foreach (DirectoryEntryEx entry in sourceFs.EnumerateEntries(sourcePath, "*", SearchOptions.Default)) + var directoryEntryBuffer = new DirectoryEntry(); + + var sourcePathNormalized = new Path(); + Result rc = InitializeFromString(ref sourcePathNormalized, sourcePath); + if (rc.IsFailure()) return rc; + + var destPathNormalized = new Path(); + rc = InitializeFromString(ref destPathNormalized, destPath); + if (rc.IsFailure()) return rc; + + byte[] workBuffer = ArrayPool.Shared.Rent(bufferSize); + try { - string subSrcPath = PathTools.Normalize(PathTools.Combine(sourcePath, entry.Name)); - string subDstPath = PathTools.Normalize(PathTools.Combine(destPath, entry.Name)); + return CopyDirectoryRecursively(destFs, sourceFs, in destPathNormalized, in sourcePathNormalized, + ref directoryEntryBuffer, workBuffer, logger, options); + } + finally + { + ArrayPool.Shared.Return(workBuffer); + logger?.SetTotal(0); + } + } - if (entry.Type == DirectoryEntryType.Directory) + public static Result CopyDirectoryRecursively(IFileSystem destinationFileSystem, IFileSystem sourceFileSystem, + in Path destinationPath, in Path sourcePath, ref DirectoryEntry dirEntry, Span workBuffer, + IProgressReport logger = null, CreateFileOptions option = CreateFileOptions.None) + { + static Result OnEnterDir(in Path path, in DirectoryEntry entry, + ref Utility12.FsIterationTaskClosure closure) + { + Result rc = closure.DestinationPathBuffer.AppendChild(entry.Name); + if (rc.IsFailure()) return rc; + + return closure.SourceFileSystem.CreateDirectory(in closure.DestinationPathBuffer); + } + + static Result OnExitDir(in Path path, in DirectoryEntry entry, ref Utility12.FsIterationTaskClosure closure) + { + return closure.DestinationPathBuffer.RemoveChild(); + } + + Result OnFile(in Path path, in DirectoryEntry entry, ref Utility12.FsIterationTaskClosure closure) + { + logger?.LogMessage(path.ToString()); + + Result result = closure.DestinationPathBuffer.AppendChild(entry.Name); + if (result.IsFailure()) return result; + + result = CopyFile(closure.DestFileSystem, closure.SourceFileSystem, in closure.DestinationPathBuffer, + in path, closure.Buffer, logger, option); + if (result.IsFailure()) return result; + + return closure.DestinationPathBuffer.RemoveChild(); + } + + var taskClosure = new Utility12.FsIterationTaskClosure(); + taskClosure.Buffer = workBuffer; + taskClosure.SourceFileSystem = sourceFileSystem; + taskClosure.DestFileSystem = destinationFileSystem; + + Result rc = taskClosure.DestinationPathBuffer.Initialize(destinationPath); + if (rc.IsFailure()) return rc; + + rc = Utility12.IterateDirectoryRecursively(sourceFileSystem, in sourcePath, ref dirEntry, OnEnterDir, + OnExitDir, OnFile, ref taskClosure); + + taskClosure.DestinationPathBuffer.Dispose(); + return rc; + } + + public static Result CopyFile(IFileSystem destFileSystem, IFileSystem sourceFileSystem, in Path destPath, + in Path sourcePath, Span workBuffer, IProgressReport logger = null, + CreateFileOptions option = CreateFileOptions.None) + { + logger?.LogMessage(sourcePath.ToString()); + + // Open source file. + Result rc = sourceFileSystem.OpenFile(out IFile sourceFile, sourcePath, OpenMode.Read); + if (rc.IsFailure()) return rc; + + using (sourceFile) + { + rc = sourceFile.GetSize(out long fileSize); + if (rc.IsFailure()) return rc; + + rc = CreateOrOverwriteFile(destFileSystem, in destPath, fileSize, option); + if (rc.IsFailure()) return rc; + + rc = destFileSystem.OpenFile(out IFile destFile, in destPath, OpenMode.Write); + if (rc.IsFailure()) return rc; + + using (destFile) { - destFs.EnsureDirectoryExists(subDstPath); + // Read/Write file in work buffer sized chunks. + long remaining = fileSize; + long offset = 0; - rc = sourceFs.CopyDirectory(destFs, subSrcPath, subDstPath, logger, options); - if (rc.IsFailure()) return rc; - } + logger?.SetTotal(fileSize); - if (entry.Type == DirectoryEntryType.File) - { - destFs.CreateOrOverwriteFile(subDstPath, entry.Size, options); - - rc = sourceFs.OpenFile(out IFile srcFile, subSrcPath.ToU8Span(), OpenMode.Read); - if (rc.IsFailure()) return rc; - - using (srcFile) + while (remaining > 0) { - rc = destFs.OpenFile(out IFile dstFile, subDstPath.ToU8Span(), OpenMode.Write | OpenMode.AllowAppend); + rc = sourceFile.Read(out long bytesRead, offset, workBuffer, ReadOption.None); if (rc.IsFailure()) return rc; - using (dstFile) - { - logger?.LogMessage(subSrcPath); - srcFile.CopyTo(dstFile, logger); - } + rc = destFile.Write(offset, workBuffer.Slice(0, (int)bytesRead), WriteOption.None); + if (rc.IsFailure()) return rc; + + remaining -= bytesRead; + offset += bytesRead; + + logger?.ReportAdd(bytesRead); } } } @@ -81,9 +163,10 @@ namespace LibHac.FsSystem bool ignoreCase = searchOptions.HasFlag(SearchOptions.CaseInsensitive); bool recurse = searchOptions.HasFlag(SearchOptions.RecurseSubdirectories); - IFileSystem fs = fileSystem; + var pathNormalized = new Path(); + InitializeFromString(ref pathNormalized, path).ThrowIfFailure(); - fileSystem.OpenDirectory(out IDirectory directory, path.ToU8Span(), OpenDirectoryMode.All).ThrowIfFailure(); + fileSystem.OpenDirectory(out IDirectory directory, in pathNormalized, OpenDirectoryMode.All).ThrowIfFailure(); while (true) { @@ -102,7 +185,7 @@ namespace LibHac.FsSystem if (entry.Type != DirectoryEntryType.Directory || !recurse) continue; IEnumerable subEntries = - fs.EnumerateEntries(PathTools.Combine(path, entry.Name), searchPattern, + fileSystem.EnumerateEntries(PathTools.Combine(path, entry.Name), searchPattern, searchOptions); foreach (DirectoryEntryEx subEntry in subEntries) @@ -202,7 +285,10 @@ namespace LibHac.FsSystem public static void SetConcatenationFileAttribute(this IFileSystem fs, string path) { - fs.QueryEntry(Span.Empty, Span.Empty, QueryId.SetConcatenationFileAttribute, path.ToU8Span()); + var pathNormalized = new Path(); + InitializeFromString(ref pathNormalized, path).ThrowIfFailure(); + + fs.QueryEntry(Span.Empty, Span.Empty, QueryId.SetConcatenationFileAttribute, in pathNormalized); } public static void CleanDirectoryRecursivelyGeneric(IFileSystem fileSystem, string path) @@ -213,14 +299,17 @@ namespace LibHac.FsSystem { string subPath = PathTools.Combine(path, entry.Name); + var subPathNormalized = new Path(); + InitializeFromString(ref subPathNormalized, subPath).ThrowIfFailure(); + if (entry.Type == DirectoryEntryType.Directory) { CleanDirectoryRecursivelyGeneric(fileSystem, subPath); - fs.DeleteDirectory(subPath.ToU8Span()); + fs.DeleteDirectory(in subPathNormalized); } else if (entry.Type == DirectoryEntryType.File) { - fs.DeleteFile(subPath.ToU8Span()); + fs.DeleteFile(in subPathNormalized); } } } @@ -251,55 +340,46 @@ namespace LibHac.FsSystem public static Result EnsureDirectoryExists(this IFileSystem fs, string path) { - path = PathTools.Normalize(path); - if (fs.DirectoryExists(path)) return Result.Success; + var pathNormalized = new Path(); + Result rc = InitializeFromString(ref pathNormalized, path); + if (rc.IsFailure()) return rc; - // Find the first subdirectory in the chain that doesn't exist - int i; - for (i = path.Length - 1; i > 0; i--) - { - if (path[i] == '/') - { - string subPath = path.Substring(0, i); - - if (fs.DirectoryExists(subPath)) - { - break; - } - } - } - - // path[i] will be a '/', so skip that character - i++; - - // loop until `path.Length - 1` so CreateDirectory won't be called multiple - // times on path if the last character in the path is a '/' - for (; i < path.Length - 1; i++) - { - if (path[i] == '/') - { - string subPath = path.Substring(0, i); - - Result rc = fs.CreateDirectory(subPath.ToU8Span()); - if (rc.IsFailure()) return rc; - } - } - - return fs.CreateDirectory(path.ToU8Span()); + return Utility12.EnsureDirectory(fs, in pathNormalized); } - public static void CreateOrOverwriteFile(this IFileSystem fs, string path, long size) + public static Result CreateOrOverwriteFile(IFileSystem fileSystem, in Path path, long size, + CreateFileOptions option = CreateFileOptions.None) { - fs.CreateOrOverwriteFile(path, size, CreateFileOptions.None); + Result rc = fileSystem.CreateFile(in path, size, option); + + if (rc.IsFailure()) + { + if (!ResultFs.PathAlreadyExists.Includes(rc)) + return rc; + + rc = fileSystem.DeleteFile(in path); + if (rc.IsFailure()) return rc; + + rc = fileSystem.CreateFile(in path, size, option); + if (rc.IsFailure()) return rc; + } + + return Result.Success; } - public static void CreateOrOverwriteFile(this IFileSystem fs, string path, long size, CreateFileOptions options) + private static Result InitializeFromString(ref Path outPath, string path) { - path = PathTools.Normalize(path); + ReadOnlySpan utf8Path = StringUtils.StringToUtf8(path); - if (fs.FileExists(path)) fs.DeleteFile(path.ToU8Span()); + Result rc = outPath.Initialize(utf8Path); + if (rc.IsFailure()) return rc; - fs.CreateFile(path.ToU8Span(), size, CreateFileOptions.None); + var pathFlags = new PathFlags(); + pathFlags.AllowEmptyPath(); + outPath.Normalize(pathFlags); + if (rc.IsFailure()) return rc; + + return Result.Success; } } diff --git a/src/LibHac/FsSystem/IUniqueLock.cs b/src/LibHac/FsSystem/IUniqueLock.cs new file mode 100644 index 00000000..e429c909 --- /dev/null +++ b/src/LibHac/FsSystem/IUniqueLock.cs @@ -0,0 +1,31 @@ +using System; +using LibHac.Common; +using LibHac.Os; + +namespace LibHac.FsSystem +{ + public interface IUniqueLock : IDisposable { } + + public class UniqueLockWithPin : IUniqueLock where T : class, IDisposable + { + private UniqueLock _semaphore; + private ReferenceCountedDisposable _pinnedObject; + + public UniqueLockWithPin(ref UniqueLock semaphore, ref ReferenceCountedDisposable pinnedObject) + { + Shared.Move(out _semaphore, ref semaphore); + Shared.Move(out _pinnedObject, ref pinnedObject); + } + + public void Dispose() + { + if (_pinnedObject != null) + { + _semaphore.Dispose(); + _pinnedObject.Dispose(); + + _pinnedObject = null; + } + } + } +} diff --git a/src/LibHac/FsSystem/LayeredFileSystem.cs b/src/LibHac/FsSystem/LayeredFileSystem.cs index 6a24c8b1..ec089b70 100644 --- a/src/LibHac/FsSystem/LayeredFileSystem.cs +++ b/src/LibHac/FsSystem/LayeredFileSystem.cs @@ -48,7 +48,7 @@ namespace LibHac.FsSystem foreach (IFileSystem fs in Sources) { - Result rc = fs.GetEntryType(out DirectoryEntryType entryType, path); + Result rc = fs.GetEntryType(out DirectoryEntryType entryType, in path); if (rc.IsSuccess()) { @@ -82,8 +82,8 @@ namespace LibHac.FsSystem if (!(multipleSources is null)) { - var dir = new MergedDirectory(multipleSources, path, mode); - Result rc = dir.Initialize(); + var dir = new MergedDirectory(multipleSources, mode); + Result rc = dir.Initialize(in path); if (rc.IsSuccess()) { @@ -207,25 +207,27 @@ namespace LibHac.FsSystem // Needed to open new directories for GetEntryCount private List SourceFileSystems { get; } private List SourceDirs { get; } - private U8String Path { get; } + private Path.Stored _path; private OpenDirectoryMode Mode { get; } // todo: Efficient way to remove duplicates private HashSet Names { get; } = new HashSet(); - public MergedDirectory(List sourceFileSystems, U8Span path, OpenDirectoryMode mode) + public MergedDirectory(List sourceFileSystems, OpenDirectoryMode mode) { SourceFileSystems = sourceFileSystems; SourceDirs = new List(sourceFileSystems.Count); - Path = path.ToU8String(); Mode = mode; } - public Result Initialize() + public Result Initialize(in Path path) { + Result rc = _path.Initialize(in path); + if (rc.IsFailure()) return rc; + foreach (IFileSystem fs in SourceFileSystems) { - Result rc = fs.OpenDirectory(out IDirectory dir, Path, Mode); + rc = fs.OpenDirectory(out IDirectory dir, in path, Mode); if (rc.IsFailure()) return rc; SourceDirs.Add(dir); @@ -268,10 +270,12 @@ namespace LibHac.FsSystem // todo: Efficient way to remove duplicates var names = new HashSet(); + Path path = _path.GetPath(); + // Open new directories for each source because we need to remove duplicate entries foreach (IFileSystem fs in SourceFileSystems) { - Result rc = fs.OpenDirectory(out IDirectory dir, Path, Mode); + Result rc = fs.OpenDirectory(out IDirectory dir, in path, Mode); if (rc.IsFailure()) return rc; long entriesRead; diff --git a/src/LibHac/FsSystem/LocalFile.cs b/src/LibHac/FsSystem/LocalFile.cs index e4faf0d8..a524c5f1 100644 --- a/src/LibHac/FsSystem/LocalFile.cs +++ b/src/LibHac/FsSystem/LocalFile.cs @@ -92,14 +92,12 @@ namespace LibHac.FsSystem return Result.Success; } - protected override void Dispose(bool disposing) + public override void Dispose() { - if (disposing) - { - File?.Dispose(); - } - + File?.Dispose(); Stream?.Dispose(); + + base.Dispose(); } } } diff --git a/src/LibHac/FsSystem/LocalFileSystem.cs b/src/LibHac/FsSystem/LocalFileSystem.cs index db98cbcb..45afc0db 100644 --- a/src/LibHac/FsSystem/LocalFileSystem.cs +++ b/src/LibHac/FsSystem/LocalFileSystem.cs @@ -2,7 +2,6 @@ using System.Buffers; using System.Collections.Generic; using System.IO; -using System.Runtime.CompilerServices; using System.Text.Unicode; using System.Threading; using LibHac.Common; @@ -33,7 +32,8 @@ namespace LibHac.FsSystem CaseSensitive } - private string _rootPath; + private Path.Stored _rootPath; + private string _rootPathUtf16; private readonly FileSystemClient _fsClient; private PathMode _mode; private readonly bool _useUnixTime; @@ -57,17 +57,9 @@ namespace LibHac.FsSystem /// The path that will be the root of the . public LocalFileSystem(string rootPath) { - _rootPath = System.IO.Path.GetFullPath(rootPath); - - if (!Directory.Exists(_rootPath)) - { - if (File.Exists(_rootPath)) - { - throw new DirectoryNotFoundException($"The specified path is a file. ({rootPath})"); - } - - Directory.CreateDirectory(_rootPath); - } + Result rc = Initialize(rootPath, PathMode.DefaultCaseSensitivity, true); + if (rc.IsFailure()) + throw new HorizonResultException(rc, "Error creating LocalFileSystem."); } public static Result Create(out LocalFileSystem fileSystem, string rootPath, @@ -85,6 +77,8 @@ namespace LibHac.FsSystem public Result Initialize(string rootPath, PathMode pathMode, bool ensurePathExists) { + Result rc; + if (rootPath == null) return ResultFs.NullptrArgument.Log(); @@ -93,13 +87,21 @@ namespace LibHac.FsSystem // If the root path is empty, we interpret any incoming paths as rooted paths. if (rootPath == string.Empty) { - _rootPath = rootPath; + var path = new Path(); + rc = path.InitializeAsEmpty(); + if (rc.IsFailure()) return rc; + + rc = _rootPath.Initialize(in path); + if (rc.IsFailure()) return rc; + return Result.Success; } + string rootPathNormalized; + try { - _rootPath = System.IO.Path.GetFullPath(rootPath); + rootPathNormalized = System.IO.Path.GetFullPath(rootPath); } catch (PathTooLongException) { @@ -110,14 +112,14 @@ namespace LibHac.FsSystem return ResultFs.InvalidCharacter.Log(); } - if (!Directory.Exists(_rootPath)) + if (!Directory.Exists(rootPathNormalized)) { - if (!ensurePathExists || File.Exists(_rootPath)) + if (!ensurePathExists || File.Exists(rootPathNormalized)) return ResultFs.PathNotFound.Log(); try { - Directory.CreateDirectory(_rootPath); + Directory.CreateDirectory(rootPathNormalized); } catch (Exception ex) when (ex.HResult < 0) { @@ -125,53 +127,78 @@ namespace LibHac.FsSystem } } - return Result.Success; - } + ReadOnlySpan utf8Path = StringUtils.StringToUtf8(rootPathNormalized); + var pathNormalized = new Path(); - private Result ResolveFullPath(out string fullPath, U8Span path, bool checkCaseSensitivity) - { - UnsafeHelpers.SkipParamInit(out fullPath); - - Unsafe.SkipInit(out FsPath normalizedPath); - - Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false); - if (rc.IsFailure()) return rc; - - fullPath = PathTools.Combine(_rootPath, normalizedPath.ToString()); - - if (_mode == PathMode.CaseSensitive && checkCaseSensitivity) + if (utf8Path.At(0) == DirectorySeparator && utf8Path.At(1) != DirectorySeparator) { - rc = CheckPathCaseSensitively(fullPath); + rc = pathNormalized.Initialize(utf8Path.Slice(1)); + if (rc.IsFailure()) return rc; + } + else + { + rc = pathNormalized.InitializeWithReplaceUnc(utf8Path); if (rc.IsFailure()) return rc; } + var flags = new PathFlags(); + flags.AllowWindowsPath(); + flags.AllowRelativePath(); + flags.AllowEmptyPath(); + + rc = pathNormalized.Normalize(flags); + if (rc.IsFailure()) return rc; + + rc = _rootPath.Initialize(in pathNormalized); + if (rc.IsFailure()) return rc; + + _rootPathUtf16 = _rootPath.ToString(); + + pathNormalized.Dispose(); return Result.Success; } - private Result CheckSubPath(U8Span path1, U8Span path2) + private Result ResolveFullPath(out string outFullPath, in Path path, bool checkCaseSensitivity) { - Unsafe.SkipInit(out FsPath normalizedPath1); - Unsafe.SkipInit(out FsPath normalizedPath2); + UnsafeHelpers.SkipParamInit(out outFullPath); - Result rc = PathNormalizer.Normalize(normalizedPath1.Str, out _, path1, false, false); + // Always normalize the incoming path even if it claims to already be normalized + // because we don't want to allow access to anything outside the root path. + + var pathNormalized = new Path(); + Result rc = pathNormalized.Initialize(path.GetString()); if (rc.IsFailure()) return rc; - rc = PathNormalizer.Normalize(normalizedPath2.Str, out _, path2, false, false); + var pathFlags = new PathFlags(); + pathFlags.AllowWindowsPath(); + pathFlags.AllowRelativePath(); + pathFlags.AllowEmptyPath(); + rc = pathNormalized.Normalize(pathFlags); if (rc.IsFailure()) return rc; - if (PathUtility.IsSubPath(normalizedPath1, normalizedPath2)) + Path rootPath = _rootPath.GetPath(); + + var fullPath = new Path(); + rc = fullPath.Combine(in rootPath, in pathNormalized); + if (rc.IsFailure()) return rc; + + string utf16FullPath = fullPath.ToString(); + + if (_mode == PathMode.CaseSensitive && checkCaseSensitivity) { - return ResultFs.DirectoryNotRenamable.Log(); + rc = CheckPathCaseSensitively(utf16FullPath); + if (rc.IsFailure()) return rc; } + outFullPath = utf16FullPath; return Result.Success; } - protected override Result DoGetFileAttributes(out NxFileAttributes attributes, U8Span path) + protected override Result DoGetFileAttributes(out NxFileAttributes attributes, in Path path) { UnsafeHelpers.SkipParamInit(out attributes); - Result rc = ResolveFullPath(out string fullPath, path, true); + Result rc = ResolveFullPath(out string fullPath, in path, true); if (rc.IsFailure()) return rc; rc = GetFileInfo(out FileInfo info, fullPath); @@ -186,9 +213,9 @@ namespace LibHac.FsSystem return Result.Success; } - protected override Result DoSetFileAttributes(U8Span path, NxFileAttributes attributes) + protected override Result DoSetFileAttributes(in Path path, NxFileAttributes attributes) { - Result rc = ResolveFullPath(out string fullPath, path, true); + Result rc = ResolveFullPath(out string fullPath, in path, true); if (rc.IsFailure()) return rc; rc = GetFileInfo(out FileInfo info, fullPath); @@ -214,11 +241,11 @@ namespace LibHac.FsSystem return Result.Success; } - protected override Result DoGetFileSize(out long fileSize, U8Span path) + protected override Result DoGetFileSize(out long fileSize, in Path path) { UnsafeHelpers.SkipParamInit(out fileSize); - Result rc = ResolveFullPath(out string fullPath, path, true); + Result rc = ResolveFullPath(out string fullPath, in path, true); if (rc.IsFailure()) return rc; rc = GetFileInfo(out FileInfo info, fullPath); @@ -232,9 +259,9 @@ namespace LibHac.FsSystem return DoCreateDirectory(path, NxFileAttributes.None); } - protected override Result DoCreateDirectory(U8Span path, NxFileAttributes archiveAttribute) + protected override Result DoCreateDirectory(in Path path, NxFileAttributes archiveAttribute) { - Result rc = ResolveFullPath(out string fullPath, path, false); + Result rc = ResolveFullPath(out string fullPath, in path, false); if (rc.IsFailure()) return rc; rc = GetDirInfo(out DirectoryInfo dir, fullPath); @@ -255,7 +282,7 @@ namespace LibHac.FsSystem protected override Result DoCreateFile(in Path path, long size, CreateFileOptions option) { - Result rc = ResolveFullPath(out string fullPath, path, false); + Result rc = ResolveFullPath(out string fullPath, in path, false); if (rc.IsFailure()) return rc; rc = GetFileInfo(out FileInfo file, fullPath); @@ -283,7 +310,7 @@ namespace LibHac.FsSystem protected override Result DoDeleteDirectory(in Path path) { - Result rc = ResolveFullPath(out string fullPath, path, true); + Result rc = ResolveFullPath(out string fullPath, in path, true); if (rc.IsFailure()) return rc; rc = GetDirInfo(out DirectoryInfo dir, fullPath); @@ -295,7 +322,7 @@ namespace LibHac.FsSystem protected override Result DoDeleteDirectoryRecursively(in Path path) { - Result rc = ResolveFullPath(out string fullPath, path, true); + Result rc = ResolveFullPath(out string fullPath, in path, true); if (rc.IsFailure()) return rc; rc = GetDirInfo(out DirectoryInfo dir, fullPath); @@ -307,7 +334,7 @@ namespace LibHac.FsSystem protected override Result DoCleanDirectoryRecursively(in Path path) { - Result rc = ResolveFullPath(out string fullPath, path, true); + Result rc = ResolveFullPath(out string fullPath, in path, true); if (rc.IsFailure()) return rc; foreach (string file in Directory.EnumerateFiles(fullPath)) @@ -343,7 +370,7 @@ namespace LibHac.FsSystem protected override Result DoDeleteFile(in Path path) { - Result rc = ResolveFullPath(out string fullPath, path, true); + Result rc = ResolveFullPath(out string fullPath, in path, true); if (rc.IsFailure()) return rc; rc = GetFileInfo(out FileInfo file, fullPath); @@ -356,7 +383,7 @@ namespace LibHac.FsSystem protected override Result DoOpenDirectory(out IDirectory directory, in Path path, OpenDirectoryMode mode) { UnsafeHelpers.SkipParamInit(out directory); - Result rc = ResolveFullPath(out string fullPath, path, true); + Result rc = ResolveFullPath(out string fullPath, in path, true); if (rc.IsFailure()) return rc; rc = GetDirInfo(out DirectoryInfo dirInfo, fullPath); @@ -380,7 +407,7 @@ namespace LibHac.FsSystem { UnsafeHelpers.SkipParamInit(out file); - Result rc = ResolveFullPath(out string fullPath, path, true); + Result rc = ResolveFullPath(out string fullPath, in path, true); if (rc.IsFailure()) return rc; rc = GetEntryType(out DirectoryEntryType entryType, path); @@ -403,13 +430,10 @@ namespace LibHac.FsSystem protected override Result DoRenameDirectory(in Path currentPath, in Path newPath) { - Result rc = CheckSubPath(currentPath, newPath); + Result rc = ResolveFullPath(out string fullCurrentPath, in currentPath, true); if (rc.IsFailure()) return rc; - rc = ResolveFullPath(out string fullCurrentPath, currentPath, true); - if (rc.IsFailure()) return rc; - - rc = ResolveFullPath(out string fullNewPath, newPath, false); + rc = ResolveFullPath(out string fullNewPath, in newPath, false); if (rc.IsFailure()) return rc; // Official FS behavior is to do nothing in this case @@ -427,10 +451,10 @@ namespace LibHac.FsSystem protected override Result DoRenameFile(in Path currentPath, in Path newPath) { - Result rc = ResolveFullPath(out string fullCurrentPath, currentPath, true); + Result rc = ResolveFullPath(out string fullCurrentPath, in currentPath, true); if (rc.IsFailure()) return rc; - rc = ResolveFullPath(out string fullNewPath, newPath, false); + rc = ResolveFullPath(out string fullNewPath, in newPath, false); if (rc.IsFailure()) return rc; // Official FS behavior is to do nothing in this case @@ -450,7 +474,7 @@ namespace LibHac.FsSystem { UnsafeHelpers.SkipParamInit(out entryType); - Result rc = ResolveFullPath(out string fullPath, path, true); + Result rc = ResolveFullPath(out string fullPath, in path, true); if (rc.IsFailure()) return rc; rc = GetDirInfo(out DirectoryInfo dir, fullPath); @@ -478,7 +502,7 @@ namespace LibHac.FsSystem { UnsafeHelpers.SkipParamInit(out timeStamp); - Result rc = ResolveFullPath(out string fullPath, path, true); + Result rc = ResolveFullPath(out string fullPath, in path, true); if (rc.IsFailure()) return rc; rc = GetFileInfo(out FileInfo file, fullPath); @@ -508,7 +532,7 @@ namespace LibHac.FsSystem { UnsafeHelpers.SkipParamInit(out freeSpace); - Result rc = ResolveFullPath(out string fullPath, path, true); + Result rc = ResolveFullPath(out string fullPath, in path, true); if (rc.IsFailure()) return rc; freeSpace = new DriveInfo(fullPath).AvailableFreeSpace; @@ -519,7 +543,7 @@ namespace LibHac.FsSystem { UnsafeHelpers.SkipParamInit(out totalSpace); - Result rc = ResolveFullPath(out string fullPath, path, true); + Result rc = ResolveFullPath(out string fullPath, in path, true); if (rc.IsFailure()) return rc; totalSpace = new DriveInfo(fullPath).TotalSize; @@ -802,7 +826,7 @@ namespace LibHac.FsSystem private Result CheckPathCaseSensitively(string path) { - Result rc = GetCaseSensitivePathFull(out string caseSensitivePath, out _, path, _rootPath); + Result rc = GetCaseSensitivePathFull(out string caseSensitivePath, out _, path, _rootPathUtf16); if (rc.IsFailure()) return rc; if (path.Length != caseSensitivePath.Length) diff --git a/src/LibHac/FsSystem/PartitionFileSystem.cs b/src/LibHac/FsSystem/PartitionFileSystem.cs index 1e415999..9aaf4707 100644 --- a/src/LibHac/FsSystem/PartitionFileSystem.cs +++ b/src/LibHac/FsSystem/PartitionFileSystem.cs @@ -42,9 +42,9 @@ namespace LibHac.FsSystem protected override Result DoOpenFile(out IFile file, in Path path, OpenMode mode) { - path = PathTools.Normalize(path.ToString()).TrimStart('/').ToU8Span(); + string pathNormalized = PathTools.Normalize(path.ToString()).TrimStart('/'); - if (!FileDict.TryGetValue(path.ToString(), out PartitionFileEntry entry)) + if (!FileDict.TryGetValue(pathNormalized, out PartitionFileEntry entry)) { ThrowHelper.ThrowResult(ResultFs.PathNotFound.Value); } diff --git a/src/LibHac/FsSystem/PartitionFileSystemCore.cs b/src/LibHac/FsSystem/PartitionFileSystemCore.cs index 51a96205..1c6726f5 100644 --- a/src/LibHac/FsSystem/PartitionFileSystemCore.cs +++ b/src/LibHac/FsSystem/PartitionFileSystemCore.cs @@ -58,7 +58,7 @@ namespace LibHac.FsSystem ReadOnlySpan rootPath = new[] { (byte)'/' }; - if (StringUtils.Compare(rootPath, path, 2) != 0) + if (path == rootPath) return ResultFs.PathNotFound.Log(); directory = new PartitionDirectory(this, mode); @@ -76,7 +76,7 @@ namespace LibHac.FsSystem if (!mode.HasFlag(OpenMode.Read) && !mode.HasFlag(OpenMode.Write)) return ResultFs.InvalidArgument.Log(); - int entryIndex = MetaData.FindEntry(path.Slice(1)); + int entryIndex = MetaData.FindEntry(new U8Span(path.GetString().Slice(1))); if (entryIndex < 0) return ResultFs.PathNotFound.Log(); ref T entry = ref MetaData.GetEntry(entryIndex); @@ -93,18 +93,20 @@ namespace LibHac.FsSystem if (!IsInitialized) return ResultFs.PreconditionViolation.Log(); - if (path.IsEmpty() || path[0] != '/') + ReadOnlySpan pathStr = path.GetString(); + + if (path.IsEmpty() || pathStr[0] != '/') return ResultFs.InvalidPathFormat.Log(); ReadOnlySpan rootPath = new[] { (byte)'/' }; - if (StringUtils.Compare(rootPath, path, 2) == 0) + if (StringUtils.Compare(rootPath, pathStr, 2) == 0) { entryType = DirectoryEntryType.Directory; return Result.Success; } - if (MetaData.FindEntry(path.Slice(1)) >= 0) + if (MetaData.FindEntry(new U8Span(pathStr.Slice(1))) >= 0) { entryType = DirectoryEntryType.File; return Result.Success; diff --git a/src/LibHac/FsSystem/PathTools.cs b/src/LibHac/FsSystem/PathTools.cs index 39aee040..34f7ed24 100644 --- a/src/LibHac/FsSystem/PathTools.cs +++ b/src/LibHac/FsSystem/PathTools.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.IO.Enumeration; using System.Runtime.CompilerServices; using LibHac.Common; +using LibHac.Diag; using LibHac.Fs; using LibHac.Util; @@ -235,7 +236,7 @@ namespace LibHac.FsSystem public static ReadOnlySpan GetParentDirectory(ReadOnlySpan path) { - Debug.Assert(IsNormalized(path)); + Assert.SdkAssert(IsNormalized(path)); int i = StringUtils.GetLength(path) - 1; @@ -288,6 +289,9 @@ namespace LibHac.FsSystem foreach (char c in path) { + if (c == 0) + break; + switch (state) { case NormalizeState.Initial when c == '/': state = NormalizeState.Delimiter; break; @@ -325,6 +329,9 @@ namespace LibHac.FsSystem foreach (byte c in path) { + if (c == 0) + break; + switch (state) { case NormalizeState.Initial when c == '/': state = NormalizeState.Delimiter; break; diff --git a/src/LibHac/FsSystem/UniqueLockSemaphore.cs b/src/LibHac/FsSystem/UniqueLockSemaphore.cs deleted file mode 100644 index dfa01846..00000000 --- a/src/LibHac/FsSystem/UniqueLockSemaphore.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System; -using System.Threading; -using LibHac.Common; -using LibHac.Diag; -using LibHac.Os; - -namespace LibHac.FsSystem -{ - public interface IUniqueLock : IDisposable - { - } - - /// - /// Represents a lock that may be passed between functions or objects. - /// - /// This struct must never be copied. It must always be passed by - /// reference or moved via the move constructor. - public struct UniqueLockSemaphore : IDisposable - { - private SemaphoreAdapter _semaphore; - private bool _isLocked; - - public UniqueLockSemaphore(SemaphoreAdapter semaphore) - { - _semaphore = semaphore; - _isLocked = false; - } - - public UniqueLockSemaphore(ref UniqueLockSemaphore other) - { - _semaphore = other._semaphore; - _isLocked = other._isLocked; - - other._isLocked = false; - other._semaphore = null; - } - - public bool IsLocked => _isLocked; - - public bool TryLock() - { - if (_isLocked) - { - throw new SynchronizationLockException("Attempted to lock a UniqueLock that was already locked."); - } - - _isLocked = _semaphore.TryLock(); - return _isLocked; - } - - public void Unlock() - { - if (!_isLocked) - { - throw new SynchronizationLockException("Attempted to unlock a UniqueLock that was not locked."); - } - - _semaphore.Unlock(); - _isLocked = false; - } - - public void Dispose() - { - if (_isLocked) - { - _semaphore.Unlock(); - - _isLocked = false; - _semaphore = null; - } - } - } - - public class UniqueLockWithPin : IUniqueLock where T : class, IDisposable - { - private UniqueLock _semaphore; - private ReferenceCountedDisposable _pinnedObject; - - public UniqueLockWithPin(ref UniqueLock semaphore, ref ReferenceCountedDisposable pinnedObject) - { - Shared.Move(out _semaphore, ref semaphore); - Shared.Move(out _pinnedObject, ref pinnedObject); - } - - public void Dispose() - { - if (_pinnedObject != null) - { - _semaphore.Dispose(); - _pinnedObject.Dispose(); - - _pinnedObject = null; - } - } - } -} diff --git a/src/LibHac/FsSystem/Utility.cs b/src/LibHac/FsSystem/Utility.cs deleted file mode 100644 index 66ae8aa2..00000000 --- a/src/LibHac/FsSystem/Utility.cs +++ /dev/null @@ -1,270 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using LibHac.Common; -using LibHac.Diag; -using LibHac.Fs; -using LibHac.Fs.Common; -using LibHac.Fs.Fsa; -using LibHac.Util; - -namespace LibHac.FsSystem -{ - internal static class Utility - { - public delegate Result FsIterationTask(U8Span path, ref DirectoryEntry entry); - - private static U8Span RootPath => new U8Span(new[] { (byte)'/' }); - private static U8Span DirectorySeparator => RootPath; - - public static Result IterateDirectoryRecursively(IFileSystem fs, U8Span rootPath, Span workPath, - ref DirectoryEntry dirEntry, FsIterationTask onEnterDir, FsIterationTask onExitDir, FsIterationTask onFile) - { - Abort.DoAbortUnless(workPath.Length >= PathTool.EntryNameLengthMax + 1); - - // Get size of the root path. - int rootPathLen = StringUtils.GetLength(rootPath, PathTool.EntryNameLengthMax + 1); - if (rootPathLen > PathTool.EntryNameLengthMax) - return ResultFs.TooLongPath.Log(); - - // Copy root path in, add a / if necessary. - rootPath.Value.Slice(0, rootPathLen).CopyTo(workPath); - if (workPath[rootPathLen - 1] != StringTraits.DirectorySeparator) - { - workPath[rootPathLen++] = StringTraits.DirectorySeparator; - } - - // Make sure the result path is still valid. - if (rootPathLen > PathTool.EntryNameLengthMax) - return ResultFs.TooLongPath.Log(); - - workPath[rootPathLen] = StringTraits.NullTerminator; - - return IterateDirectoryRecursivelyImpl(fs, workPath, ref dirEntry, onEnterDir, onExitDir, onFile); - } - - public static Result IterateDirectoryRecursively(IFileSystem fs, U8Span rootPath, FsIterationTask onEnterDir, - FsIterationTask onExitDir, FsIterationTask onFile) - { - var entry = new DirectoryEntry(); - Span workPath = stackalloc byte[PathTools.MaxPathLength + 1]; - - return IterateDirectoryRecursively(fs, rootPath, workPath, ref entry, onEnterDir, onExitDir, - onFile); - } - - public static Result IterateDirectoryRecursively(IFileSystem fs, FsIterationTask onEnterDir, - FsIterationTask onExitDir, FsIterationTask onFile) - { - return IterateDirectoryRecursively(fs, RootPath, onEnterDir, onExitDir, onFile); - } - - private static Result IterateDirectoryRecursivelyImpl(IFileSystem fs, Span workPath, - ref DirectoryEntry dirEntry, FsIterationTask onEnterDir, FsIterationTask onExitDir, FsIterationTask onFile) - { - Result rc = fs.OpenDirectory(out IDirectory dir, new U8Span(workPath), OpenDirectoryMode.All); - if (rc.IsFailure()) return rc; - - int parentLen = StringUtils.GetLength(workPath); - - // Read and handle entries. - while (true) - { - // Read a single entry. - rc = dir.Read(out long readCount, SpanHelpers.AsSpan(ref dirEntry)); - if (rc.IsFailure()) return rc; - - // If we're out of entries, we're done. - if (readCount == 0) - break; - - // Validate child path size. - int childNameLen = StringUtils.GetLength(dirEntry.Name); - bool isDir = dirEntry.Type == DirectoryEntryType.Directory; - int separatorSize = isDir ? 1 : 0; - - if (parentLen + childNameLen + separatorSize >= workPath.Length) - return ResultFs.TooLongPath.Log(); - - // Set child path. - StringUtils.Concat(workPath, dirEntry.Name); - { - if (isDir) - { - // Enter directory. - rc = onEnterDir(new U8Span(workPath), ref dirEntry); - if (rc.IsFailure()) return rc; - - // Append separator, recurse. - StringUtils.Concat(workPath, DirectorySeparator); - - rc = IterateDirectoryRecursivelyImpl(fs, workPath, ref dirEntry, onEnterDir, onExitDir, onFile); - if (rc.IsFailure()) return rc; - - // Exit directory. - rc = onExitDir(new U8Span(workPath), ref dirEntry); - if (rc.IsFailure()) return rc; - } - else - { - // Call file handler. - rc = onFile(new U8Span(workPath), ref dirEntry); - if (rc.IsFailure()) return rc; - } - } - - // Restore parent path. - workPath[parentLen] = StringTraits.NullTerminator; - } - - return Result.Success; - } - - public static Result CopyDirectoryRecursively(IFileSystem fileSystem, U8Span destPath, U8Span sourcePath, - Span workBuffer) - { - return CopyDirectoryRecursively(fileSystem, fileSystem, destPath, sourcePath, workBuffer); - } - - public static unsafe Result CopyDirectoryRecursively(IFileSystem destFileSystem, IFileSystem sourceFileSystem, - U8Span destPath, U8Span sourcePath, Span workBuffer) - { - var destPathBuf = new FsPath(); - int originalSize = StringUtils.Copy(destPathBuf.Str, destPath); - Abort.DoAbortUnless(originalSize < Unsafe.SizeOf()); - - // Pin and recreate the span because C# can't use byref-like types in a closure - int workBufferSize = workBuffer.Length; - fixed (byte* pWorkBuffer = workBuffer) - { - // Copy the pointer to workaround CS1764. - // IterateDirectoryRecursively won't store the delegate anywhere, so it should be safe - byte* pWorkBuffer2 = pWorkBuffer; - - Result OnEnterDir(U8Span path, ref DirectoryEntry entry) - { - // Update path, create new dir. - StringUtils.Concat(SpanHelpers.AsByteSpan(ref destPathBuf), entry.Name); - StringUtils.Concat(SpanHelpers.AsByteSpan(ref destPathBuf), DirectorySeparator); - - return destFileSystem.CreateDirectory(destPathBuf); - } - - Result OnExitDir(U8Span path, ref DirectoryEntry entry) - { - // Check we have a parent directory. - int len = StringUtils.GetLength(SpanHelpers.AsByteSpan(ref destPathBuf)); - if (len < 2) - return ResultFs.InvalidPathFormat.Log(); - - // Find previous separator, add null terminator - int cur = len - 2; - while (SpanHelpers.AsByteSpan(ref destPathBuf)[cur] != StringTraits.DirectorySeparator && cur > 0) - { - cur--; - } - - SpanHelpers.AsByteSpan(ref destPathBuf)[cur + 1] = StringTraits.NullTerminator; - - return Result.Success; - } - - Result OnFile(U8Span path, ref DirectoryEntry entry) - { - var buffer = new Span(pWorkBuffer2, workBufferSize); - - return CopyFile(destFileSystem, sourceFileSystem, destPathBuf, path, ref entry, buffer); - } - - return IterateDirectoryRecursively(sourceFileSystem, sourcePath, OnEnterDir, OnExitDir, OnFile); - } - } - - public static Result CopyFile(IFileSystem destFileSystem, IFileSystem sourceFileSystem, U8Span destParentPath, - U8Span sourcePath, ref DirectoryEntry entry, Span workBuffer) - { - // Open source file. - Result rc = sourceFileSystem.OpenFile(out IFile sourceFile, sourcePath, OpenMode.Read); - if (rc.IsFailure()) return rc; - - using (sourceFile) - { - // Open dest file. - Unsafe.SkipInit(out FsPath destPath); - - var sb = new U8StringBuilder(destPath.Str); - sb.Append(destParentPath).Append(entry.Name); - - Assert.SdkLess(sb.Length, Unsafe.SizeOf()); - - rc = destFileSystem.CreateFile(new U8Span(destPath.Str), entry.Size); - if (rc.IsFailure()) return rc; - - rc = destFileSystem.OpenFile(out IFile destFile, new U8Span(destPath.Str), OpenMode.Write); - if (rc.IsFailure()) return rc; - - using (destFile) - { - // Read/Write file in work buffer sized chunks. - long remaining = entry.Size; - long offset = 0; - - while (remaining > 0) - { - rc = sourceFile.Read(out long bytesRead, offset, workBuffer, ReadOption.None); - if (rc.IsFailure()) return rc; - - rc = destFile.Write(offset, workBuffer.Slice(0, (int)bytesRead), WriteOption.None); - if (rc.IsFailure()) return rc; - - remaining -= bytesRead; - offset += bytesRead; - } - } - } - - return Result.Success; - } - - public static Result TryAcquireCountSemaphore(out UniqueLockSemaphore uniqueLock, SemaphoreAdapter semaphore) - { - UniqueLockSemaphore tempUniqueLock = default; - try - { - tempUniqueLock = new UniqueLockSemaphore(semaphore); - - if (!tempUniqueLock.TryLock()) - { - UnsafeHelpers.SkipParamInit(out uniqueLock); - return ResultFs.OpenCountLimit.Log(); - } - - uniqueLock = new UniqueLockSemaphore(ref tempUniqueLock); - return Result.Success; - } - finally - { - tempUniqueLock.Dispose(); - } - } - - public static Result MakeUniqueLockWithPin(out IUniqueLock uniqueLock, SemaphoreAdapter semaphore, - ref ReferenceCountedDisposable objectToPin) where T : class, IDisposable - { - UnsafeHelpers.SkipParamInit(out uniqueLock); - - UniqueLockSemaphore tempUniqueLock = default; - try - { - Result rc = TryAcquireCountSemaphore(out tempUniqueLock, semaphore); - if (rc.IsFailure()) return rc; - - uniqueLock = new UniqueLockWithPin(ref tempUniqueLock, ref objectToPin); - return Result.Success; - } - finally - { - tempUniqueLock.Dispose(); - } - } - } -} diff --git a/src/LibHac/FsSystem/Utility12.cs b/src/LibHac/FsSystem/Utility12.cs index b3427b11..918852a1 100644 --- a/src/LibHac/FsSystem/Utility12.cs +++ b/src/LibHac/FsSystem/Utility12.cs @@ -433,7 +433,7 @@ namespace LibHac.FsSystem UniqueLock tempUniqueLock = default; try { - tempUniqueLock = new UniqueLock(semaphore); + tempUniqueLock = new UniqueLock(semaphore, new DeferLock()); if (!tempUniqueLock.TryLock()) { diff --git a/src/LibHac/Lr/LrService.cs b/src/LibHac/Lr/LrService.cs index e7aeeccb..6057dd91 100644 --- a/src/LibHac/Lr/LrService.cs +++ b/src/LibHac/Lr/LrService.cs @@ -8,7 +8,7 @@ namespace LibHac.Lr internal struct LrServiceGlobals { public ILocationResolverManager LocationResolver; - public SdkMutex InitializationMutex; + public SdkMutexType InitializationMutex; public void Initialize() { @@ -34,7 +34,7 @@ namespace LibHac.Lr Assert.SdkRequiresNotNull(globals.LocationResolver); // The lock over getting the service object is a LibHac addition. - using ScopedLock scopedLock = ScopedLock.Lock(ref lr.Globals.LrService.InitializationMutex); + using ScopedLock scopedLock = ScopedLock.Lock(ref lr.Globals.LrService.InitializationMutex); if (globals.LocationResolver is not null) return; diff --git a/src/LibHac/Os/UniqueLock.cs b/src/LibHac/Os/UniqueLock.cs index 13f34b52..3712605c 100644 --- a/src/LibHac/Os/UniqueLock.cs +++ b/src/LibHac/Os/UniqueLock.cs @@ -5,6 +5,12 @@ using LibHac.Common; namespace LibHac.Os { + /// + /// Specifies that a constructed should not be automatically locked upon construction.
+ /// Used only to differentiate between constructor signatures. + ///
+ public struct DeferLock { } + public static class UniqueLock { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -33,6 +39,13 @@ namespace LibHac.Os _ownsLock = true; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public UniqueLockRef(ref TMutex mutex, DeferLock tag) + { + _mutex = new Ref(ref mutex); + _ownsLock = false; + } + public UniqueLockRef(ref UniqueLockRef other) { this = other; @@ -99,11 +112,18 @@ namespace LibHac.Os [MethodImpl(MethodImplOptions.AggressiveInlining)] public UniqueLock(TMutex mutex) { - _mutex = new Ref(ref mutex); + _mutex = mutex; mutex.Lock(); _ownsLock = true; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public UniqueLock(TMutex mutex, DeferLock tag) + { + _mutex = mutex; + _ownsLock = false; + } + public UniqueLock(ref UniqueLock other) { this = other; diff --git a/src/LibHac/SwitchFs.cs b/src/LibHac/SwitchFs.cs index 0bf609ed..ede41179 100644 --- a/src/LibHac/SwitchFs.cs +++ b/src/LibHac/SwitchFs.cs @@ -44,14 +44,20 @@ namespace LibHac { var concatFs = new ConcatenationFileSystem(fileSystem); + var contentDirPath = new Fs.Path(); + PathFunctions.SetUpFixedPath(ref contentDirPath, "/Nintendo/Contents".ToU8String()).ThrowIfFailure(); + + var saveDirPath = new Fs.Path(); + PathFunctions.SetUpFixedPath(ref saveDirPath, "/Nintendo/save".ToU8String()).ThrowIfFailure(); + var contentDirFs = new SubdirectoryFileSystem(concatFs); - contentDirFs.Initialize("/Nintendo/Contents".ToU8String()).ThrowIfFailure(); + contentDirFs.Initialize(in contentDirPath).ThrowIfFailure(); AesXtsFileSystem encSaveFs = null; if (fileSystem.DirectoryExists("/Nintendo/save")) { var saveDirFs = new SubdirectoryFileSystem(concatFs); - saveDirFs.Initialize("/Nintendo/save".ToU8String()).ThrowIfFailure(); + saveDirFs.Initialize(in saveDirPath).ThrowIfFailure(); encSaveFs = new AesXtsFileSystem(saveDirFs, keySet.SdCardEncryptionKeys[0].DataRo.ToArray(), 0x4000); } @@ -65,7 +71,7 @@ namespace LibHac { var concatFs = new ConcatenationFileSystem(fileSystem); SubdirectoryFileSystem saveDirFs = null; - SubdirectoryFileSystem contentDirFs = null; + SubdirectoryFileSystem contentDirFs; if (concatFs.DirectoryExists("/save")) { diff --git a/src/hactoolnet/ProcessFsBuild.cs b/src/hactoolnet/ProcessFsBuild.cs index 3f56759a..521a69aa 100644 --- a/src/hactoolnet/ProcessFsBuild.cs +++ b/src/hactoolnet/ProcessFsBuild.cs @@ -15,7 +15,7 @@ namespace hactoolnet return; } - var localFs = new LocalFileSystem(ctx.Options.InFile); + LocalFileSystem.Create(out LocalFileSystem localFs, ctx.Options.InFile).ThrowIfFailure(); var builder = new RomFsBuilder(localFs); IStorage romFs = builder.Build(); diff --git a/tests/LibHac.Tests/Fs/FileSystemClientTests/ShimTests/SaveData.cs b/tests/LibHac.Tests/Fs/FileSystemClientTests/ShimTests/SaveData.cs index 65d4eeab..e7f557db 100644 --- a/tests/LibHac.Tests/Fs/FileSystemClientTests/ShimTests/SaveData.cs +++ b/tests/LibHac.Tests/Fs/FileSystemClientTests/ShimTests/SaveData.cs @@ -43,8 +43,8 @@ namespace LibHac.Tests.Fs.FileSystemClientTests.ShimTests var applicationId = new Ncm.ApplicationId(1); FileSystemClient fs = FileSystemServerFactory.CreateClient(true); - fs.CreateCacheStorage(applicationId, SaveDataSpaceId.SdCache, applicationId.Value, 0, 0, SaveDataFlags.None); - fs.MountCacheStorage("cache".ToU8Span(), applicationId); + Assert.Success(fs.CreateCacheStorage(applicationId, SaveDataSpaceId.SdCache, applicationId.Value, 0, 0, SaveDataFlags.None)); + Assert.Success(fs.MountCacheStorage("cache".ToU8Span(), applicationId)); fs.CreateFile("cache:/sd".ToU8Span(), 0); fs.Commit("cache".ToU8Span()); fs.Unmount("cache".ToU8Span()); diff --git a/tests/LibHac.Tests/FsSystem/ConcatenationFileSystemTests.cs b/tests/LibHac.Tests/FsSystem/ConcatenationFileSystemTests.cs index 92539021..d60d302a 100644 --- a/tests/LibHac.Tests/FsSystem/ConcatenationFileSystemTests.cs +++ b/tests/LibHac.Tests/FsSystem/ConcatenationFileSystemTests.cs @@ -1,13 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace LibHac.Tests.FsSystem +namespace LibHac.Tests.FsSystem { - class ConcatenationFileSystemTests + public class ConcatenationFileSystemTests { - asdf + //asdf } } diff --git a/tests/LibHac.Tests/SpanEqualAsserts.cs b/tests/LibHac.Tests/SpanEqualAsserts.cs index 8bdc74ed..3cd04c66 100644 --- a/tests/LibHac.Tests/SpanEqualAsserts.cs +++ b/tests/LibHac.Tests/SpanEqualAsserts.cs @@ -1,6 +1,7 @@ using System; using Xunit.Sdk; +// ReSharper disable once CheckNamespace namespace Xunit { public partial class Assert