diff --git a/src/LibHac/FsSystem/ConcatenationFileSystem.cs b/src/LibHac/FsSystem/ConcatenationFileSystem.cs index 2212404c..2ded58e4 100644 --- a/src/LibHac/FsSystem/ConcatenationFileSystem.cs +++ b/src/LibHac/FsSystem/ConcatenationFileSystem.cs @@ -4,6 +4,7 @@ using System.Buffers.Text; using System.Collections.Generic; using System.Runtime.CompilerServices; using LibHac.Common; +using LibHac.Common.FixedArrays; using LibHac.Diag; using LibHac.Fs; using LibHac.Fs.Fsa; @@ -16,23 +17,24 @@ namespace LibHac.FsSystem; /// An that stores large files as smaller, separate sub-files. /// /// -/// This filesystem is mainly used to allow storing large files on filesystems that have low +/// 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.
-///
-/// A may contain both standard files or Concatenation files. +/// 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 +/// as a single file. These internal files must follow the naming scheme "00", "01", "02", ... +/// Each internal file except the final one must have the internal file size that was specified /// at the creation of the . -///
Based on FS 12.1.0 (nnSdk 12.3.1) +///
+/// Based on FS 12.1.0 (nnSdk 12.3.1) ///
public class ConcatenationFileSystem : IFileSystem { private class ConcatenationFile : IFile { private OpenMode _mode; - private List _files; + private List _fileArray; private long _internalFileSize; private IFileSystem _baseFileSystem; private Path.Stored _path; @@ -40,7 +42,7 @@ public class ConcatenationFileSystem : IFileSystem public ConcatenationFile(OpenMode mode, ref List internalFiles, long internalFileSize, IFileSystem baseFileSystem) { _mode = mode; - _files = Shared.Move(ref internalFiles); + _fileArray = Shared.Move(ref internalFiles); _internalFileSize = internalFileSize; _baseFileSystem = baseFileSystem; _path = new Path.Stored(); @@ -50,19 +52,19 @@ public class ConcatenationFileSystem : IFileSystem { _path.Dispose(); - foreach (IFile file in _files) + foreach (IFile file in _fileArray) { file?.Dispose(); } - _files.Clear(); + _fileArray.Clear(); base.Dispose(); } public Result Initialize(in Path path) { - return _path.Initialize(in path); + return _path.Initialize(in path).Ret(); } private int GetInternalFileIndex(long offset) @@ -96,10 +98,11 @@ public class ConcatenationFileSystem : IFileSystem 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; + if (rc.IsFailure()) return rc.Miss(); + + int bufferOffset = 0; while (remaining > 0) { @@ -109,11 +112,11 @@ public class ConcatenationFileSystem : IFileSystem int bytesToRead = (int)Math.Min(remaining, internalFileRemaining); - Assert.SdkAssert(fileIndex < _files.Count); + Assert.SdkAssert(fileIndex < _fileArray.Count); - rc = _files[fileIndex].Read(out long internalFileBytesRead, internalFileOffset, + rc = _fileArray[fileIndex].Read(out long internalFileBytesRead, internalFileOffset, destination.Slice(bufferOffset, bytesToRead), in option); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); remaining -= internalFileBytesRead; bufferOffset += (int)internalFileBytesRead; @@ -130,12 +133,12 @@ public class ConcatenationFileSystem : IFileSystem 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 (rc.IsFailure()) return rc.Miss(); if (source.Length > 0 && needsAppend) { rc = SetSize(offset + source.Length); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); } int remaining = source.Length; @@ -153,11 +156,11 @@ public class ConcatenationFileSystem : IFileSystem int bytesToWrite = (int)Math.Min(remaining, internalFileRemaining); - Assert.SdkAssert(fileIndex < _files.Count); + Assert.SdkAssert(fileIndex < _fileArray.Count); - rc = _files[fileIndex].Write(internalFileOffset, source.Slice(bufferOffset, bytesToWrite), + rc = _fileArray[fileIndex].Write(internalFileOffset, source.Slice(bufferOffset, bytesToWrite), in internalFileOption); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); remaining -= bytesToWrite; bufferOffset += bytesToWrite; @@ -167,7 +170,7 @@ public class ConcatenationFileSystem : IFileSystem if (option.HasFlushFlag()) { rc = Flush(); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); } return Result.Success; @@ -178,10 +181,12 @@ public class ConcatenationFileSystem : IFileSystem if (!_mode.HasFlag(OpenMode.Write)) return Result.Success; - foreach (IFile file in _files) + for (int index = 0; index < _fileArray.Count; index++) { - Result rc = file.Flush(); - if (rc.IsFailure()) return rc; + Assert.SdkNotNull(_fileArray[index]); + + Result rc = _fileArray[index].Flush(); + if (rc.IsFailure()) return rc.Miss(); } return Result.Success; @@ -190,10 +195,10 @@ public class ConcatenationFileSystem : IFileSystem protected override Result DoSetSize(long size) { Result rc = DrySetSize(size, _mode); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); rc = GetSize(out long currentSize); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); if (currentSize == size) return Result.Success; @@ -202,51 +207,51 @@ public class ConcatenationFileSystem : IFileSystem using var internalFilePath = new Path(); rc = internalFilePath.Initialize(in _path); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); if (size > currentSize) { - rc = _files[currentTailIndex].SetSize(GetInternalFileSize(size, currentTailIndex)); - if (rc.IsFailure()) return rc; + rc = _fileArray[currentTailIndex].SetSize(GetInternalFileSize(size, currentTailIndex)); + if (rc.IsFailure()) return rc.Miss(); - for (int i = currentTailIndex + 1; i < newTailIndex; i++) + for (int i = currentTailIndex + 1; i <= newTailIndex; i++) { rc = AppendInternalFilePath(ref internalFilePath.Ref(), i); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); rc = _baseFileSystem.CreateFile(in internalFilePath, GetInternalFileSize(size, i), CreateFileOptions.None); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); using var newInternalFile = new UniqueRef(); rc = _baseFileSystem.OpenFile(ref newInternalFile.Ref(), in internalFilePath, _mode); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); - _files.Add(newInternalFile.Release()); + _fileArray.Add(newInternalFile.Release()); rc = internalFilePath.RemoveChild(); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); } } else { - for (int i = currentTailIndex - 1; i > newTailIndex; i--) + for (int i = currentTailIndex; i > newTailIndex; i--) { - _files[i].Dispose(); - _files.RemoveAt(i); + _fileArray[i].Dispose(); + _fileArray.RemoveAt(i); rc = AppendInternalFilePath(ref internalFilePath.Ref(), i); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); rc = _baseFileSystem.DeleteFile(in internalFilePath); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); rc = internalFilePath.RemoveChild(); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); } - rc = _files[newTailIndex].SetSize(GetInternalFileSize(size, newTailIndex)); - if (rc.IsFailure()) return rc; + rc = _fileArray[newTailIndex].SetSize(GetInternalFileSize(size, newTailIndex)); + if (rc.IsFailure()) return rc.Miss(); } return Result.Success; @@ -258,10 +263,10 @@ public class ConcatenationFileSystem : IFileSystem long totalSize = 0; - foreach (IFile file in _files) + foreach (IFile file in _fileArray) { Result rc = file.GetSize(out long internalFileSize); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); totalSize += internalFileSize; } @@ -273,44 +278,43 @@ public class ConcatenationFileSystem : IFileSystem protected override Result DoOperateRange(Span outBuffer, OperationId operationId, long offset, long size, ReadOnlySpan inBuffer) { - if (operationId == OperationId.InvalidateCache) + switch (operationId) { - if (!_mode.HasFlag(OpenMode.Read)) - return ResultFs.ReadUnpermitted.Log(); + case OperationId.InvalidateCache: + { + if (!_mode.HasFlag(OpenMode.Read)) + return ResultFs.ReadUnpermitted.Log(); - var closure = new OperateRangeClosure(); - closure.OutBuffer = outBuffer; - closure.InBuffer = inBuffer; - closure.OperationId = operationId; + 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; + return DoOperateRangeImpl(offset, size, InvalidateCacheImpl, ref closure).Ret(); + } + case 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.Miss(); + + SpanHelpers.AsByteSpan(ref closure.InfoMerged).CopyTo(outBuffer); + return Result.Success; + } + default: + return ResultFs.UnsupportedOperateRangeForConcatenationFile.Log(); } - 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); + return file.OperateRange(closure.OutBuffer, closure.OperationId, offset, size, closure.InBuffer).Ret(); } static Result QueryRangeImpl(IFile file, long offset, long size, ref OperateRangeClosure closure) @@ -319,7 +323,7 @@ public class ConcatenationFileSystem : IFileSystem Result rc = file.OperateRange(SpanHelpers.AsByteSpan(ref infoEntry), closure.OperationId, offset, size, closure.InBuffer); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); closure.InfoMerged.Merge(in infoEntry); return Result.Success; @@ -333,7 +337,7 @@ public class ConcatenationFileSystem : IFileSystem return ResultFs.OutOfRange.Log(); Result rc = GetSize(out long currentSize); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); if (offset > currentSize) return ResultFs.OutOfRange.Log(); @@ -350,10 +354,10 @@ public class ConcatenationFileSystem : IFileSystem long sizeToOperate = Math.Min(remaining, internalFileRemaining); - Assert.SdkAssert(fileIndex < _files.Count); + Assert.SdkAssert(fileIndex < _fileArray.Count); - rc = func(_files[fileIndex], internalFileOffset, sizeToOperate, ref closure); - if (rc.IsFailure()) return rc; + rc = func(_fileArray[fileIndex], internalFileOffset, sizeToOperate, ref closure); + if (rc.IsFailure()) return rc.Miss(); remaining -= sizeToOperate; currentOffset += sizeToOperate; @@ -386,6 +390,7 @@ public class ConcatenationFileSystem : IFileSystem { _mode = mode; _baseDirectory = new UniqueRef(ref baseDirectory); + _path = new Path.Stored(); _baseFileSystem = baseFileSystem; _concatenationFileSystem = concatFileSystem; } @@ -400,10 +405,7 @@ public class ConcatenationFileSystem : IFileSystem public Result Initialize(in Path path) { - Result rc = _path.Initialize(in path); - if (rc.IsFailure()) return rc; - - return Result.Success; + return _path.Initialize(in path).Ret(); } protected override Result DoRead(out long entriesRead, Span entryBuffer) @@ -416,7 +418,7 @@ public class ConcatenationFileSystem : IFileSystem while (readCountTotal < entryBuffer.Length) { Result rc = _baseDirectory.Get.Read(out long readCount, SpanHelpers.AsSpan(ref entry)); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); if (readCount == 0) break; @@ -432,13 +434,13 @@ public class ConcatenationFileSystem : IFileSystem { using var internalFilePath = new Path(); rc = internalFilePath.Initialize(in _path); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); rc = internalFilePath.AppendChild(entry.Name); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); rc = _concatenationFileSystem.GetFileSize(out entry.Size, in internalFilePath); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); } } @@ -461,14 +463,14 @@ public class ConcatenationFileSystem : IFileSystem Result rc = _baseFileSystem.OpenDirectory(ref directory.Ref(), in path, OpenDirectoryMode.All | OpenDirectoryMode.NoFileSize); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); long entryCountTotal = 0; while (true) { directory.Get.Read(out long readCount, SpanHelpers.AsSpan(ref entry)); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); if (readCount == 0) break; @@ -492,15 +494,17 @@ public class ConcatenationFileSystem : IFileSystem public static readonly long DefaultInternalFileSize = 0xFFFF0000; // Hard-coded value used by FS - private IAttributeFileSystem _baseFileSystem; - private long _InternalFileSize; + private UniqueRef _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) { } + public ConcatenationFileSystem(ref UniqueRef baseFileSystem) : this(ref baseFileSystem, + DefaultInternalFileSize) + { } /// /// Initializes a new . @@ -508,41 +512,41 @@ public class ConcatenationFileSystem : IFileSystem /// 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) + public ConcatenationFileSystem(ref UniqueRef baseFileSystem, long internalFileSize) { - _baseFileSystem = baseFileSystem; - _InternalFileSize = internalFileSize; + _baseFileSystem = new UniqueRef(ref baseFileSystem); + _internalFileSize = internalFileSize; } public override void Dispose() { - _baseFileSystem?.Dispose(); - _baseFileSystem = null; + _baseFileSystem.Destroy(); base.Dispose(); } private static ReadOnlySpan RootPath => new[] { (byte)'/' }; + /// + /// Appends the two-digit-padded to the given . + /// + /// The to be modified. + /// The index to append to the . + /// : The operation was successful. 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)); + var buffer = new Array3(); + Utf8Formatter.TryFormat(index, buffer.Items, out _, new StandardFormat('d', 2)); - return path.AppendChild(SpanHelpers.AsByteSpan(ref bufferInt)); + return path.AppendChild(buffer.ItemsRo).Ret(); } private static Result GenerateInternalFilePath(ref Path outPath, int index, in Path basePath) { Result rc = outPath.Initialize(in basePath); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); - rc = AppendInternalFilePath(ref outPath, index); - if (rc.IsFailure()) return rc; - - return Result.Success; + return AppendInternalFilePath(ref outPath, index).Ret(); } private static Result GenerateParentPath(ref Path outParentPath, in Path path) @@ -551,12 +555,9 @@ public class ConcatenationFileSystem : IFileSystem return ResultFs.PathNotFound.Log(); Result rc = outParentPath.Initialize(in path); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); - rc = outParentPath.RemoveChild(); - if (rc.IsFailure()) return rc; - - return Result.Success; + return outParentPath.RemoveChild().Ret(); } private static bool IsConcatenationFileAttribute(NxFileAttributes attribute) @@ -566,7 +567,7 @@ public class ConcatenationFileSystem : IFileSystem private bool IsConcatenationFile(in Path path) { - Result rc = _baseFileSystem.GetFileAttributes(out NxFileAttributes attribute, in path); + Result rc = _baseFileSystem.Get.GetFileAttributes(out NxFileAttributes attribute, in path); if (rc.IsFailure()) return false; @@ -579,29 +580,32 @@ public class ConcatenationFileSystem : IFileSystem using var internalFilePath = new Path(); Result rc = internalFilePath.Initialize(in path); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); for (int i = 0; ; i++) { rc = AppendInternalFilePath(ref internalFilePath.Ref(), i); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); - rc = _baseFileSystem.GetEntryType(out _, in internalFilePath); + rc = _baseFileSystem.Get.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)) { + rc.Catch(); count = i; + rc.Handle(); + return Result.Success; } - return rc; + return rc.Miss(); } rc = internalFilePath.RemoveChild(); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); } } @@ -613,67 +617,70 @@ public class ConcatenationFileSystem : IFileSystem return Result.Success; } - return _baseFileSystem.GetEntryType(out entryType, path); + return _baseFileSystem.Get.GetEntryType(out entryType, path).Ret(); } protected override Result DoGetFreeSpaceSize(out long freeSpace, in Path path) { - return _baseFileSystem.GetFreeSpaceSize(out freeSpace, path); + return _baseFileSystem.Get.GetFreeSpaceSize(out freeSpace, path).Ret(); } protected override Result DoGetTotalSpaceSize(out long totalSpace, in Path path) { - return _baseFileSystem.GetTotalSpaceSize(out totalSpace, path); + return _baseFileSystem.Get.GetTotalSpaceSize(out totalSpace, path).Ret(); } protected override Result DoGetFileTimeStampRaw(out FileTimeStampRaw timeStamp, in Path path) { - return _baseFileSystem.GetFileTimeStampRaw(out timeStamp, path); + return _baseFileSystem.Get.GetFileTimeStampRaw(out timeStamp, path).Ret(); } protected override Result DoFlush() { - return _baseFileSystem.Flush(); + return _baseFileSystem.Get.Flush().Ret(); } protected override Result DoOpenFile(ref UniqueRef outFile, in Path path, OpenMode mode) { if (!IsConcatenationFile(in path)) { - return _baseFileSystem.OpenFile(ref outFile, in path, mode); + return _baseFileSystem.Get.OpenFile(ref outFile, in path, mode).Ret(); } Result rc = GetInternalFileCount(out int fileCount, in path); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); + + if (fileCount <= 0) + return ResultFs.ConcatenationFsInvalidInternalFileCount.Log(); var internalFiles = new List(fileCount); using var filePath = new Path(); filePath.Initialize(in path); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); try { for (int i = 0; i < fileCount; i++) { rc = AppendInternalFilePath(ref filePath.Ref(), i); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); using var internalFile = new UniqueRef(); - rc = _baseFileSystem.OpenFile(ref internalFile.Ref(), in filePath, mode); - if (rc.IsFailure()) return rc; + rc = _baseFileSystem.Get.OpenFile(ref internalFile.Ref(), in filePath, mode); + if (rc.IsFailure()) return rc.Miss(); internalFiles.Add(internalFile.Release()); rc = filePath.RemoveChild(); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); } using var concatFile = new UniqueRef( - new ConcatenationFile(mode, ref internalFiles, _InternalFileSize, _baseFileSystem)); + new ConcatenationFile(mode, ref internalFiles, _internalFileSize, _baseFileSystem.Get)); rc = concatFile.Get.Initialize(in path); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); outFile.Set(ref concatFile.Ref()); return Result.Success; @@ -699,13 +706,13 @@ public class ConcatenationFileSystem : IFileSystem } using var baseDirectory = new UniqueRef(); - Result rc = _baseFileSystem.OpenDirectory(ref baseDirectory.Ref(), path, OpenDirectoryMode.All); - if (rc.IsFailure()) return rc; + Result rc = _baseFileSystem.Get.OpenDirectory(ref baseDirectory.Ref(), path, OpenDirectoryMode.All); + if (rc.IsFailure()) return rc.Miss(); using var concatDirectory = new UniqueRef( - new ConcatenationDirectory(mode, ref baseDirectory.Ref(), this, _baseFileSystem)); + new ConcatenationDirectory(mode, ref baseDirectory.Ref(), this, _baseFileSystem.Get)); rc = concatDirectory.Get.Initialize(in path); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); outDirectory.Set(ref concatDirectory.Ref()); return Result.Success; @@ -718,12 +725,12 @@ public class ConcatenationFileSystem : IFileSystem // Create a normal file if the concatenation file flag isn't set if (!option.HasFlag(CreateFileOptions.CreateConcatenationFile)) { - return _baseFileSystem.CreateFile(path, size, newOption); + return _baseFileSystem.Get.CreateFile(path, size, newOption).Ret(); } using var parentPath = new Path(); Result rc = GenerateParentPath(ref parentPath.Ref(), in path); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); if (IsConcatenationFile(in parentPath)) { @@ -731,18 +738,18 @@ public class ConcatenationFileSystem : IFileSystem return ResultFs.PathNotFound.Log(); } - rc = _baseFileSystem.CreateDirectory(in path, NxFileAttributes.Archive); - if (rc.IsFailure()) return rc; + rc = _baseFileSystem.Get.CreateDirectory(in path, NxFileAttributes.Archive); + if (rc.IsFailure()) return rc.Miss(); // Handle the empty file case by manually creating a single empty internal file if (size == 0) { using var emptyFilePath = new Path(); rc = GenerateInternalFilePath(ref emptyFilePath.Ref(), 0, in path); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); - rc = _baseFileSystem.CreateFile(in emptyFilePath, 0, newOption); - if (rc.IsFailure()) return rc; + rc = _baseFileSystem.Get.CreateFile(in emptyFilePath, 0, newOption); + if (rc.IsFailure()) return rc.Miss(); return Result.Success; } @@ -750,38 +757,42 @@ public class ConcatenationFileSystem : IFileSystem long remaining = size; using var filePath = new Path(); filePath.Initialize(in path); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); for (int i = 0; remaining > 0; i++) { rc = AppendInternalFilePath(ref filePath.Ref(), i); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); - long fileSize = Math.Min(remaining, _InternalFileSize); - Result createInternalFileResult = _baseFileSystem.CreateFile(in filePath, fileSize, newOption); + long fileSize = Math.Min(remaining, _internalFileSize); + Result createInternalFileResult = _baseFileSystem.Get.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()) { + createInternalFileResult.Catch(); + for (int index = i - 1; index >= 0; index--) { rc = GenerateInternalFilePath(ref filePath.Ref(), index, in path); - if (rc.IsFailure()) return rc; - - rc = _baseFileSystem.DeleteFile(in filePath); - if (rc.IsFailure()) + { + createInternalFileResult.Handle(); + return rc.Miss(); + } + + if (_baseFileSystem.Get.DeleteFile(in filePath).IsFailure()) break; } - _baseFileSystem.DeleteDirectoryRecursively(in path).IgnoreResult(); - return createInternalFileResult; + _baseFileSystem.Get.DeleteDirectoryRecursively(in path).IgnoreResult(); + return createInternalFileResult.Rethrow(); } rc = filePath.RemoveChild(); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); remaining -= fileSize; } @@ -793,30 +804,30 @@ public class ConcatenationFileSystem : IFileSystem { if (!IsConcatenationFile(in path)) { - return _baseFileSystem.DeleteFile(in path); + return _baseFileSystem.Get.DeleteFile(in path).Ret(); } Result rc = GetInternalFileCount(out int count, path); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); using var filePath = new Path(); rc = filePath.Initialize(in path); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); for (int i = count - 1; i >= 0; i--) { rc = AppendInternalFilePath(ref filePath.Ref(), i); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); - rc = _baseFileSystem.DeleteFile(in filePath); - if (rc.IsFailure()) return rc; + rc = _baseFileSystem.Get.DeleteFile(in filePath); + if (rc.IsFailure()) return rc.Miss(); rc = filePath.RemoveChild(); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); } - rc = _baseFileSystem.DeleteDirectoryRecursively(in path); - if (rc.IsFailure()) return rc; + rc = _baseFileSystem.Get.DeleteDirectoryRecursively(in path); + if (rc.IsFailure()) return rc.Miss(); return Result.Success; } @@ -826,15 +837,12 @@ public class ConcatenationFileSystem : IFileSystem // Check if the parent path is a concatenation file because we can't create a directory inside one. using var parentPath = new Path(); Result rc = GenerateParentPath(ref parentPath.Ref(), in path); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); if (IsConcatenationFile(in parentPath)) return ResultFs.PathNotFound.Log(); - rc = _baseFileSystem.CreateDirectory(in path); - if (rc.IsFailure()) return rc; - - return Result.Success; + return _baseFileSystem.Get.CreateDirectory(in path).Ret(); } protected override Result DoDeleteDirectory(in Path path) @@ -843,7 +851,7 @@ public class ConcatenationFileSystem : IFileSystem if (IsConcatenationFile(path)) return ResultFs.PathNotFound.Log(); - return _baseFileSystem.DeleteDirectory(path); + return _baseFileSystem.Get.DeleteDirectory(path).Ret(); } private Result CleanDirectoryRecursivelyImpl(in Path path) @@ -852,17 +860,17 @@ public class ConcatenationFileSystem : IFileSystem Result.Success; static Result OnExitDir(in Path path, in DirectoryEntry entry, ref FsIterationTaskClosure closure) => - closure.SourceFileSystem.DeleteDirectory(in path); + closure.SourceFileSystem.DeleteDirectory(in path).Ret(); static Result OnFile(in Path path, in DirectoryEntry entry, ref FsIterationTaskClosure closure) => - closure.SourceFileSystem.DeleteFile(in path); + closure.SourceFileSystem.DeleteFile(in path).Ret(); var closure = new FsIterationTaskClosure(); closure.SourceFileSystem = this; var directoryEntry = new DirectoryEntry(); return CleanupDirectoryRecursively(this, in path, ref directoryEntry, OnEnterDir, OnExitDir, OnFile, - ref closure); + ref closure).Ret(); } protected override Result DoDeleteDirectoryRecursively(in Path path) @@ -871,12 +879,9 @@ public class ConcatenationFileSystem : IFileSystem return ResultFs.PathNotFound.Log(); Result rc = CleanDirectoryRecursivelyImpl(in path); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); - rc = _baseFileSystem.DeleteDirectory(in path); - if (rc.IsFailure()) return rc; - - return Result.Success; + return _baseFileSystem.Get.DeleteDirectory(in path).Ret(); } protected override Result DoCleanDirectoryRecursively(in Path path) @@ -884,20 +889,15 @@ public class ConcatenationFileSystem : IFileSystem if (IsConcatenationFile(in path)) return ResultFs.PathNotFound.Log(); - Result rc = CleanDirectoryRecursivelyImpl(in path); - if (rc.IsFailure()) return rc; - - return Result.Success; + return CleanDirectoryRecursivelyImpl(in path).Ret(); } protected override Result DoRenameFile(in Path currentPath, in Path newPath) { if (IsConcatenationFile(in currentPath)) - { - return _baseFileSystem.RenameDirectory(in currentPath, in newPath); - } + return _baseFileSystem.Get.RenameDirectory(in currentPath, in newPath).Ret(); - return _baseFileSystem.RenameFile(in currentPath, in newPath); + return _baseFileSystem.Get.RenameFile(in currentPath, in newPath).Ret(); } protected override Result DoRenameDirectory(in Path currentPath, in Path newPath) @@ -905,7 +905,7 @@ public class ConcatenationFileSystem : IFileSystem if (IsConcatenationFile(in currentPath)) return ResultFs.PathNotFound.Log(); - return _baseFileSystem.RenameDirectory(in currentPath, in newPath); + return _baseFileSystem.Get.RenameDirectory(in currentPath, in newPath).Ret(); } public Result GetFileSize(out long size, in Path path) @@ -914,31 +914,34 @@ public class ConcatenationFileSystem : IFileSystem using var internalFilePath = new Path(); Result rc = internalFilePath.Initialize(in path); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); long sizeTotal = 0; for (int i = 0; ; i++) { rc = AppendInternalFilePath(ref internalFilePath.Ref(), i); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); - rc = _baseFileSystem.GetFileSize(out long internalFileSize, in internalFilePath); + rc = _baseFileSystem.Get.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)) { + rc.Catch(); size = sizeTotal; + rc.Handle(); + return Result.Success; } - return rc; + return rc.Miss(); } rc = internalFilePath.RemoveChild(); - if (rc.IsFailure()) return rc; + if (rc.IsFailure()) return rc.Miss(); sizeTotal += internalFileSize; } @@ -950,16 +953,16 @@ public class ConcatenationFileSystem : IFileSystem if (queryId != QueryId.SetConcatenationFileAttribute) return ResultFs.UnsupportedQueryEntryForConcatenationFileSystem.Log(); - return _baseFileSystem.SetFileAttributes(in path, NxFileAttributes.Archive); + return _baseFileSystem.Get.SetFileAttributes(in path, NxFileAttributes.Archive).Ret(); } protected override Result DoCommit() { - return _baseFileSystem.Commit(); + return _baseFileSystem.Get.Commit().Ret(); } protected override Result DoCommitProvisionally(long counter) { - return _baseFileSystem.CommitProvisionally(counter); + return _baseFileSystem.Get.CommitProvisionally(counter).Ret(); } -} +} \ No newline at end of file diff --git a/src/LibHac/Result.cs b/src/LibHac/Result.cs index fc12ec98..c096ed4b 100644 --- a/src/LibHac/Result.cs +++ b/src/LibHac/Result.cs @@ -132,6 +132,11 @@ public readonly struct Result : IEquatable return this; } + public Result Ret() + { + return this; + } + public bool TryGetResultName(out string name) { IResultNameResolver resolver = NameResolver; @@ -419,4 +424,4 @@ public readonly struct Result : IEquatable { public bool TryResolveName(Result result, out string name); } -} +} \ No newline at end of file diff --git a/src/LibHac/Tools/Fs/SwitchFs.cs b/src/LibHac/Tools/Fs/SwitchFs.cs index 3713029d..0eaf2d74 100644 --- a/src/LibHac/Tools/Fs/SwitchFs.cs +++ b/src/LibHac/Tools/Fs/SwitchFs.cs @@ -42,9 +42,9 @@ public class SwitchFs : IDisposable CreateApplications(); } - public static SwitchFs OpenSdCard(KeySet keySet, IAttributeFileSystem fileSystem) + public static SwitchFs OpenSdCard(KeySet keySet, ref UniqueRef fileSystem) { - var concatFs = new ConcatenationFileSystem(fileSystem); + var concatFs = new ConcatenationFileSystem(ref fileSystem); using var contentDirPath = new LibHac.Fs.Path(); PathFunctions.SetUpFixedPath(ref contentDirPath.Ref(), "/Nintendo/Contents".ToU8String()).ThrowIfFailure(); @@ -56,7 +56,7 @@ public class SwitchFs : IDisposable contentDirFs.Initialize(in contentDirPath).ThrowIfFailure(); AesXtsFileSystem encSaveFs = null; - if (fileSystem.DirectoryExists("/Nintendo/save")) + if (concatFs.DirectoryExists("/Nintendo/save")) { var saveDirFs = new SubdirectoryFileSystem(concatFs); saveDirFs.Initialize(in saveDirPath).ThrowIfFailure(); @@ -69,9 +69,9 @@ public class SwitchFs : IDisposable return new SwitchFs(keySet, encContentFs, encSaveFs); } - public static SwitchFs OpenNandPartition(KeySet keySet, IAttributeFileSystem fileSystem) + public static SwitchFs OpenNandPartition(KeySet keySet, ref UniqueRef fileSystem) { - var concatFs = new ConcatenationFileSystem(fileSystem); + var concatFs = new ConcatenationFileSystem(ref fileSystem); SubdirectoryFileSystem saveDirFs = null; SubdirectoryFileSystem contentDirFs; diff --git a/src/hactoolnet/ProcessSwitchFs.cs b/src/hactoolnet/ProcessSwitchFs.cs index 2eb5a186..5ffe7a51 100644 --- a/src/hactoolnet/ProcessSwitchFs.cs +++ b/src/hactoolnet/ProcessSwitchFs.cs @@ -21,26 +21,26 @@ internal static class ProcessSwitchFs public static void Process(Context ctx) { SwitchFs switchFs; - var baseFs = new LocalFileSystem(ctx.Options.InFile); + using var baseFs = new UniqueRef(new LocalFileSystem(ctx.Options.InFile)); if (Directory.Exists(Path.Combine(ctx.Options.InFile, "Nintendo", "Contents", "registered"))) { ctx.Logger.LogMessage("Treating path as SD card storage"); - switchFs = SwitchFs.OpenSdCard(ctx.KeySet, baseFs); + switchFs = SwitchFs.OpenSdCard(ctx.KeySet, ref baseFs.Ref()); CheckForNcaFolders(ctx, switchFs); } else if (Directory.Exists(Path.Combine(ctx.Options.InFile, "Contents", "registered"))) { ctx.Logger.LogMessage("Treating path as NAND storage"); - switchFs = SwitchFs.OpenNandPartition(ctx.KeySet, baseFs); + switchFs = SwitchFs.OpenNandPartition(ctx.KeySet, ref baseFs.Ref()); CheckForNcaFolders(ctx, switchFs); } else { ctx.Logger.LogMessage("Treating path as a directory of loose NCAs"); - switchFs = SwitchFs.OpenNcaDirectory(ctx.KeySet, baseFs); + switchFs = SwitchFs.OpenNcaDirectory(ctx.KeySet, baseFs.Get); } if (ctx.Options.ListNcas) diff --git a/tests/LibHac.Tests/Fs/IFileSystemTestBase/IFileSystemTests.OpenDirectory.cs b/tests/LibHac.Tests/Fs/IFileSystemTestBase/IFileSystemTests.OpenDirectory.cs index 8d2aff1c..e6147867 100644 --- a/tests/LibHac.Tests/Fs/IFileSystemTestBase/IFileSystemTests.OpenDirectory.cs +++ b/tests/LibHac.Tests/Fs/IFileSystemTestBase/IFileSystemTests.OpenDirectory.cs @@ -19,4 +19,15 @@ public abstract partial class IFileSystemTests Assert.Result(ResultFs.PathNotFound, rc); } -} + + [Fact] + public void OpenDirectory_PathDoesNotExist_ReturnsPathNotFound() + { + IFileSystem fs = CreateFileSystem(); + + using var directory = new UniqueRef(); + Result rc = fs.OpenDirectory(ref directory.Ref(), "/dir", OpenDirectoryMode.All); + + Assert.Result(ResultFs.PathNotFound, rc); + } +} \ No newline at end of file diff --git a/tests/LibHac.Tests/Fs/IFileSystemTestBase/IFileSystemTests.OpenFile.cs b/tests/LibHac.Tests/Fs/IFileSystemTestBase/IFileSystemTests.OpenFile.cs index 35f5f2c5..a50e1d71 100644 --- a/tests/LibHac.Tests/Fs/IFileSystemTestBase/IFileSystemTests.OpenFile.cs +++ b/tests/LibHac.Tests/Fs/IFileSystemTestBase/IFileSystemTests.OpenFile.cs @@ -19,4 +19,15 @@ public abstract partial class IFileSystemTests Assert.Result(ResultFs.PathNotFound, rc); } -} + + [Fact] + public void OpenFile_PathDoesNotExist_ReturnsPathNotFound() + { + IFileSystem fs = CreateFileSystem(); + + using var file = new UniqueRef(); + Result rc = fs.OpenFile(ref file.Ref(), "/file", OpenMode.All); + + Assert.Result(ResultFs.PathNotFound, rc); + } +} \ No newline at end of file diff --git a/tests/LibHac.Tests/FsSystem/ConcatenationFileSystemTests.cs b/tests/LibHac.Tests/FsSystem/ConcatenationFileSystemTests.cs index 0222ef6d..9a4ce758 100644 --- a/tests/LibHac.Tests/FsSystem/ConcatenationFileSystemTests.cs +++ b/tests/LibHac.Tests/FsSystem/ConcatenationFileSystemTests.cs @@ -1,6 +1,592 @@ -namespace LibHac.Tests.FsSystem; +using System; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.Tests.Fs; +using LibHac.Tests.Fs.IFileSystemTestBase; +using LibHac.Tools.Fs; +using Xunit; -public class ConcatenationFileSystemTests +namespace LibHac.Tests.FsSystem; + +public class ConcatenationFileSystemTests : IFileSystemTests { - //asdf -} + private const int InternalFileSize = 0x10000; + protected override IFileSystem CreateFileSystem() + { + return CreateFileSystemInternal().concatFs; + } + + private (InMemoryFileSystem baseFs, ConcatenationFileSystem concatFs) CreateFileSystemInternal() + { + var baseFs = new InMemoryFileSystem(); + + using var uniqueBaseFs = new UniqueRef(baseFs); + var concatFs = new ConcatenationFileSystem(ref uniqueBaseFs.Ref(), InternalFileSize); + + return (baseFs, concatFs); + } + + [Fact] + public void OpenFile_OpenInternalFile_OpensSuccessfully() + { + IFileSystem fs = CreateFileSystem(); + + Assert.Success(fs.CreateFile("/file", InternalFileSize * 3, CreateFileOptions.CreateConcatenationFile)); + + using var file = new UniqueRef(); + Assert.Success(fs.OpenFile(ref file.Ref(), "/file/01", OpenMode.All)); + } + + [Fact] + public void OpenFile_ConcatFileWithNoInternalFiles_ReturnsConcatenationFsInvalidInternalFileCount() + { + (IAttributeFileSystem baseFs, IFileSystem concatFs) = CreateFileSystemInternal(); + + Assert.Success(concatFs.CreateFile("/file", InternalFileSize * 3, CreateFileOptions.CreateConcatenationFile)); + Assert.Success(baseFs.DeleteFile("/file/00")); + + using var file = new UniqueRef(); + Assert.Result(ResultFs.ConcatenationFsInvalidInternalFileCount, concatFs.OpenFile(ref file.Ref(), "/file", OpenMode.All)); + } + + [Fact] + public void OpenDirectory_OpenConcatFile_ReturnsPathNotFound() + { + IFileSystem fs = CreateFileSystem(); + + Assert.Success(fs.CreateFile("/file", InternalFileSize * 3, CreateFileOptions.CreateConcatenationFile)); + + using var dir = new UniqueRef(); + Assert.Result(ResultFs.PathNotFound, fs.OpenDirectory(ref dir.Ref(), "/file", OpenDirectoryMode.All)); + } + + [Fact] + public void CreateFile_ConcatenationFile_GetEntryTypeReturnsFile() + { + IFileSystem concatFs = CreateFileSystem(); + + Assert.Success(concatFs.CreateFile("/file", 0, CreateFileOptions.CreateConcatenationFile)); + + Assert.Success(concatFs.GetEntryType(out DirectoryEntryType type, "/file")); + Assert.Equal(DirectoryEntryType.File, type); + } + + [Fact] + public void CreateFile_EmptyConcatenationFile_BaseDirHasCorrectStructure() + { + (IAttributeFileSystem baseFs, IFileSystem concatFs) = CreateFileSystemInternal(); + + Assert.Success(concatFs.CreateFile("/file", 0, CreateFileOptions.CreateConcatenationFile)); + + // Ensure the directory exists with the archive bit set + Assert.Success(baseFs.GetEntryType(out DirectoryEntryType dirType, "/file")); + Assert.Success(baseFs.GetFileAttributes(out NxFileAttributes dirAttributes, "/file")); + Assert.Equal(DirectoryEntryType.Directory, dirType); + Assert.Equal(NxFileAttributes.Directory | NxFileAttributes.Archive, dirAttributes); + + // Ensure the internal files exist + Assert.Success(baseFs.GetEntryType(out DirectoryEntryType internalFile00Type, "/file/00")); + Assert.Equal(DirectoryEntryType.File, internalFile00Type); + + // Ensure no additional internal files exist + Assert.Result(ResultFs.PathNotFound, baseFs.GetEntryType(out _, "/file/01")); + + // Ensure the internal file sizes are correct + Assert.Success(baseFs.GetFileSize(out long internalFile00Size, "/file/00")); + Assert.Equal(0, internalFile00Size); + } + + [Fact] + public void CreateFile_SizeIsMultipleOfInternalFile_BaseDirHasCorrectStructure() + { + (IAttributeFileSystem baseFs, IFileSystem concatFs) = CreateFileSystemInternal(); + + Assert.Success(concatFs.CreateFile("/file", InternalFileSize * 2, CreateFileOptions.CreateConcatenationFile)); + + // Ensure the directory exists with the archive bit set + Assert.Success(baseFs.GetEntryType(out DirectoryEntryType dirType, "/file")); + Assert.Success(baseFs.GetFileAttributes(out NxFileAttributes dirAttributes, "/file")); + Assert.Equal(DirectoryEntryType.Directory, dirType); + Assert.Equal(NxFileAttributes.Directory | NxFileAttributes.Archive, dirAttributes); + + // Ensure no additional internal files exist + Assert.Result(ResultFs.PathNotFound, baseFs.GetEntryType(out _, "/file/02")); + + // Ensure the internal file sizes are correct + Assert.Success(baseFs.GetFileSize(out long internalFile00Size, "/file/00")); + Assert.Success(baseFs.GetFileSize(out long internalFile01Size, "/file/01")); + Assert.Equal(InternalFileSize, internalFile00Size); + Assert.Equal(InternalFileSize, internalFile01Size); + } + + [Fact] + public void CreateFile_NormalFileInsideConcatFile_CreatesSuccessfully() + { + IFileSystem fs = CreateFileSystem(); + + Assert.Success(fs.CreateFile("/file", 0, CreateFileOptions.CreateConcatenationFile)); + Assert.Success(fs.CreateFile("/file/file", 0)); + + Assert.Success(fs.GetEntryType(out DirectoryEntryType type, "/file/file")); + Assert.Equal(DirectoryEntryType.File, type); + } + + [Fact] + public void CreateFile_ConcatFileInsideConcatFile_ReturnsPathNotFound() + { + IFileSystem fs = CreateFileSystem(); + + Assert.Success(fs.CreateFile("/file", 0, CreateFileOptions.CreateConcatenationFile)); + + Assert.Result(ResultFs.PathNotFound, fs.CreateFile("/file/file", 0, CreateFileOptions.CreateConcatenationFile)); + } + + [Fact] + public void DeleteFile_DeleteConcatFile_DeletesSuccessfully() + { + (IAttributeFileSystem baseFs, IFileSystem concatFs) = CreateFileSystemInternal(); + + Assert.Success(concatFs.CreateFile("/file", 0, CreateFileOptions.CreateConcatenationFile)); + Assert.Success(concatFs.DeleteFile("/file")); + + Assert.Result(ResultFs.PathNotFound, baseFs.GetEntryType(out _, "/file")); + } + + [Fact] + public void CreateDirectory_InsideConcatFile_ReturnsPathNotFound() + { + IFileSystem fs = CreateFileSystem(); + + Assert.Success(fs.CreateFile("/file", 0, CreateFileOptions.CreateConcatenationFile)); + + Assert.Result(ResultFs.PathNotFound, fs.CreateDirectory("/file/dir")); + } + + [Fact] + public void DeleteDirectory_DeleteConcatFile_ReturnsPathNotFound() + { + IFileSystem fs = CreateFileSystem(); + + Assert.Success(fs.CreateFile("/file", 0, CreateFileOptions.CreateConcatenationFile)); + + Assert.Result(ResultFs.PathNotFound, fs.DeleteDirectory("/file")); + } + + [Fact] + public void CleanDirectoryRecursively_CleanConcatFile_ReturnsPathNotFound() + { + IFileSystem fs = CreateFileSystem(); + + Assert.Success(fs.CreateFile("/file", 0, CreateFileOptions.CreateConcatenationFile)); + + Assert.Result(ResultFs.PathNotFound, fs.CleanDirectoryRecursively("/file")); + } + + [Fact] + public void RenameFile_RenameConcatFile_RenamesSuccessfully() + { + (IAttributeFileSystem baseFs, IFileSystem concatFs) = CreateFileSystemInternal(); + + Assert.Success(concatFs.CreateFile("/file", 0, CreateFileOptions.CreateConcatenationFile)); + Assert.Success(concatFs.RenameFile("/file", "/file2")); + + Assert.Success(baseFs.GetEntryType(out DirectoryEntryType type, "/file2")); + Assert.Equal(DirectoryEntryType.Directory, type); + } + + [Fact] + public void RenameDirectory_RenameConcatFile_ReturnsPathNotFound() + { + IFileSystem fs = CreateFileSystem(); + + Assert.Success(fs.CreateFile("/file", 0, CreateFileOptions.CreateConcatenationFile)); + + Assert.Result(ResultFs.PathNotFound, fs.RenameDirectory("/file", "/file2")); + } + + [Fact] + public void Write_ConcatFileWithMultipleInternalFiles_CanReadBackWrittenData() + { + const long fileSize = InternalFileSize * 5 - 5; + + byte[] data = new byte[fileSize]; + new Random(1234).NextBytes(data); + + IFileSystem fs = CreateFileSystem(); + + fs.CreateFile("/file", fileSize, CreateFileOptions.CreateConcatenationFile); + + using var file = new UniqueRef(); + fs.OpenFile(ref file.Ref(), "/file", OpenMode.Write); + file.Get.Write(0, data, WriteOption.None); + file.Reset(); + + byte[] readData = new byte[data.Length]; + fs.OpenFile(ref file.Ref(), "/file", OpenMode.Read); + + Assert.Success(file.Get.Read(out long bytesRead, 0, readData, ReadOption.None)); + Assert.Equal(data.Length, bytesRead); + + Assert.Equal(data, readData); + } + + [Fact] + public void SetSize_ResizeToHigherMultipleOfInternalFile_BaseDirHasCorrectStructure() + { + const long originalSize = InternalFileSize; + const long newSize = InternalFileSize * 2; + + (IAttributeFileSystem baseFs, IFileSystem concatFs) = CreateFileSystemInternal(); + + using var file = new UniqueRef(); + + // Create the file and then resize it + Assert.Success(concatFs.CreateFile("/file", originalSize, CreateFileOptions.CreateConcatenationFile)); + Assert.Success(concatFs.OpenFile(ref file.Ref(), "/file", OpenMode.All)); + Assert.Success(file.Get.SetSize(newSize)); + + Assert.Success(file.Get.GetSize(out long concatFileSize)); + Assert.Equal(newSize, concatFileSize); + + // Ensure the directory exists with the archive bit set + Assert.Success(baseFs.GetEntryType(out DirectoryEntryType dirType, "/file")); + Assert.Success(baseFs.GetFileAttributes(out NxFileAttributes dirAttributes, "/file")); + Assert.Equal(DirectoryEntryType.Directory, dirType); + Assert.Equal(NxFileAttributes.Directory | NxFileAttributes.Archive, dirAttributes); + + // Ensure the internal files exist + Assert.Success(baseFs.GetEntryType(out DirectoryEntryType internalFile00Type, "/file/00")); + Assert.Success(baseFs.GetEntryType(out DirectoryEntryType internalFile01Type, "/file/01")); + Assert.Equal(DirectoryEntryType.File, internalFile00Type); + Assert.Equal(DirectoryEntryType.File, internalFile01Type); + + // Ensure no additional internal files exist + Assert.Result(ResultFs.PathNotFound, baseFs.GetEntryType(out _, "/file/02")); + + // Ensure the internal file sizes are correct + Assert.Success(baseFs.GetFileSize(out long internalFile00Size, "/file/00")); + Assert.Success(baseFs.GetFileSize(out long internalFile01Size, "/file/01")); + Assert.Equal(InternalFileSize, internalFile00Size); + Assert.Equal(InternalFileSize, internalFile01Size); + } + + [Fact] + public void SetSize_ResizeToLowerMultipleOfInternalFile_BaseDirHasCorrectStructure() + { + const long originalSize = InternalFileSize * 5; + const long newSize = InternalFileSize * 2; + + (IAttributeFileSystem baseFs, IFileSystem concatFs) = CreateFileSystemInternal(); + + using var file = new UniqueRef(); + + // Create the file and then resize it + Assert.Success(concatFs.CreateFile("/file", originalSize, CreateFileOptions.CreateConcatenationFile)); + Assert.Success(concatFs.OpenFile(ref file.Ref(), "/file", OpenMode.All)); + Assert.Success(file.Get.SetSize(newSize)); + + Assert.Success(file.Get.GetSize(out long concatFileSize)); + Assert.Equal(newSize, concatFileSize); + + // Ensure the directory exists with the archive bit set + Assert.Success(baseFs.GetEntryType(out DirectoryEntryType dirType, "/file")); + Assert.Success(baseFs.GetFileAttributes(out NxFileAttributes dirAttributes, "/file")); + Assert.Equal(DirectoryEntryType.Directory, dirType); + Assert.Equal(NxFileAttributes.Directory | NxFileAttributes.Archive, dirAttributes); + + // Ensure the internal files exist + Assert.Success(baseFs.GetEntryType(out DirectoryEntryType internalFile00Type, "/file/00")); + Assert.Success(baseFs.GetEntryType(out DirectoryEntryType internalFile01Type, "/file/01")); + Assert.Equal(DirectoryEntryType.File, internalFile00Type); + Assert.Equal(DirectoryEntryType.File, internalFile01Type); + + // Ensure no additional internal files exist + Assert.Result(ResultFs.PathNotFound, baseFs.GetEntryType(out _, "/file/02")); + Assert.Result(ResultFs.PathNotFound, baseFs.GetEntryType(out _, "/file/03")); + Assert.Result(ResultFs.PathNotFound, baseFs.GetEntryType(out _, "/file/04")); + Assert.Result(ResultFs.PathNotFound, baseFs.GetEntryType(out _, "/file/05")); + + // Ensure the internal file sizes are correct + Assert.Success(baseFs.GetFileSize(out long internalFile00Size, "/file/00")); + Assert.Success(baseFs.GetFileSize(out long internalFile01Size, "/file/01")); + Assert.Equal(InternalFileSize, internalFile00Size); + Assert.Equal(InternalFileSize, internalFile01Size); + } + + [Fact] + public void SetSize_ResizeSmallerWithoutChangingInternalFileCount_BaseDirHasCorrectStructure() + { + const long originalSize = InternalFileSize * 2; + const long newSize = InternalFileSize * 2 - 5; + + (IAttributeFileSystem baseFs, IFileSystem concatFs) = CreateFileSystemInternal(); + + using var file = new UniqueRef(); + + // Create the file and then resize it + Assert.Success(concatFs.CreateFile("/file", originalSize, CreateFileOptions.CreateConcatenationFile)); + Assert.Success(concatFs.OpenFile(ref file.Ref(), "/file", OpenMode.All)); + Assert.Success(file.Get.SetSize(newSize)); + + Assert.Success(file.Get.GetSize(out long concatFileSize)); + Assert.Equal(newSize, concatFileSize); + + // Ensure the directory exists with the archive bit set + Assert.Success(baseFs.GetEntryType(out DirectoryEntryType dirType, "/file")); + Assert.Success(baseFs.GetFileAttributes(out NxFileAttributes dirAttributes, "/file")); + Assert.Equal(DirectoryEntryType.Directory, dirType); + Assert.Equal(NxFileAttributes.Directory | NxFileAttributes.Archive, dirAttributes); + + // Ensure the internal files exist + Assert.Success(baseFs.GetEntryType(out DirectoryEntryType internalFile00Type, "/file/00")); + Assert.Success(baseFs.GetEntryType(out DirectoryEntryType internalFile01Type, "/file/01")); + Assert.Equal(DirectoryEntryType.File, internalFile00Type); + Assert.Equal(DirectoryEntryType.File, internalFile01Type); + + // Ensure no additional internal files exist + Assert.Result(ResultFs.PathNotFound, baseFs.GetEntryType(out _, "/file/02")); + + // Ensure the internal file sizes are correct + Assert.Success(baseFs.GetFileSize(out long internalFile00Size, "/file/00")); + Assert.Success(baseFs.GetFileSize(out long internalFile01Size, "/file/01")); + Assert.Equal(InternalFileSize, internalFile00Size); + Assert.Equal(InternalFileSize - 5, internalFile01Size); + } + + [Fact] + public void SetSize_ResizeLargerWithoutChangingInternalFileCount_BaseDirHasCorrectStructure() + { + const long originalSize = InternalFileSize + 5; + const long newSize = InternalFileSize * 2 - 5; + + (IAttributeFileSystem baseFs, IFileSystem concatFs) = CreateFileSystemInternal(); + + using var file = new UniqueRef(); + + // Create the file and then resize it + Assert.Success(concatFs.CreateFile("/file", originalSize, CreateFileOptions.CreateConcatenationFile)); + Assert.Success(concatFs.OpenFile(ref file.Ref(), "/file", OpenMode.All)); + Assert.Success(file.Get.SetSize(newSize)); + + Assert.Success(file.Get.GetSize(out long concatFileSize)); + Assert.Equal(newSize, concatFileSize); + + // Ensure the directory exists with the archive bit set + Assert.Success(baseFs.GetEntryType(out DirectoryEntryType dirType, "/file")); + Assert.Success(baseFs.GetFileAttributes(out NxFileAttributes dirAttributes, "/file")); + Assert.Equal(DirectoryEntryType.Directory, dirType); + Assert.Equal(NxFileAttributes.Directory | NxFileAttributes.Archive, dirAttributes); + + // Ensure the internal files exist + Assert.Success(baseFs.GetEntryType(out DirectoryEntryType internalFile00Type, "/file/00")); + Assert.Success(baseFs.GetEntryType(out DirectoryEntryType internalFile01Type, "/file/01")); + Assert.Equal(DirectoryEntryType.File, internalFile00Type); + Assert.Equal(DirectoryEntryType.File, internalFile01Type); + + // Ensure no additional internal files exist + Assert.Result(ResultFs.PathNotFound, baseFs.GetEntryType(out _, "/file/02")); + + // Ensure the internal file sizes are correct + Assert.Success(baseFs.GetFileSize(out long internalFile00Size, "/file/00")); + Assert.Success(baseFs.GetFileSize(out long internalFile01Size, "/file/01")); + Assert.Equal(InternalFileSize, internalFile00Size); + Assert.Equal(InternalFileSize - 5, internalFile01Size); + } + + [Fact] + public void SetSize_ResizeSmallerChangingInternalFileCount_BaseDirHasCorrectStructure() + { + const long originalSize = InternalFileSize * 4 + 5; + const long newSize = InternalFileSize * 2 - 5; + + (IAttributeFileSystem baseFs, IFileSystem concatFs) = CreateFileSystemInternal(); + + using var file = new UniqueRef(); + + // Create the file and then resize it + Assert.Success(concatFs.CreateFile("/file", originalSize, CreateFileOptions.CreateConcatenationFile)); + Assert.Success(concatFs.OpenFile(ref file.Ref(), "/file", OpenMode.All)); + Assert.Success(file.Get.SetSize(newSize)); + + Assert.Success(file.Get.GetSize(out long concatFileSize)); + Assert.Equal(newSize, concatFileSize); + + // Ensure the directory exists with the archive bit set + Assert.Success(baseFs.GetEntryType(out DirectoryEntryType dirType, "/file")); + Assert.Success(baseFs.GetFileAttributes(out NxFileAttributes dirAttributes, "/file")); + Assert.Equal(DirectoryEntryType.Directory, dirType); + Assert.Equal(NxFileAttributes.Directory | NxFileAttributes.Archive, dirAttributes); + + // Ensure the internal files exist + Assert.Success(baseFs.GetEntryType(out DirectoryEntryType internalFile00Type, "/file/00")); + Assert.Success(baseFs.GetEntryType(out DirectoryEntryType internalFile01Type, "/file/01")); + Assert.Equal(DirectoryEntryType.File, internalFile00Type); + Assert.Equal(DirectoryEntryType.File, internalFile01Type); + + // Ensure no additional internal files exist + Assert.Result(ResultFs.PathNotFound, baseFs.GetEntryType(out _, "/file/02")); + Assert.Result(ResultFs.PathNotFound, baseFs.GetEntryType(out _, "/file/03")); + Assert.Result(ResultFs.PathNotFound, baseFs.GetEntryType(out _, "/file/04")); + + // Ensure the internal file sizes are correct + Assert.Success(baseFs.GetFileSize(out long internalFile00Size, "/file/00")); + Assert.Success(baseFs.GetFileSize(out long internalFile01Size, "/file/01")); + Assert.Equal(InternalFileSize, internalFile00Size); + Assert.Equal(InternalFileSize - 5, internalFile01Size); + } + + [Fact] + public void SetSize_ResizeLargerChangingInternalFileCount_BaseDirHasCorrectStructure() + { + const long originalSize = InternalFileSize - 5; + const long newSize = InternalFileSize * 2 - 5; + + (IAttributeFileSystem baseFs, IFileSystem concatFs) = CreateFileSystemInternal(); + + using var file = new UniqueRef(); + + // Create the file and then resize it + Assert.Success(concatFs.CreateFile("/file", originalSize, CreateFileOptions.CreateConcatenationFile)); + Assert.Success(concatFs.OpenFile(ref file.Ref(), "/file", OpenMode.All)); + Assert.Success(file.Get.SetSize(newSize)); + + Assert.Success(file.Get.GetSize(out long concatFileSize)); + Assert.Equal(newSize, concatFileSize); + + // Ensure the directory exists with the archive bit set + Assert.Success(baseFs.GetEntryType(out DirectoryEntryType dirType, "/file")); + Assert.Success(baseFs.GetFileAttributes(out NxFileAttributes dirAttributes, "/file")); + Assert.Equal(DirectoryEntryType.Directory, dirType); + Assert.Equal(NxFileAttributes.Directory | NxFileAttributes.Archive, dirAttributes); + + // Ensure the internal files exist + Assert.Success(baseFs.GetEntryType(out DirectoryEntryType internalFile00Type, "/file/00")); + Assert.Success(baseFs.GetEntryType(out DirectoryEntryType internalFile01Type, "/file/01")); + Assert.Equal(DirectoryEntryType.File, internalFile00Type); + Assert.Equal(DirectoryEntryType.File, internalFile01Type); + + // Ensure no additional internal files exist + Assert.Result(ResultFs.PathNotFound, baseFs.GetEntryType(out _, "/file/02")); + + // Ensure the internal file sizes are correct + Assert.Success(baseFs.GetFileSize(out long internalFile00Size, "/file/00")); + Assert.Success(baseFs.GetFileSize(out long internalFile01Size, "/file/01")); + Assert.Equal(InternalFileSize, internalFile00Size); + Assert.Equal(InternalFileSize - 5, internalFile01Size); + } + + [Fact] + public void SetSize_ResizeToEmpty_BaseDirHasCorrectStructure() + { + const long originalSize = InternalFileSize * 2 + 5; + const long newSize = 0; + + (IAttributeFileSystem baseFs, IFileSystem concatFs) = CreateFileSystemInternal(); + + using var file = new UniqueRef(); + + // Create the file and then resize it + Assert.Success(concatFs.CreateFile("/file", originalSize, CreateFileOptions.CreateConcatenationFile)); + Assert.Success(concatFs.OpenFile(ref file.Ref(), "/file", OpenMode.All)); + Assert.Success(file.Get.SetSize(newSize)); + + Assert.Success(file.Get.GetSize(out long concatFileSize)); + Assert.Equal(newSize, concatFileSize); + + // Ensure the directory exists with the archive bit set + Assert.Success(baseFs.GetEntryType(out DirectoryEntryType dirType, "/file")); + Assert.Success(baseFs.GetFileAttributes(out NxFileAttributes dirAttributes, "/file")); + Assert.Equal(DirectoryEntryType.Directory, dirType); + Assert.Equal(NxFileAttributes.Directory | NxFileAttributes.Archive, dirAttributes); + + // Ensure the internal files exist + Assert.Success(baseFs.GetEntryType(out DirectoryEntryType internalFile00Type, "/file/00")); + Assert.Equal(DirectoryEntryType.File, internalFile00Type); + + // Ensure no additional internal files exist + Assert.Result(ResultFs.PathNotFound, baseFs.GetEntryType(out _, "/file/01")); + Assert.Result(ResultFs.PathNotFound, baseFs.GetEntryType(out _, "/file/02")); + + // Ensure the internal file sizes are correct + Assert.Success(baseFs.GetFileSize(out long internalFile00Size, "/file/00")); + Assert.Equal(0, internalFile00Size); + } + + [Fact] + public void SetSize_ResizeToHigherMultipleOfInternalFile_FileContentsAreRetained() + { + EnsureContentsAreRetainedOnResize(InternalFileSize, InternalFileSize * 2); + } + + [Fact] + public void SetSize_ResizeToLowerMultipleOfInternalFile_FileContentsAreRetained() + { + EnsureContentsAreRetainedOnResize(InternalFileSize * 5, InternalFileSize * 2); + } + + [Fact] + public void SetSize__ResizeSmallerWithoutChangingInternalFileCount_FileContentsAreRetained() + { + EnsureContentsAreRetainedOnResize(InternalFileSize * 2, InternalFileSize * 2 - 5); + } + + [Fact] + public void SetSize_ResizeLargerWithoutChangingInternalFileCount_FileContentsAreRetained() + { + EnsureContentsAreRetainedOnResize(InternalFileSize + 5, InternalFileSize * 2 - 5); + } + + [Fact] + public void SetSize_ResizeSmallerChangingInternalFileCount_FileContentsAreRetained() + { + EnsureContentsAreRetainedOnResize(InternalFileSize * 4 + 5, InternalFileSize * 2 - 5); + } + + [Fact] + public void SetSize_ResizeLargerChangingInternalFileCount_FileContentsAreRetained() + { + EnsureContentsAreRetainedOnResize(InternalFileSize - 5, InternalFileSize * 2 - 5); + } + + private void EnsureContentsAreRetainedOnResize(int originalSize, int newSize) + { + const string fileName = "/file"; + + byte[] originalData = new byte[originalSize]; + new Random(1234).NextBytes(originalData); + + byte[] newData = new byte[newSize]; + byte[] actualNewData = new byte[newSize]; + originalData.AsSpan(0, Math.Min(originalSize, newSize)).CopyTo(newData); + + IFileSystem fs = CreateFileSystem(); + + // Create the file and write the data to it + using (var file = new UniqueRef()) + { + // Create the file and then write the data to it + Assert.Success(fs.CreateFile(fileName, originalSize, CreateFileOptions.CreateConcatenationFile)); + Assert.Success(fs.OpenFile(ref file.Ref(), fileName, OpenMode.Write)); + Assert.Success(file.Get.Write(0, originalData, WriteOption.None)); + } + + // Resize the file + using (var file = new UniqueRef()) + { + Assert.Success(fs.OpenFile(ref file.Ref(), fileName, OpenMode.Write)); + Assert.Success(file.Get.SetSize(newSize)); + } + + // Read back the entire resized file and ensure the contents are as expected + using (var file = new UniqueRef()) + { + Assert.Success(fs.OpenFile(ref file.Ref(), fileName, OpenMode.Read)); + Assert.Success(file.Get.Read(out long bytesRead, 0, actualNewData)); + Assert.Equal(newSize, bytesRead); + } + + Assert.Equal(newData, actualNewData); + } +} \ No newline at end of file