From 2f58e2fd5a6c4ce1adaf91a2b2905484763d8b18 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Mon, 26 Jul 2021 10:18:27 -0700 Subject: [PATCH] Update ConcatenationFileSystem --- src/LibHac/FsSystem/ConcatenationDirectory.cs | 133 -- src/LibHac/FsSystem/ConcatenationFile.cs | 249 ---- .../FsSystem/ConcatenationFileSystem.cs | 1162 ++++++++++++----- .../FsSystem/ConcatenationFileSystemTests.cs | 13 + 4 files changed, 881 insertions(+), 676 deletions(-) delete mode 100644 src/LibHac/FsSystem/ConcatenationDirectory.cs delete mode 100644 src/LibHac/FsSystem/ConcatenationFile.cs create mode 100644 tests/LibHac.Tests/FsSystem/ConcatenationFileSystemTests.cs diff --git a/src/LibHac/FsSystem/ConcatenationDirectory.cs b/src/LibHac/FsSystem/ConcatenationDirectory.cs deleted file mode 100644 index e8883b8e..00000000 --- a/src/LibHac/FsSystem/ConcatenationDirectory.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using LibHac.Common; -using LibHac.Fs; -using LibHac.Fs.Fsa; -using LibHac.Util; - -namespace LibHac.FsSystem -{ - public class ConcatenationDirectory : IDirectory - { - private OpenDirectoryMode Mode { get; } - private IDirectory ParentDirectory { get; } - private IFileSystem BaseFileSystem { get; } - private ConcatenationFileSystem ParentFileSystem { get; } - - private FsPath _path; - - public ConcatenationDirectory(ConcatenationFileSystem fs, IFileSystem baseFs, IDirectory parentDirectory, OpenDirectoryMode mode, U8Span path) - { - ParentFileSystem = fs; - BaseFileSystem = baseFs; - ParentDirectory = parentDirectory; - Mode = mode; - - StringUtils.Copy(_path.Str, path); - _path.Str[PathTools.MaxPathLength] = StringTraits.NullTerminator; - - // Ensure the path ends with a separator - int pathLength = StringUtils.GetLength(path, PathTools.MaxPathLength + 1); - - if (pathLength != 0 && _path.Str[pathLength - 1] == StringTraits.DirectorySeparator) - return; - - if (pathLength >= PathTools.MaxPathLength) - throw new HorizonResultException(ResultFs.TooLongPath.Value, "abort"); - - _path.Str[pathLength] = StringTraits.DirectorySeparator; - _path.Str[pathLength + 1] = StringTraits.NullTerminator; - _path.Str[PathTools.MaxPathLength] = StringTraits.NullTerminator; - } - - protected override Result DoRead(out long entriesRead, Span entryBuffer) - { - entriesRead = 0; - var entry = new DirectoryEntry(); - Span entrySpan = SpanHelpers.AsSpan(ref entry); - - int i; - for (i = 0; i < entryBuffer.Length; i++) - { - Result rc = ParentDirectory.Read(out long baseEntriesRead, entrySpan); - if (rc.IsFailure()) return rc; - - if (baseEntriesRead == 0) break; - - // Check if the current open mode says we should return the entry - bool isConcatFile = IsConcatenationFile(entry); - if (!CanReturnEntry(entry, isConcatFile)) continue; - - if (isConcatFile) - { - entry.Type = DirectoryEntryType.File; - - if (!Mode.HasFlag(OpenDirectoryMode.NoFileSize)) - { - string entryName = StringUtils.NullTerminatedUtf8ToString(entry.Name); - string entryFullPath = PathTools.Combine(_path.ToString(), entryName); - - rc = ParentFileSystem.GetConcatenationFileSize(out long fileSize, entryFullPath.ToU8Span()); - if (rc.IsFailure()) return rc; - - entry.Size = fileSize; - } - } - - entry.Attributes = NxFileAttributes.None; - - entryBuffer[i] = entry; - } - - entriesRead = i; - return Result.Success; - } - - protected override Result DoGetEntryCount(out long entryCount) - { - entryCount = 0; - long count = 0; - - Result rc = BaseFileSystem.OpenDirectory(out IDirectory _, _path, - OpenDirectoryMode.All | OpenDirectoryMode.NoFileSize); - if (rc.IsFailure()) return rc; - - var entry = new DirectoryEntry(); - Span entrySpan = SpanHelpers.AsSpan(ref entry); - - while (true) - { - rc = ParentDirectory.Read(out long baseEntriesRead, entrySpan); - if (rc.IsFailure()) return rc; - - if (baseEntriesRead == 0) break; - - if (CanReturnEntry(entry, IsConcatenationFile(entry))) count++; - } - - entryCount = count; - return Result.Success; - } - - private bool CanReturnEntry(DirectoryEntry entry, bool isConcatFile) - { - return Mode.HasFlag(OpenDirectoryMode.File) && (entry.Type == DirectoryEntryType.File || isConcatFile) || - Mode.HasFlag(OpenDirectoryMode.Directory) && entry.Type == DirectoryEntryType.Directory && !isConcatFile; - } - - private bool IsConcatenationFile(DirectoryEntry entry) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return ConcatenationFileSystem.HasConcatenationFileAttribute(entry.Attributes); - } - else - { - string name = StringUtils.NullTerminatedUtf8ToString(entry.Name); - var fullPath = PathTools.Combine(_path.ToString(), name).ToU8Span(); - - return ParentFileSystem.IsConcatenationFile(fullPath); - } - } - } -} diff --git a/src/LibHac/FsSystem/ConcatenationFile.cs b/src/LibHac/FsSystem/ConcatenationFile.cs deleted file mode 100644 index 7794e0e5..00000000 --- a/src/LibHac/FsSystem/ConcatenationFile.cs +++ /dev/null @@ -1,249 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Runtime.CompilerServices; -using LibHac.Common; -using LibHac.Fs; -using LibHac.Fs.Fsa; -using LibHac.Util; - -namespace LibHac.FsSystem -{ - public class ConcatenationFile : IFile - { - private IFileSystem BaseFileSystem { get; } - private U8String FilePath { get; } - private List Sources { get; } - private long SubFileSize { get; } - private OpenMode Mode { get; } - - internal ConcatenationFile(IFileSystem baseFileSystem, U8Span path, IEnumerable sources, long subFileSize, OpenMode mode) - { - BaseFileSystem = baseFileSystem; - FilePath = path.ToU8String(); - Sources = sources.ToList(); - SubFileSize = subFileSize; - Mode = mode; - - for (int i = 0; i < Sources.Count - 1; i++) - { - Sources[i].GetSize(out long actualSubFileSize).ThrowIfFailure(); - - if (actualSubFileSize != SubFileSize) - { - throw new ArgumentException($"Source file must have size {subFileSize}"); - } - } - } - - protected override Result DoRead(out long bytesRead, long offset, Span destination, - in ReadOption option) - { - UnsafeHelpers.SkipParamInit(out bytesRead); - - long inPos = offset; - int outPos = 0; - - Result rc = DryRead(out long remaining, offset, destination.Length, in option, Mode); - if (rc.IsFailure()) return rc; - - GetSize(out long fileSize).ThrowIfFailure(); - - while (remaining > 0) - { - int fileIndex = GetSubFileIndexFromOffset(offset); - IFile file = Sources[fileIndex]; - long fileOffset = offset - fileIndex * SubFileSize; - - long fileEndOffset = Math.Min((fileIndex + 1) * SubFileSize, fileSize); - int bytesToRead = (int)Math.Min(fileEndOffset - inPos, remaining); - - rc = file.Read(out long subFileBytesRead, fileOffset, destination.Slice(outPos, bytesToRead), option); - if (rc.IsFailure()) return rc; - - outPos += (int)subFileBytesRead; - inPos += subFileBytesRead; - remaining -= subFileBytesRead; - - if (bytesRead < bytesToRead) break; - } - - bytesRead = outPos; - - return Result.Success; - } - - protected override Result DoWrite(long offset, ReadOnlySpan source, in WriteOption option) - { - Result rc = DryWrite(out _, offset, source.Length, in option, Mode); - if (rc.IsFailure()) return rc; - - int inPos = 0; - long outPos = offset; - int remaining = source.Length; - - rc = GetSize(out long fileSize); - if (rc.IsFailure()) return rc; - - while (remaining > 0) - { - int fileIndex = GetSubFileIndexFromOffset(outPos); - IFile file = Sources[fileIndex]; - long fileOffset = outPos - fileIndex * SubFileSize; - - long fileEndOffset = Math.Min((fileIndex + 1) * SubFileSize, fileSize); - int bytesToWrite = (int)Math.Min(fileEndOffset - outPos, remaining); - - rc = file.Write(fileOffset, source.Slice(inPos, bytesToWrite), option); - if (rc.IsFailure()) return rc; - - outPos += bytesToWrite; - inPos += bytesToWrite; - remaining -= bytesToWrite; - } - - if (option.HasFlushFlag()) - { - return Flush(); - } - - return Result.Success; - } - - protected override Result DoFlush() - { - foreach (IFile file in Sources) - { - Result rc = file.Flush(); - if (rc.IsFailure()) return rc; - } - - return Result.Success; - } - - protected override Result DoGetSize(out long size) - { - UnsafeHelpers.SkipParamInit(out size); - - foreach (IFile file in Sources) - { - Result rc = file.GetSize(out long subFileSize); - if (rc.IsFailure()) return rc; - - size += subFileSize; - } - - return Result.Success; - } - - protected override Result DoOperateRange(Span outBuffer, OperationId operationId, long offset, long size, ReadOnlySpan inBuffer) - { - return ResultFs.NotImplemented.Log(); - } - - protected override Result DoSetSize(long size) - { - Result rc = GetSize(out long currentSize); - if (rc.IsFailure()) return rc; - - if (currentSize == size) return Result.Success; - - int currentSubFileCount = QuerySubFileCount(currentSize, SubFileSize); - int newSubFileCount = QuerySubFileCount(size, SubFileSize); - - if (size > currentSize) - { - IFile currentLastSubFile = Sources[currentSubFileCount - 1]; - long newSubFileSize = QuerySubFileSize(currentSubFileCount - 1, size, SubFileSize); - - rc = currentLastSubFile.SetSize(newSubFileSize); - if (rc.IsFailure()) return rc; - - for (int i = currentSubFileCount; i < newSubFileCount; i++) - { - Unsafe.SkipInit(out FsPath newSubFilePath); - - rc = ConcatenationFileSystem.GetSubFilePath(newSubFilePath.Str, FilePath, i); - if (rc.IsFailure()) return rc; - - newSubFileSize = QuerySubFileSize(i, size, SubFileSize); - - rc = BaseFileSystem.CreateFile(newSubFilePath, newSubFileSize, CreateFileOptions.None); - if (rc.IsFailure()) return rc; - - rc = BaseFileSystem.OpenFile(out IFile newSubFile, newSubFilePath, Mode); - if (rc.IsFailure()) return rc; - - Sources.Add(newSubFile); - } - } - else - { - for (int i = currentSubFileCount - 1; i > newSubFileCount - 1; i--) - { - Sources[i].Dispose(); - Sources.RemoveAt(i); - - Unsafe.SkipInit(out FsPath subFilePath); - - rc = ConcatenationFileSystem.GetSubFilePath(subFilePath.Str, FilePath, i); - if (rc.IsFailure()) return rc; - - rc = BaseFileSystem.DeleteFile(subFilePath); - if (rc.IsFailure()) return rc; - } - - long newLastFileSize = QuerySubFileSize(newSubFileCount - 1, size, SubFileSize); - - rc = Sources[newSubFileCount - 1].SetSize(newLastFileSize); - if (rc.IsFailure()) return rc; - } - - return Result.Success; - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - foreach (IFile file in Sources) - { - file?.Dispose(); - } - - Sources.Clear(); - } - } - - private int GetSubFileIndexFromOffset(long offset) - { - return (int)(offset / SubFileSize); - } - - private static int QuerySubFileCount(long size, long subFileSize) - { - Debug.Assert(size >= 0); - Debug.Assert(subFileSize > 0); - - if (size == 0) return 1; - - return (int)BitUtil.DivideUp(size, subFileSize); - } - - private static long QuerySubFileSize(int subFileIndex, long totalSize, long subFileSize) - { - int subFileCount = QuerySubFileCount(totalSize, subFileSize); - - Debug.Assert(subFileIndex < subFileCount); - - if (subFileIndex + 1 == subFileCount) - { - long remainder = totalSize % subFileSize; - return remainder == 0 ? subFileSize : remainder; - } - - return subFileSize; - } - } -} diff --git a/src/LibHac/FsSystem/ConcatenationFileSystem.cs b/src/LibHac/FsSystem/ConcatenationFileSystem.cs index 5c5ddc34..abd80351 100644 --- a/src/LibHac/FsSystem/ConcatenationFileSystem.cs +++ b/src/LibHac/FsSystem/ConcatenationFileSystem.cs @@ -3,11 +3,12 @@ using System.Buffers; using System.Buffers.Text; using System.Collections.Generic; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using LibHac.Common; +using LibHac.Diag; using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.Util; +using static LibHac.FsSystem.Utility12; namespace LibHac.FsSystem { @@ -17,193 +18,694 @@ namespace LibHac.FsSystem /// /// This filesystem is mainly used to allow storing large files on filesystems that have low /// limits on file size such as FAT filesystems. The underlying base filesystem must have - /// support for the "Archive" file attribute found in FAT or NTFS filesystems. - /// + /// support for the "Archive" file attribute found in FAT or NTFS filesystems.
+ ///
/// A may contain both standard files or Concatenation files. /// If a directory has the archive attribute set, its contents will be concatenated and treated /// as a single file. These sub-files must follow the naming scheme "00", "01", "02", ... - /// Each sub-file except the final one must have the size that was specified + /// Each sub-file except the final one must have the size that was specified /// at the creation of the . + ///
Based on FS 12.0.3 (nnSdk 12.3.1) ///
public class ConcatenationFileSystem : IFileSystem { - private const long DefaultSubFileSize = 0xFFFF0000; // Hard-coded value used by FS - private IAttributeFileSystem BaseFileSystem { get; } - private long SubFileSize { get; } - - /// - /// Initializes a new . - /// - /// The base for the - /// new . - public ConcatenationFileSystem(IAttributeFileSystem baseFileSystem) : this(baseFileSystem, DefaultSubFileSize) { } - - /// - /// Initializes a new . - /// - /// The base for the - /// new . - /// The size of each sub-file. Once a file exceeds this size, a new sub-file will be created - public ConcatenationFileSystem(IAttributeFileSystem baseFileSystem, long subFileSize) + private class ConcatenationFile : IFile { - BaseFileSystem = baseFileSystem; - SubFileSize = subFileSize; - } + private OpenMode _mode; + private List _files; + private long _internalFileSize; + private IFileSystem _baseFileSystem; + private Path.Stored _path; - // .NET Core on platforms other than Windows doesn't support getting the - // archive flag in FAT file systems. Try to work around that for now for reading, - // but writing still won't work properly on those platforms - internal bool IsConcatenationFile(U8Span path) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + public ConcatenationFile(OpenMode mode, ref List internalFiles, long internalFileSize, IFileSystem baseFileSystem) { - Result rc = BaseFileSystem.GetFileAttributes(out NxFileAttributes attributes, path); - if (rc.IsFailure()) return false; - - return HasConcatenationFileAttribute(attributes); - } - else - { - return IsConcatenationFileHeuristic(path); - } - } - - private bool IsConcatenationFileHeuristic(U8Span path) - { - // Check if the path is a directory - Result getTypeResult = BaseFileSystem.GetEntryType(out DirectoryEntryType pathType, path); - if (getTypeResult.IsFailure() || pathType != DirectoryEntryType.Directory) return false; - - // Check if the directory contains at least one subfile - getTypeResult = BaseFileSystem.GetEntryType(out DirectoryEntryType subFileType, PathTools.Combine(path.ToString(), "00").ToU8Span()); - if (getTypeResult.IsFailure() || subFileType != DirectoryEntryType.File) return false; - - // Make sure the directory contains no subdirectories - Result rc = BaseFileSystem.OpenDirectory(out IDirectory dir, path, OpenDirectoryMode.Directory); - if (rc.IsFailure()) return false; - - rc = dir.GetEntryCount(out long subDirCount); - if (rc.IsFailure() || subDirCount > 0) return false; - - // Should be enough checks to avoid most false positives. Maybe - return true; - } - - internal static bool HasConcatenationFileAttribute(NxFileAttributes attributes) - { - return (attributes & NxFileAttributes.Directory) != 0 && (attributes & NxFileAttributes.Archive) != 0; - } - - private Result SetConcatenationFileAttribute(U8Span path) - { - return BaseFileSystem.SetFileAttributes(path, NxFileAttributes.Archive); - } - - protected override Result DoCreateDirectory(in Path path) - { - var parent = new U8Span(PathTools.GetParentDirectory(path)); - - if (IsConcatenationFile(parent)) - { - // Cannot create a directory inside of a concatenation file - return ResultFs.PathNotFound.Log(); + _mode = mode; + _files = Shared.Move(ref internalFiles); + _internalFileSize = internalFileSize; + _baseFileSystem = baseFileSystem; + _path = new Path.Stored(); } - return BaseFileSystem.CreateDirectory(path); - } - - protected override Result DoCreateFile(in Path path, long size, CreateFileOptions option) - { - CreateFileOptions newOptions = option & ~CreateFileOptions.CreateConcatenationFile; - - if (!option.HasFlag(CreateFileOptions.CreateConcatenationFile)) + protected override void Dispose(bool disposing) { - return BaseFileSystem.CreateFile(path, size, newOptions); + _path.Dispose(); + + if (disposing) + { + foreach (IFile file in _files) + { + file?.Dispose(); + } + + _files.Clear(); + } } - // A concatenation file directory can't contain normal files - ReadOnlySpan parentDir = PathTools.GetParentDirectory(path); - - if (IsConcatenationFile(new U8Span(parentDir))) + public Result Initialize(in Path path) { - // Cannot create a file inside of a concatenation file - return ResultFs.PathNotFound.Log(); + return _path.Initialize(in path); } - Result rc = BaseFileSystem.CreateDirectory(path, NxFileAttributes.Archive); - if (rc.IsFailure()) return rc; - - long remaining = size; - - for (int i = 0; remaining > 0; i++) + private int GetInternalFileIndex(long offset) { - long fileSize = Math.Min(SubFileSize, remaining); + return (int)(offset / _internalFileSize); + } - Unsafe.SkipInit(out FsPath fileName); + private int GetInternalFileCount(long size) + { + if (size == 0) + return 1; - rc = GetSubFilePath(fileName.Str, path, i); + return (int)BitUtil.DivideUp(size, _internalFileSize); + } + + private long GetInternalFileSize(long offset, int tailIndex) + { + int index = GetInternalFileIndex(offset); + + if (tailIndex < index) + return _internalFileSize; + + Assert.SdkAssert(index == tailIndex); + + return offset % _internalFileSize; + } + + protected override Result DoRead(out long bytesRead, long offset, Span destination, + in ReadOption option) + { + UnsafeHelpers.SkipParamInit(out bytesRead); + + long fileOffset = offset; + int bufferOffset = 0; + + Result rc = DryRead(out long remaining, offset, destination.Length, in option, _mode); if (rc.IsFailure()) return rc; - Result createSubFileResult = BaseFileSystem.CreateFile(fileName, fileSize, CreateFileOptions.None); - - if (createSubFileResult.IsFailure()) + while (remaining > 0) { - BaseFileSystem.DeleteDirectoryRecursively(path); - return createSubFileResult; + int fileIndex = GetInternalFileIndex(fileOffset); + long internalFileRemaining = _internalFileSize - GetInternalFileSize(fileOffset, fileIndex); + long internalFileOffset = fileOffset - _internalFileSize * fileIndex; + + int bytesToRead = (int)Math.Min(remaining, internalFileRemaining); + + Assert.SdkAssert(fileIndex < _files.Count); + + rc = _files[fileIndex].Read(out long internalFileBytesRead, internalFileOffset, + destination.Slice(bufferOffset, bytesToRead), in option); + if (rc.IsFailure()) return rc; + + remaining -= internalFileBytesRead; + bufferOffset += (int)internalFileBytesRead; + fileOffset += internalFileBytesRead; + + if (internalFileBytesRead < bytesToRead) + break; } - remaining -= fileSize; + bytesRead = bufferOffset; + return Result.Success; } + protected override Result DoWrite(long offset, ReadOnlySpan source, in WriteOption option) + { + Result rc = DryWrite(out bool needsAppend, offset, source.Length, in option, _mode); + if (rc.IsFailure()) return rc; + + if (source.Length > 0 && needsAppend) + { + rc = SetSize(offset + source.Length); + if (rc.IsFailure()) return rc; + } + + int remaining = source.Length; + int bufferOffset = 0; + long fileOffset = offset; + + // No need to send the flush option to the internal files. We'll flush them after all the writes are done. + var internalFileOption = new WriteOption(option.Flags & ~WriteOptionFlag.Flush); + + while (remaining > 0) + { + int fileIndex = GetInternalFileIndex(fileOffset); + long internalFileRemaining = _internalFileSize - GetInternalFileSize(fileOffset, fileIndex); + long internalFileOffset = fileOffset - _internalFileSize * fileIndex; + + int bytesToWrite = (int)Math.Min(remaining, internalFileRemaining); + + Assert.SdkAssert(fileIndex < _files.Count); + + rc = _files[fileIndex].Write(internalFileOffset, source.Slice(bufferOffset, bytesToWrite), + in internalFileOption); + if (rc.IsFailure()) return rc; + + remaining -= bytesToWrite; + bufferOffset += bytesToWrite; + fileOffset += bytesToWrite; + } + + if (option.HasFlushFlag()) + { + rc = Flush(); + if (rc.IsFailure()) return rc; + } + + return Result.Success; + } + + protected override Result DoFlush() + { + if (!_mode.HasFlag(OpenMode.Write)) + return Result.Success; + + foreach (IFile file in _files) + { + Result rc = file.Flush(); + if (rc.IsFailure()) return rc; + } + + return Result.Success; + } + + protected override Result DoSetSize(long size) + { + Result rc = DrySetSize(size, _mode); + if (rc.IsFailure()) return rc; + + rc = GetSize(out long currentSize); + if (rc.IsFailure()) return rc; + + if (currentSize == size) return Result.Success; + + int currentTailIndex = GetInternalFileCount(currentSize) - 1; + int newTailIndex = GetInternalFileCount(size) - 1; + + var internalFilePath = new Path(); + rc = internalFilePath.Initialize(in _path); + if (rc.IsFailure()) return rc; + + if (size > currentSize) + { + rc = _files[currentTailIndex].SetSize(GetInternalFileSize(size, currentTailIndex)); + if (rc.IsFailure()) return rc; + + for (int i = currentTailIndex + 1; i < newTailIndex; i++) + { + rc = AppendInternalFilePath(ref internalFilePath, i); + if (rc.IsFailure()) return rc; + + rc = _baseFileSystem.CreateFile(in internalFilePath, GetInternalFileSize(size, i), + CreateFileOptions.None); + if (rc.IsFailure()) return rc; + + rc = _baseFileSystem.OpenFile(out IFile newInternalFile, in internalFilePath, _mode); + if (rc.IsFailure()) return rc; + + _files.Add(newInternalFile); + + rc = internalFilePath.RemoveChild(); + if (rc.IsFailure()) return rc; + } + } + else + { + for (int i = currentTailIndex - 1; i > newTailIndex; i--) + { + _files[i].Dispose(); + _files.RemoveAt(i); + + rc = AppendInternalFilePath(ref internalFilePath, i); + if (rc.IsFailure()) return rc; + + rc = _baseFileSystem.DeleteFile(in internalFilePath); + if (rc.IsFailure()) return rc; + + rc = internalFilePath.RemoveChild(); + if (rc.IsFailure()) return rc; + } + + rc = _files[newTailIndex].SetSize(GetInternalFileSize(size, newTailIndex)); + if (rc.IsFailure()) return rc; + } + + internalFilePath.Dispose(); + return Result.Success; + } + + protected override Result DoGetSize(out long size) + { + UnsafeHelpers.SkipParamInit(out size); + + long totalSize = 0; + + foreach (IFile file in _files) + { + Result rc = file.GetSize(out long internalFileSize); + if (rc.IsFailure()) return rc; + + totalSize += internalFileSize; + } + + size = totalSize; + return Result.Success; + } + + protected override Result DoOperateRange(Span outBuffer, OperationId operationId, long offset, long size, + ReadOnlySpan inBuffer) + { + if (operationId == OperationId.InvalidateCache) + { + if (!_mode.HasFlag(OpenMode.Read)) + return ResultFs.ReadUnpermitted.Log(); + + var closure = new OperateRangeClosure(); + closure.OutBuffer = outBuffer; + closure.InBuffer = inBuffer; + closure.OperationId = operationId; + + Result rc = DoOperateRangeImpl(offset, size, InvalidateCacheImpl, ref closure); + if (rc.IsFailure()) return rc; + } + else if (operationId == OperationId.QueryRange) + { + if (outBuffer.Length != Unsafe.SizeOf()) + return ResultFs.InvalidSize.Log(); + + var closure = new OperateRangeClosure(); + closure.InBuffer = inBuffer; + closure.OperationId = operationId; + closure.InfoMerged.Clear(); + + Result rc = DoOperateRangeImpl(offset, size, QueryRangeImpl, ref closure); + if (rc.IsFailure()) return rc; + + SpanHelpers.AsByteSpan(ref closure.InfoMerged).CopyTo(outBuffer); + } + else + { + return ResultFs.UnsupportedOperateRangeForConcatenationFile.Log(); + } + + return Result.Success; + + static Result InvalidateCacheImpl(IFile file, long offset, long size, ref OperateRangeClosure closure) + { + return file.OperateRange(closure.OutBuffer, closure.OperationId, offset, size, closure.InBuffer); + } + + static Result QueryRangeImpl(IFile file, long offset, long size, ref OperateRangeClosure closure) + { + Unsafe.SkipInit(out QueryRangeInfo infoEntry); + + Result rc = file.OperateRange(SpanHelpers.AsByteSpan(ref infoEntry), closure.OperationId, offset, size, + closure.InBuffer); + if (rc.IsFailure()) return rc; + + closure.InfoMerged.Merge(in infoEntry); + return Result.Success; + } + } + + private Result DoOperateRangeImpl(long offset, long size, OperateRangeTask func, + ref OperateRangeClosure closure) + { + if (offset < 0) + return ResultFs.OutOfRange.Log(); + + Result rc = GetSize(out long currentSize); + if (rc.IsFailure()) return rc; + + if (offset > currentSize) + return ResultFs.OutOfRange.Log(); + + long currentOffset = offset; + long availableSize = currentSize - offset; + long remaining = Math.Min(size, availableSize); + + while (remaining > 0) + { + int fileIndex = GetInternalFileIndex(currentOffset); + long internalFileRemaining = _internalFileSize - GetInternalFileSize(currentOffset, fileIndex); + long internalFileOffset = currentOffset - _internalFileSize * fileIndex; + + long sizeToOperate = Math.Min(remaining, internalFileRemaining); + + Assert.SdkAssert(fileIndex < _files.Count); + + rc = func(_files[fileIndex], internalFileOffset, sizeToOperate, ref closure); + if (rc.IsFailure()) return rc; + + remaining -= sizeToOperate; + currentOffset += sizeToOperate; + } + + return Result.Success; + } + + private delegate Result OperateRangeTask(IFile file, long offset, long size, ref OperateRangeClosure closure); + + private ref struct OperateRangeClosure + { + public Span OutBuffer; + public ReadOnlySpan InBuffer; + public OperationId OperationId; + public QueryRangeInfo InfoMerged; + } + } + + private class ConcatenationDirectory : IDirectory + { + private OpenDirectoryMode _mode; + private IDirectory _baseDirectory; + private Path.Stored _path; + private IFileSystem _baseFileSystem; + private ConcatenationFileSystem _concatenationFileSystem; + + public ConcatenationDirectory(OpenDirectoryMode mode, IDirectory baseDirectory, + ConcatenationFileSystem concatFileSystem, IFileSystem baseFileSystem) + { + _mode = mode; + _baseDirectory = baseDirectory; + _baseFileSystem = baseFileSystem; + _concatenationFileSystem = concatFileSystem; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _path.Dispose(); + _baseDirectory.Dispose(); + } + + base.Dispose(disposing); + } + + public Result Initialize(in Path path) + { + Result rc = _path.Initialize(in path); + if (rc.IsFailure()) return rc; + + return Result.Success; + } + + protected override Result DoRead(out long entriesRead, Span entryBuffer) + { + UnsafeHelpers.SkipParamInit(out entriesRead); + + Unsafe.SkipInit(out DirectoryEntry entry); + int readCountTotal = 0; + + while (readCountTotal < entryBuffer.Length) + { + Result rc = _baseDirectory.Read(out long readCount, SpanHelpers.AsSpan(ref entry)); + if (rc.IsFailure()) return rc; + + if (readCount == 0) + break; + + if (!IsReadTarget(in entry)) + continue; + + if (IsConcatenationFileAttribute(entry.Attributes)) + { + entry.Type = DirectoryEntryType.File; + + if (!_mode.HasFlag(OpenDirectoryMode.NoFileSize)) + { + var internalFilePath = new Path(); + rc = internalFilePath.Initialize(in _path); + if (rc.IsFailure()) return rc; + + rc = internalFilePath.AppendChild(entry.Name); + if (rc.IsFailure()) return rc; + + rc = _concatenationFileSystem.GetFileSize(out entry.Size, in internalFilePath); + if (rc.IsFailure()) return rc; + + internalFilePath.Dispose(); + } + } + + entry.Attributes = NxFileAttributes.None; + entryBuffer[readCountTotal++] = entry; + } + + entriesRead = readCountTotal; + return Result.Success; + } + + protected override Result DoGetEntryCount(out long entryCount) + { + UnsafeHelpers.SkipParamInit(out entryCount); + + Unsafe.SkipInit(out DirectoryEntry entry); + IDirectory directory = null; + + try + { + Path path = _path.GetPath(); + + Result rc = _baseFileSystem.OpenDirectory(out directory, in path, + OpenDirectoryMode.All | OpenDirectoryMode.NoFileSize); + if (rc.IsFailure()) return rc; + + long entryCountTotal = 0; + + while (true) + { + directory.Read(out long readCount, SpanHelpers.AsSpan(ref entry)); + if (rc.IsFailure()) return rc; + + if (readCount == 0) + break; + + if (IsReadTarget(in entry)) + entryCountTotal++; + } + + entryCount = entryCountTotal; + return Result.Success; + } + finally + { + directory?.Dispose(); + } + } + + private bool IsReadTarget(in DirectoryEntry entry) + { + bool hasConcatAttribute = IsConcatenationFileAttribute(entry.Attributes); + + return _mode.HasFlag(OpenDirectoryMode.File) && (entry.Type == DirectoryEntryType.File || hasConcatAttribute) || + _mode.HasFlag(OpenDirectoryMode.Directory) && entry.Type == DirectoryEntryType.Directory && !hasConcatAttribute; + } + } + + public static readonly long DefaultInternalFileSize = 0xFFFF0000; // Hard-coded value used by FS + + private IAttributeFileSystem _baseFileSystem; + private long _InternalFileSize; + + /// + /// Initializes a new with an internal file size of . + /// + /// The base for the + /// new . + public ConcatenationFileSystem(IAttributeFileSystem baseFileSystem) : this(baseFileSystem, DefaultInternalFileSize) { } + + /// + /// Initializes a new . + /// + /// The base for the + /// new . + /// The size of each internal file. Once a file exceeds this size, a new internal file will be created + public ConcatenationFileSystem(IAttributeFileSystem baseFileSystem, long internalFileSize) + { + _baseFileSystem = baseFileSystem; + _InternalFileSize = internalFileSize; + } + + public override void Dispose() + { + _baseFileSystem?.Dispose(); + _baseFileSystem = null; + + base.Dispose(); + } + + private static ReadOnlySpan RootPath => new[] { (byte)'/' }; + + private static Result AppendInternalFilePath(ref Path path, int index) + { + // Use an int as the buffer instead of a stackalloc byte[3] to workaround CS8350. + // Path.AppendChild will not save the span passed to it so this should be safe. + int bufferInt = 0; + Utf8Formatter.TryFormat(index, SpanHelpers.AsByteSpan(ref bufferInt), out _, new StandardFormat('d', 2)); + + return path.AppendChild(SpanHelpers.AsByteSpan(ref bufferInt)); + } + + private static Result GenerateInternalFilePath(ref Path outPath, int index, in Path basePath) + { + Result rc = outPath.Initialize(in basePath); + if (rc.IsFailure()) return rc; + + rc = AppendInternalFilePath(ref outPath, index); + if (rc.IsFailure()) return rc; + return Result.Success; } - protected override Result DoDeleteDirectory(in Path path) + private static Result GenerateParentPath(ref Path outParentPath, in Path path) { - if (IsConcatenationFile(path)) - { + if (path == RootPath) return ResultFs.PathNotFound.Log(); - } - return BaseFileSystem.DeleteDirectory(path); - } - - protected override Result DoDeleteDirectoryRecursively(in Path path) - { - if (IsConcatenationFile(path)) return ResultFs.PathNotFound.Log(); - - return BaseFileSystem.DeleteDirectoryRecursively(path); - } - - protected override Result DoCleanDirectoryRecursively(in Path path) - { - if (IsConcatenationFile(path)) return ResultFs.PathNotFound.Log(); - - return BaseFileSystem.CleanDirectoryRecursively(path); - } - - protected override Result DoDeleteFile(in Path path) - { - if (!IsConcatenationFile(path)) - { - return BaseFileSystem.DeleteFile(path); - } - - Result rc = GetSubFileCount(out int count, path); + Result rc = outParentPath.Initialize(in path); if (rc.IsFailure()) return rc; - for (int i = 0; i < count; i++) - { - Unsafe.SkipInit(out FsPath subFilePath); + rc = outParentPath.RemoveChild(); + if (rc.IsFailure()) return rc; - rc = GetSubFilePath(subFilePath.Str, path, i); + return Result.Success; + } + + private static bool IsConcatenationFileAttribute(NxFileAttributes attribute) + { + return attribute.HasFlag(NxFileAttributes.Directory | NxFileAttributes.Archive); + } + + private bool IsConcatenationFile(in Path path) + { + Result rc = _baseFileSystem.GetFileAttributes(out NxFileAttributes attribute, in path); + if (rc.IsFailure()) + return false; + + return IsConcatenationFileAttribute(attribute); + } + + private Result GetInternalFileCount(out int count, in Path path) + { + UnsafeHelpers.SkipParamInit(out count); + + var internalFilePath = new Path(); + Result rc = internalFilePath.Initialize(in path); + if (rc.IsFailure()) return rc; + + for (int i = 0; ; i++) + { + rc = AppendInternalFilePath(ref internalFilePath, i); if (rc.IsFailure()) return rc; - rc = BaseFileSystem.DeleteFile(subFilePath); + rc = _baseFileSystem.GetEntryType(out _, in internalFilePath); + if (rc.IsFailure()) + { + // We've passed the last internal file of the concatenation file + // once the next internal file doesn't exist. + if (ResultFs.PathNotFound.Includes(rc)) + { + count = i; + internalFilePath.Dispose(); + return Result.Success; + } + + return rc; + } + + rc = internalFilePath.RemoveChild(); if (rc.IsFailure()) return rc; } + } - return BaseFileSystem.DeleteDirectory(path); + protected override Result DoGetEntryType(out DirectoryEntryType entryType, in Path path) + { + if (IsConcatenationFile(in path)) + { + entryType = DirectoryEntryType.File; + return Result.Success; + } + + return _baseFileSystem.GetEntryType(out entryType, path); + } + + protected override Result DoGetFreeSpaceSize(out long freeSpace, in Path path) + { + return _baseFileSystem.GetFreeSpaceSize(out freeSpace, path); + } + + protected override Result DoGetTotalSpaceSize(out long totalSpace, in Path path) + { + return _baseFileSystem.GetTotalSpaceSize(out totalSpace, path); + } + + protected override Result DoGetFileTimeStampRaw(out FileTimeStampRaw timeStamp, in Path path) + { + return _baseFileSystem.GetFileTimeStampRaw(out timeStamp, path); + } + + protected override Result DoFlush() + { + return _baseFileSystem.Flush(); + } + + protected override Result DoOpenFile(out IFile file, in Path path, OpenMode mode) + { + UnsafeHelpers.SkipParamInit(out file); + + if (!IsConcatenationFile(in path)) + { + return _baseFileSystem.OpenFile(out file, in path, mode); + } + + Result rc = GetInternalFileCount(out int fileCount, in path); + if (rc.IsFailure()) return rc; + + ConcatenationFile concatFile = null; + var internalFiles = new List(fileCount); + + var filePath = new Path(); + filePath.Initialize(in path); + if (rc.IsFailure()) return rc; + + try + { + for (int i = 0; i < fileCount; i++) + { + rc = AppendInternalFilePath(ref filePath, i); + if (rc.IsFailure()) return rc; + + rc = _baseFileSystem.OpenFile(out IFile internalFile, in filePath, mode); + if (rc.IsFailure()) return rc; + + internalFiles.Add(internalFile); + + rc = filePath.RemoveChild(); + if (rc.IsFailure()) return rc; + } + + concatFile = new ConcatenationFile(mode, ref internalFiles, _InternalFileSize, _baseFileSystem); + + rc = concatFile.Initialize(in path); + if (rc.IsFailure()) return rc; + + file = Shared.Move(ref concatFile); + return Result.Success; + } + finally + { + filePath.Dispose(); + concatFile?.Dispose(); + + if (internalFiles is not null) + { + foreach (IFile internalFile in internalFiles) + { + internalFile?.Dispose(); + } + } + } } protected override Result DoOpenDirectory(out IDirectory directory, in Path path, OpenDirectoryMode mode) @@ -215,199 +717,271 @@ namespace LibHac.FsSystem return ResultFs.PathNotFound.Log(); } - Result rc = BaseFileSystem.OpenDirectory(out IDirectory parentDir, path, OpenDirectoryMode.All); + Result rc = _baseFileSystem.OpenDirectory(out IDirectory baseDirectory, path, OpenDirectoryMode.All); if (rc.IsFailure()) return rc; - directory = new ConcatenationDirectory(this, BaseFileSystem, parentDir, mode, path); + var concatDirectory = new ConcatenationDirectory(mode, baseDirectory, this, _baseFileSystem); + rc = concatDirectory.Initialize(in path); + if (rc.IsFailure()) return rc; + + directory = concatDirectory; return Result.Success; } - protected override Result DoOpenFile(out IFile file, in Path path, OpenMode mode) + protected override Result DoCreateFile(in Path path, long size, CreateFileOptions option) { - UnsafeHelpers.SkipParamInit(out file); + CreateFileOptions newOption = option & ~CreateFileOptions.CreateConcatenationFile; - if (!IsConcatenationFile(path)) + // Create a normal file if the concatenation file flag isn't set + if (!option.HasFlag(CreateFileOptions.CreateConcatenationFile)) { - return BaseFileSystem.OpenFile(out file, path, mode); + return _baseFileSystem.CreateFile(path, size, newOption); } - Result rc = GetSubFileCount(out int fileCount, path); + var parentPath = new Path(); + Result rc = GenerateParentPath(ref parentPath, in path); if (rc.IsFailure()) return rc; - var files = new List(fileCount); - - for (int i = 0; i < fileCount; i++) - { - Unsafe.SkipInit(out FsPath subFilePath); - - rc = GetSubFilePath(subFilePath.Str, path, i); - if (rc.IsFailure()) return rc; - - rc = BaseFileSystem.OpenFile(out IFile subFile, subFilePath, mode); - if (rc.IsFailure()) return rc; - - files.Add(subFile); - } - - file = new ConcatenationFile(BaseFileSystem, path, files, SubFileSize, mode); - return Result.Success; - } - - protected override Result DoRenameDirectory(in Path currentPath, in Path newPath) - { - if (IsConcatenationFile(currentPath)) + if (IsConcatenationFile(in parentPath)) { + // Cannot create a file inside of a concatenation file return ResultFs.PathNotFound.Log(); } - return BaseFileSystem.RenameDirectory(currentPath, newPath); + rc = _baseFileSystem.CreateDirectory(in path, NxFileAttributes.Archive); + if (rc.IsFailure()) return rc; + + // Handle the empty file case by manually creating a single empty internal file + if (size == 0) + { + var emptyFilePath = new Path(); + rc = GenerateInternalFilePath(ref emptyFilePath, 0, in path); + if (rc.IsFailure()) return rc; + + rc = _baseFileSystem.CreateFile(in emptyFilePath, 0, newOption); + if (rc.IsFailure()) return rc; + + emptyFilePath.Dispose(); + return Result.Success; + } + + long remaining = size; + var filePath = new Path(); + filePath.Initialize(in path); + if (rc.IsFailure()) return rc; + + for (int i = 0; remaining > 0; i++) + { + rc = AppendInternalFilePath(ref filePath, i); + if (rc.IsFailure()) return rc; + + long fileSize = Math.Min(remaining, _InternalFileSize); + Result createInternalFileResult = _baseFileSystem.CreateFile(in filePath, fileSize, newOption); + + // If something goes wrong when creating an internal file, delete all the + // internal files we've created so far and delete the directory. + // This will allow results like insufficient space results to be returned properly. + if (createInternalFileResult.IsFailure()) + { + for (int index = i - 1; index >= 0; index--) + { + rc = GenerateInternalFilePath(ref filePath, index, in path); + if (rc.IsFailure()) return rc; + + rc = _baseFileSystem.DeleteFile(in filePath); + + if (rc.IsFailure()) + break; + } + + _baseFileSystem.DeleteDirectoryRecursively(in path).IgnoreResult(); + return createInternalFileResult; + } + + rc = filePath.RemoveChild(); + if (rc.IsFailure()) return rc; + + remaining -= fileSize; + } + + filePath.Dispose(); + return Result.Success; + } + + protected override Result DoDeleteFile(in Path path) + { + if (!IsConcatenationFile(in path)) + { + return _baseFileSystem.DeleteFile(in path); + } + + Result rc = GetInternalFileCount(out int count, path); + if (rc.IsFailure()) return rc; + + var filePath = new Path(); + rc = filePath.Initialize(in path); + if (rc.IsFailure()) return rc; + + for (int i = count - 1; i >= 0; i--) + { + rc = AppendInternalFilePath(ref filePath, i); + if (rc.IsFailure()) return rc; + + rc = _baseFileSystem.DeleteFile(in filePath); + if (rc.IsFailure()) return rc; + + rc = filePath.RemoveChild(); + if (rc.IsFailure()) return rc; + } + + rc = _baseFileSystem.DeleteDirectoryRecursively(in path); + if (rc.IsFailure()) return rc; + + filePath.Dispose(); + return Result.Success; + } + + protected override Result DoCreateDirectory(in Path path) + { + // Check if the parent path is a concatenation file because we can't create a directory inside one. + var parentPath = new Path(); + Result rc = GenerateParentPath(ref parentPath, in path); + if (rc.IsFailure()) return rc; + + if (IsConcatenationFile(in parentPath)) + return ResultFs.PathNotFound.Log(); + + rc = _baseFileSystem.CreateDirectory(in path); + if (rc.IsFailure()) return rc; + + return Result.Success; + } + + protected override Result DoDeleteDirectory(in Path path) + { + // Make sure the directory isn't a concatenation file. + if (IsConcatenationFile(path)) + return ResultFs.PathNotFound.Log(); + + return _baseFileSystem.DeleteDirectory(path); + } + + private Result CleanDirectoryRecursivelyImpl(in Path path) + { + static Result OnEnterDir(in Path path, in DirectoryEntry entry, ref FsIterationTaskClosure closure) => + Result.Success; + + static Result OnExitDir(in Path path, in DirectoryEntry entry, ref FsIterationTaskClosure closure) => + closure.SourceFileSystem.DeleteDirectory(in path); + + static Result OnFile(in Path path, in DirectoryEntry entry, ref FsIterationTaskClosure closure) => + closure.SourceFileSystem.DeleteFile(in path); + + var closure = new FsIterationTaskClosure(); + closure.SourceFileSystem = this; + + var directoryEntry = new DirectoryEntry(); + return CleanupDirectoryRecursively(this, in path, ref directoryEntry, OnEnterDir, OnExitDir, OnFile, + ref closure); + } + + protected override Result DoDeleteDirectoryRecursively(in Path path) + { + if (IsConcatenationFile(in path)) + return ResultFs.PathNotFound.Log(); + + Result rc = CleanDirectoryRecursivelyImpl(in path); + if (rc.IsFailure()) return rc; + + rc = _baseFileSystem.DeleteDirectory(in path); + if (rc.IsFailure()) return rc; + + return Result.Success; + } + + protected override Result DoCleanDirectoryRecursively(in Path path) + { + if (IsConcatenationFile(in path)) + return ResultFs.PathNotFound.Log(); + + Result rc = CleanDirectoryRecursivelyImpl(in path); + if (rc.IsFailure()) return rc; + + return Result.Success; } protected override Result DoRenameFile(in Path currentPath, in Path newPath) { - if (IsConcatenationFile(currentPath)) + if (IsConcatenationFile(in currentPath)) { - return BaseFileSystem.RenameDirectory(currentPath, newPath); - } - else - { - return BaseFileSystem.RenameFile(currentPath, newPath); - } - } - - protected override Result DoGetEntryType(out DirectoryEntryType entryType, in Path path) - { - if (IsConcatenationFile(path)) - { - entryType = DirectoryEntryType.File; - return Result.Success; + return _baseFileSystem.RenameDirectory(in currentPath, in newPath); } - return BaseFileSystem.GetEntryType(out entryType, path); + return _baseFileSystem.RenameFile(in currentPath, in newPath); } - protected override Result DoGetFreeSpaceSize(out long freeSpace, in Path path) + protected override Result DoRenameDirectory(in Path currentPath, in Path newPath) { - return BaseFileSystem.GetFreeSpaceSize(out freeSpace, path); + if (IsConcatenationFile(in currentPath)) + return ResultFs.PathNotFound.Log(); + + return _baseFileSystem.RenameDirectory(in currentPath, in newPath); } - protected override Result DoGetTotalSpaceSize(out long totalSpace, in Path path) + public Result GetFileSize(out long size, in Path path) { - return BaseFileSystem.GetTotalSpaceSize(out totalSpace, path); - } + UnsafeHelpers.SkipParamInit(out size); - protected override Result DoGetFileTimeStampRaw(out FileTimeStampRaw timeStamp, in Path path) - { - return BaseFileSystem.GetFileTimeStampRaw(out timeStamp, path); - } + var internalFilePath = new Path(); + Result rc = internalFilePath.Initialize(in path); + if (rc.IsFailure()) return rc; - protected override Result DoCommit() - { - return BaseFileSystem.Commit(); - } + long sizeTotal = 0; - protected override Result DoCommitProvisionally(long counter) - { - return BaseFileSystem.CommitProvisionally(counter); - } + for (int i = 0; ; i++) + { + rc = AppendInternalFilePath(ref internalFilePath, i); + if (rc.IsFailure()) return rc; - protected override Result DoFlush() - { - return BaseFileSystem.Flush(); + rc = _baseFileSystem.GetFileSize(out long internalFileSize, in internalFilePath); + if (rc.IsFailure()) + { + // We've passed the last internal file of the concatenation file + // once the next internal file doesn't exist. + if (ResultFs.PathNotFound.Includes(rc)) + { + size = sizeTotal; + internalFilePath.Dispose(); + return Result.Success; + } + + return rc; + } + + rc = internalFilePath.RemoveChild(); + if (rc.IsFailure()) return rc; + + sizeTotal += internalFileSize; + } } protected override Result DoQueryEntry(Span outBuffer, ReadOnlySpan inBuffer, QueryId queryId, in Path path) { - if (queryId != QueryId.SetConcatenationFileAttribute) return ResultFs.UnsupportedQueryEntryForConcatenationFileSystem.Log(); + if (queryId != QueryId.SetConcatenationFileAttribute) + return ResultFs.UnsupportedQueryEntryForConcatenationFileSystem.Log(); - return SetConcatenationFileAttribute(path); + return _baseFileSystem.SetFileAttributes(in path, NxFileAttributes.Archive); } - private Result GetSubFileCount(out int fileCount, U8Span dirPath) + protected override Result DoCommit() { - UnsafeHelpers.SkipParamInit(out fileCount); - - Unsafe.SkipInit(out FsPath buffer); - - int pathLen = StringUtils.Copy(buffer.Str, dirPath); - - // Make sure we have at least 3 bytes for the sub file name - if (pathLen + 3 > PathTools.MaxPathLength) - return ResultFs.TooLongPath.Log(); - - buffer.Str[pathLen] = StringTraits.DirectorySeparator; - Span subFileName = buffer.Str.Slice(pathLen + 1); - - Result rc; - int count; - - for (count = 0; ; count++) - { - Utf8Formatter.TryFormat(count, subFileName, out _, new StandardFormat('D', 2)); - - rc = BaseFileSystem.GetEntryType(out _, buffer); - if (rc.IsFailure()) break; - } - - if (!ResultFs.PathNotFound.Includes(rc)) - { - return rc; - } - - fileCount = count; - return Result.Success; + return _baseFileSystem.Commit(); } - internal static Result GetSubFilePath(Span subFilePathBuffer, ReadOnlySpan basePath, int index) + protected override Result DoCommitProvisionally(long counter) { - int basePathLen = StringUtils.Copy(subFilePathBuffer, basePath); - - // Make sure we have at least 3 bytes for the sub file name - if (basePathLen + 3 > PathTools.MaxPathLength) - return ResultFs.TooLongPath.Log(); - - subFilePathBuffer[basePathLen] = StringTraits.DirectorySeparator; - - Utf8Formatter.TryFormat(index, subFilePathBuffer.Slice(basePathLen + 1), out _, new StandardFormat('D', 2)); - - return Result.Success; - } - - internal Result GetConcatenationFileSize(out long size, ReadOnlySpan path) - { - UnsafeHelpers.SkipParamInit(out size); - Unsafe.SkipInit(out FsPath buffer); - - int pathLen = StringUtils.Copy(buffer.Str, path); - - // Make sure we have at least 3 bytes for the sub file name - if (pathLen + 3 > PathTools.MaxPathLength) - return ResultFs.TooLongPath.Log(); - - buffer.Str[pathLen] = StringTraits.DirectorySeparator; - Span subFileName = buffer.Str.Slice(pathLen + 1); - - Result rc; - long totalSize = 0; - - for (int i = 0; ; i++) - { - Utf8Formatter.TryFormat(i, subFileName, out _, new StandardFormat('D', 2)); - - rc = BaseFileSystem.GetFileSize(out long fileSize, buffer); - if (rc.IsFailure()) break; - - totalSize += fileSize; - } - - if (!ResultFs.PathNotFound.Includes(rc)) - { - return rc; - } - - size = totalSize; - return Result.Success; + return _baseFileSystem.CommitProvisionally(counter); } } } diff --git a/tests/LibHac.Tests/FsSystem/ConcatenationFileSystemTests.cs b/tests/LibHac.Tests/FsSystem/ConcatenationFileSystemTests.cs new file mode 100644 index 00000000..92539021 --- /dev/null +++ b/tests/LibHac.Tests/FsSystem/ConcatenationFileSystemTests.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LibHac.Tests.FsSystem +{ + class ConcatenationFileSystemTests + { + asdf + } +}