diff --git a/src/LibHac/IO/ConcatenationDirectory.cs b/src/LibHac/IO/ConcatenationDirectory.cs new file mode 100644 index 00000000..5a1fa2d0 --- /dev/null +++ b/src/LibHac/IO/ConcatenationDirectory.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; + +namespace LibHac.IO +{ + public class ConcatenationDirectory : IDirectory + { + IFileSystem IDirectory.ParentFileSystem => ParentFileSystem; + public string FullPath { get; } + public OpenDirectoryMode Mode { get; } + + private ConcatenationFileSystem ParentFileSystem { get; } + private IDirectory ParentDirectory { get; } + + public ConcatenationDirectory(ConcatenationFileSystem fs, IDirectory parentDirectory, OpenDirectoryMode mode) + { + ParentFileSystem = fs; + ParentDirectory = parentDirectory; + Mode = mode; + FullPath = parentDirectory.FullPath; + } + + public IEnumerable Read() + { + foreach (DirectoryEntry entry in ParentDirectory.Read()) + { + bool isSplit = ParentFileSystem.IsSplitFile(entry.FullPath); + + if (!CanReturnEntry(entry, isSplit)) continue; + + if (!isSplit) + { + yield return entry; + } + else + { + long size = ParentFileSystem.GetSplitFileSize(entry.FullPath); + yield return new DirectoryEntry(entry.Name, entry.FullPath, DirectoryEntryType.File, size); + } + } + } + + public int GetEntryCount() + { + int count = 0; + + foreach (DirectoryEntry entry in ParentDirectory.Read()) + { + bool isSplit = ParentFileSystem.IsSplitFile(entry.FullPath); + + if (CanReturnEntry(entry, isSplit)) count++; + } + + return count; + } + + private bool CanReturnEntry(DirectoryEntry entry, bool isSplit) + { + return Mode.HasFlag(OpenDirectoryMode.Files) && (entry.Type == DirectoryEntryType.File || isSplit) || + Mode.HasFlag(OpenDirectoryMode.Directories) && entry.Type == DirectoryEntryType.Directory && !isSplit; + } + } +} diff --git a/src/LibHac/IO/ConcatenationFile.cs b/src/LibHac/IO/ConcatenationFile.cs new file mode 100644 index 00000000..bf9ed4bb --- /dev/null +++ b/src/LibHac/IO/ConcatenationFile.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace LibHac.IO +{ + public class ConcatenationFile : FileBase + { + private IFile[] Sources { get; } + private long SplitFileSize { get; } + + public ConcatenationFile(IList sources, long splitFileSize, OpenMode mode) + { + Sources = sources.ToArray(); + SplitFileSize = splitFileSize; + Mode = mode; + + for (int i = 0; i < Sources.Length - 1; i++) + { + if (Sources[i].GetSize() != SplitFileSize) + { + throw new ArgumentException($"Source file must have size {splitFileSize}"); + } + } + } + + public override int Read(Span destination, long offset) + { + long inPos = offset; + int outPos = 0; + int remaining = ValidateReadParamsAndGetSize(destination, offset); + + while (remaining > 0) + { + int fileIndex = GetFileIndexFromOffset(offset); + IFile file = Sources[fileIndex]; + long fileOffset = offset - fileIndex * SplitFileSize; + + long fileEndOffset = Math.Min((fileIndex + 1) * SplitFileSize, GetSize()); + int bytesToRead = (int)Math.Min(fileEndOffset - inPos, remaining); + int bytesRead = file.Read(destination.Slice(outPos, bytesToRead), fileOffset); + + outPos += bytesRead; + inPos += bytesRead; + remaining -= bytesRead; + + if (bytesRead < bytesToRead) break; + } + + return outPos; + } + + public override void Write(ReadOnlySpan source, long offset) + { + throw new NotImplementedException(); + } + + public override void Flush() + { + foreach (IFile file in Sources) + { + file.Flush(); + } + } + + public override long GetSize() + { + long size = 0; + + foreach (IFile file in Sources) + { + size += file.GetSize(); + } + + return size; + } + + public override void SetSize(long size) + { + throw new NotImplementedException(); + } + + private int GetFileIndexFromOffset(long offset) + { + return (int)(offset / SplitFileSize); + } + } +} diff --git a/src/LibHac/IO/ConcatenationFileSystem.cs b/src/LibHac/IO/ConcatenationFileSystem.cs new file mode 100644 index 00000000..dfb8388a --- /dev/null +++ b/src/LibHac/IO/ConcatenationFileSystem.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace LibHac.IO +{ + public class ConcatenationFileSystem : IFileSystem + { + private IAttributeFileSystem BaseFileSystem { get; } + private long SplitFileSize { get; } = 0xFFFF0000; // Hard-coded value used by FS + + public ConcatenationFileSystem(IAttributeFileSystem baseFileSystem) + { + BaseFileSystem = baseFileSystem; + } + + internal bool IsSplitFile(string path) + { + FileAttributes attributes = BaseFileSystem.GetFileAttributes(path); + + return (attributes & FileAttributes.Directory) != 0 && (attributes & FileAttributes.Archive) != 0; + } + + public void CreateDirectory(string path) + { + throw new NotImplementedException(); + } + + public void CreateFile(string path, long size) + { + throw new NotImplementedException(); + } + + public void DeleteDirectory(string path) + { + throw new NotImplementedException(); + } + + public void DeleteFile(string path) + { + throw new NotImplementedException(); + } + + public IDirectory OpenDirectory(string path, OpenDirectoryMode mode) + { + path = PathTools.Normalize(path); + + if (IsSplitFile(path)) + { + throw new DirectoryNotFoundException(path); + } + + IDirectory parentDir = BaseFileSystem.OpenDirectory(path, OpenDirectoryMode.All); + var dir = new ConcatenationDirectory(this, parentDir, mode); + return dir; + } + + public IFile OpenFile(string path, OpenMode mode) + { + path = PathTools.Normalize(path); + + if (!IsSplitFile(path)) + { + return BaseFileSystem.OpenFile(path, mode); + } + + int fileCount = GetSplitFileCount(path); + + var files = new List(); + + for (int i = 0; i < fileCount; i++) + { + string filePath = GetSplitFilePath(path, i); + IFile file = BaseFileSystem.OpenFile(filePath, mode); + files.Add(file); + } + + return new ConcatenationFile(files, SplitFileSize, mode); + } + + public void RenameDirectory(string srcPath, string dstPath) + { + srcPath = PathTools.Normalize(srcPath); + dstPath = PathTools.Normalize(dstPath); + + if (IsSplitFile(srcPath)) + { + throw new DirectoryNotFoundException(); + } + + BaseFileSystem.RenameDirectory(srcPath, dstPath); + } + + public void RenameFile(string srcPath, string dstPath) + { + srcPath = PathTools.Normalize(srcPath); + dstPath = PathTools.Normalize(dstPath); + + if (IsSplitFile(srcPath)) + { + BaseFileSystem.RenameDirectory(srcPath, dstPath); + } + else + { + BaseFileSystem.RenameFile(srcPath, dstPath); + } + } + + public bool DirectoryExists(string path) + { + path = PathTools.Normalize(path); + + return BaseFileSystem.DirectoryExists(path) && !IsSplitFile(path); + } + + public bool FileExists(string path) + { + path = PathTools.Normalize(path); + + return BaseFileSystem.FileExists(path) || BaseFileSystem.DirectoryExists(path) && IsSplitFile(path); + } + + public void Commit() + { + BaseFileSystem.Commit(); + } + + private int GetSplitFileCount(string dirPath) + { + int count = 0; + + while (BaseFileSystem.FileExists(GetSplitFilePath(dirPath, count))) + { + count++; + } + + return count; + } + + private static string GetSplitFilePath(string dirPath, int index) + { + return $"{dirPath}/{index:D2}"; + } + + internal long GetSplitFileSize(string path) + { + int fileCount = GetSplitFileCount(path); + long size = 0; + + for (int i = 0; i < fileCount; i++) + { + size += BaseFileSystem.GetFileSize(GetSplitFilePath(path, i)); + } + + return size; + } + } +}