From b8412c2556971fc265e26592f6b0abdddea3d5a4 Mon Sep 17 00:00:00 2001 From: duckdoom4 <60387522+duckdoom4@users.noreply.github.com> Date: Mon, 16 Jan 2023 19:32:19 +0100 Subject: [PATCH] Add VFS --- .../Archives/ZipArchiveFileSystem.cs | 92 +++++++ .../VFS/FileSystems/IFileSystem.cs | 44 ++++ .../VFS/FileSystems/LayeredFileSystem.cs | 83 ++++++ .../VFS/FileSystems/PhysicalFileSystem.cs | 95 +++++++ .../VFS/FileSystems/ReadOnlyFileSystem.cs | 55 ++++ pkNX.Containers/VFS/Util/FileSystemEntity.cs | 40 +++ .../VFS/Util/FileSystemExtensions.cs | 107 ++++++++ pkNX.Containers/VFS/Util/FileSystemPath.cs | 236 ++++++++++++++++++ pkNX.Containers/VFS/Util/VirtualDirectory.cs | 22 ++ pkNX.Containers/VFS/Util/VirtualFile.cs | 24 ++ pkNX.Containers/VFS/VirtualFileSystem.cs | 80 ++++++ pkNX.Game/GameManagerPLA.cs | 12 + 12 files changed, 890 insertions(+) create mode 100644 pkNX.Containers/VFS/FileSystems/Archives/ZipArchiveFileSystem.cs create mode 100644 pkNX.Containers/VFS/FileSystems/IFileSystem.cs create mode 100644 pkNX.Containers/VFS/FileSystems/LayeredFileSystem.cs create mode 100644 pkNX.Containers/VFS/FileSystems/PhysicalFileSystem.cs create mode 100644 pkNX.Containers/VFS/FileSystems/ReadOnlyFileSystem.cs create mode 100644 pkNX.Containers/VFS/Util/FileSystemEntity.cs create mode 100644 pkNX.Containers/VFS/Util/FileSystemExtensions.cs create mode 100644 pkNX.Containers/VFS/Util/FileSystemPath.cs create mode 100644 pkNX.Containers/VFS/Util/VirtualDirectory.cs create mode 100644 pkNX.Containers/VFS/Util/VirtualFile.cs create mode 100644 pkNX.Containers/VFS/VirtualFileSystem.cs diff --git a/pkNX.Containers/VFS/FileSystems/Archives/ZipArchiveFileSystem.cs b/pkNX.Containers/VFS/FileSystems/Archives/ZipArchiveFileSystem.cs new file mode 100644 index 00000000..2f5802ec --- /dev/null +++ b/pkNX.Containers/VFS/FileSystems/Archives/ZipArchiveFileSystem.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.IO.Compression; + +namespace pkNX.Containers.VFS; + +public class ZipArchiveFileSystem : IFileSystem +{ + public ZipArchive ZipArchive { get; private set; } + + public bool IsReadOnly => false; + + public static ZipArchiveFileSystem Open(Stream s) + { + return new ZipArchiveFileSystem(new ZipArchive(s, ZipArchiveMode.Update, true)); + } + + public static ZipArchiveFileSystem Create(Stream s) + { + return new ZipArchiveFileSystem(new ZipArchive(s, ZipArchiveMode.Create, true)); + } + + private ZipArchiveFileSystem(ZipArchive archive) + { + ZipArchive = archive; + } + public void Dispose() + { + ZipArchive.Dispose(); + } + + protected IEnumerable GetZipEntries() + { + return ZipArchive.Entries; + } + protected FileSystemPath ToPath(ZipArchiveEntry entry) + { + return FileSystemPath.Parse(FileSystemPath.DirectorySeparator + entry.FullName); + } + protected string ToEntryPath(FileSystemPath path) + { + // Remove heading '/' from path. + return path.Path.TrimStart(FileSystemPath.DirectorySeparator); + } + + protected ZipArchiveEntry? ToEntry(FileSystemPath path) + { + return ZipArchive.GetEntry(ToEntryPath(path)); + } + public IEnumerable GetEntities(FileSystemPath path) + { + return GetZipEntries().Select(ToPath).Where(path.IsParentOf) + .Select(entryPath => entryPath.ParentPath == path + ? entryPath + : path.AppendDirectory(entryPath.RemoveParent(path).GetDirectorySegments().First())) + .Distinct() + .ToList(); + } + + public bool Exists(FileSystemPath path) + { + if (path.IsFile) + return ToEntry(path) != null; + return GetZipEntries() + .Select(ToPath) + .Any(entryPath => entryPath.IsChildOf(path) || entryPath.Equals(path)); + } + + public Stream CreateFile(FileSystemPath path) + { + var zae = ZipArchive.CreateEntry(ToEntryPath(path)); + return zae.Open(); + } + + public Stream OpenFile(FileSystemPath path, FileAccess access) + { + var entry = ZipArchive.GetEntry(ToEntryPath(path)); + return entry?.Open() ?? Stream.Null; + } + + public void CreateDirectory(FileSystemPath path) + { + ZipArchive.CreateEntry(ToEntryPath(path)); + } + + public void Delete(FileSystemPath path) + { + var entry = ZipArchive.GetEntry(ToEntryPath(path)); + entry?.Delete(); + } +} diff --git a/pkNX.Containers/VFS/FileSystems/IFileSystem.cs b/pkNX.Containers/VFS/FileSystems/IFileSystem.cs new file mode 100644 index 00000000..fd2ad5c7 --- /dev/null +++ b/pkNX.Containers/VFS/FileSystems/IFileSystem.cs @@ -0,0 +1,44 @@ +using System.IO; +using System.Collections.Generic; +using System; + +namespace pkNX.Containers.VFS; + +public interface IFileSystem : IDisposable +{ + IEnumerable GetEntities(FileSystemPath path); + bool Exists(FileSystemPath path); + Stream CreateFile(FileSystemPath path); + Stream OpenFile(FileSystemPath path, FileAccess access); + void CreateDirectory(FileSystemPath path); + void Delete(FileSystemPath path); + + bool IsReadOnly => false; + + public string ReadAllText(FileSystemPath path) + { + if (!Exists(path)) + return string.Empty; + + using var stream = OpenFile(path, FileAccess.Read); + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + + public void WriteAllText(FileSystemPath path, string content) + { + if (!Exists(path)) + { + CreateFile(path); + } + + using var stream = OpenFile(path, FileAccess.Write); + using var writer = new StreamWriter(stream); + writer.Write(content); + } +} + +public static class IFileSystemExtensions +{ + public static ReadOnlyFileSystem AsReadOnlyFileSystem(this IFileSystem self) => new(self); +} diff --git a/pkNX.Containers/VFS/FileSystems/LayeredFileSystem.cs b/pkNX.Containers/VFS/FileSystems/LayeredFileSystem.cs new file mode 100644 index 00000000..c24fc0df --- /dev/null +++ b/pkNX.Containers/VFS/FileSystems/LayeredFileSystem.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace pkNX.Containers.VFS; + +public class LayeredFileSystem : IFileSystem +{ + public IEnumerable FileSystems { get; } + public LayeredFileSystem(IEnumerable fileSystems) + { + FileSystems = fileSystems.ToArray(); + } + + public LayeredFileSystem(params IFileSystem[] fileSystems) + { + FileSystems = fileSystems.ToArray(); + } + + public void Dispose() + { + foreach (var fs in FileSystems) + fs.Dispose(); + + GC.SuppressFinalize(this); + } + + public IEnumerable GetEntities(FileSystemPath path) + { + var entities = new SortedList(); + foreach (var fs in FileSystems.Where(fs => fs.Exists(path))) + { + foreach (var entity in fs.GetEntities(path)) + { + if (!entities.ContainsKey(entity)) + entities.Add(entity, entity); + } + } + return entities.Values; + } + + public bool Exists(FileSystemPath path) + { + return FileSystems.Any(fs => fs.Exists(path)); + } + + public IFileSystem? GetFirst(FileSystemPath path) + { + return FileSystems.FirstOrDefault(fs => fs.Exists(path)); + } + + public Stream CreateFile(FileSystemPath path) + { + IFileSystem fs = GetFirst(path) ?? FileSystems.First(); + return fs.CreateFile(path); + } + + public Stream OpenFile(FileSystemPath path, FileAccess access) + { + IFileSystem? fs = GetFirst(path); + if (fs == null) + throw new FileNotFoundException(); + return fs.OpenFile(path, access); + } + + public void CreateDirectory(FileSystemPath path) + { + if (Exists(path)) + throw new ArgumentException("The specified directory already exists."); + IFileSystem? fs = GetFirst(path.ParentPath); + if (fs == null) + throw new ArgumentException("The directory-parent does not exist."); + fs.CreateDirectory(path); + } + + public void Delete(FileSystemPath path) + { + foreach (var fs in FileSystems.Where(fs => fs.Exists(path))) + fs.Delete(path); + } +} diff --git a/pkNX.Containers/VFS/FileSystems/PhysicalFileSystem.cs b/pkNX.Containers/VFS/FileSystems/PhysicalFileSystem.cs new file mode 100644 index 00000000..80471d13 --- /dev/null +++ b/pkNX.Containers/VFS/FileSystems/PhysicalFileSystem.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace pkNX.Containers.VFS; + +public class PhysicalFileSystem : IFileSystem +{ + #region Internals + public string PhysicalRoot { get; } + + public PhysicalFileSystem(string physicalRoot) + { + if (!Path.IsPathRooted(physicalRoot)) + physicalRoot = Path.GetFullPath(physicalRoot); + if (physicalRoot[^1] != Path.DirectorySeparatorChar) + physicalRoot += Path.DirectorySeparatorChar; + PhysicalRoot = physicalRoot; + } + + public string GetPhysicalPath(FileSystemPath path) + { + return Path.Combine(PhysicalRoot, path.ToString().Remove(0, 1).Replace(FileSystemPath.DirectorySeparator, Path.DirectorySeparatorChar)); + } + + public FileSystemPath GetVirtualFilePath(string physicalPath) + { + if (!physicalPath.StartsWith(PhysicalRoot, StringComparison.InvariantCultureIgnoreCase)) + throw new ArgumentException("The specified path is not member of the PhysicalRoot.", nameof(physicalPath)); + string virtualPath = FileSystemPath.DirectorySeparator + physicalPath.Remove(0, PhysicalRoot.Length).Replace(Path.DirectorySeparatorChar, FileSystemPath.DirectorySeparator); + return FileSystemPath.Parse(virtualPath); + } + + public FileSystemPath GetVirtualDirectoryPath(string physicalPath) + { + if (!physicalPath.StartsWith(PhysicalRoot, StringComparison.InvariantCultureIgnoreCase)) + throw new ArgumentException("The specified path is not member of the PhysicalRoot.", nameof(physicalPath)); + string virtualPath = FileSystemPath.DirectorySeparator + physicalPath.Remove(0, PhysicalRoot.Length).Replace(Path.DirectorySeparatorChar, FileSystemPath.DirectorySeparator); + if (virtualPath[^1] != FileSystemPath.DirectorySeparator) + virtualPath += FileSystemPath.DirectorySeparator; + return FileSystemPath.Parse(virtualPath); + } + + #endregion + + public IEnumerable GetEntities(FileSystemPath path) + { + string physicalPath = GetPhysicalPath(path); + string[] directories = System.IO.Directory.GetDirectories(physicalPath); + string[] files = System.IO.Directory.GetFiles(physicalPath); + var virtualDirectories = directories.Select(GetVirtualDirectoryPath); + var virtualFiles = files.Select(GetVirtualFilePath); + return virtualDirectories.Concat(virtualFiles); + } + + public bool Exists(FileSystemPath path) + { + return path.IsFile ? System.IO.File.Exists(GetPhysicalPath(path)) : System.IO.Directory.Exists(GetPhysicalPath(path)); + } + + public Stream CreateFile(FileSystemPath path) + { + if (!path.IsFile) + throw new ArgumentException("The specified path is not a file.", nameof(path)); + return System.IO.File.Create(GetPhysicalPath(path)); + } + + public Stream OpenFile(FileSystemPath path, FileAccess access) + { + if (!path.IsFile) + throw new ArgumentException("The specified path is not a file.", nameof(path)); + return System.IO.File.Open(GetPhysicalPath(path), FileMode.Open, access); + } + + public void CreateDirectory(FileSystemPath path) + { + if (!path.IsDirectory) + throw new ArgumentException("The specified path is not a directory.", nameof(path)); + System.IO.Directory.CreateDirectory(GetPhysicalPath(path)); + } + + public void Delete(FileSystemPath path) + { + if (path.IsFile) + System.IO.File.Delete(GetPhysicalPath(path)); + else + System.IO.Directory.Delete(GetPhysicalPath(path), true); + } + + public void Dispose() + { + GC.SuppressFinalize(this); + } +} diff --git a/pkNX.Containers/VFS/FileSystems/ReadOnlyFileSystem.cs b/pkNX.Containers/VFS/FileSystems/ReadOnlyFileSystem.cs new file mode 100644 index 00000000..4b2c35a3 --- /dev/null +++ b/pkNX.Containers/VFS/FileSystems/ReadOnlyFileSystem.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace pkNX.Containers.VFS; + +public class ReadOnlyFileSystem : IFileSystem +{ + public bool IsReadOnly => true; + + public IFileSystem FileSystem { get; } + + public ReadOnlyFileSystem(IFileSystem fileSystem) + { + FileSystem = fileSystem; + } + + public void Dispose() + { + FileSystem.Dispose(); + GC.SuppressFinalize(this); + } + + public IEnumerable GetEntities(FileSystemPath path) + { + return FileSystem.GetEntities(path); + } + + public bool Exists(FileSystemPath path) + { + return FileSystem.Exists(path); + } + + public Stream OpenFile(FileSystemPath path, FileAccess access) + { + if (access != FileAccess.Read) + throw new InvalidOperationException("This is a read-only filesystem."); + return FileSystem.OpenFile(path, access); + } + + public Stream CreateFile(FileSystemPath path) + { + throw new InvalidOperationException("This is a read-only filesystem."); + } + + public void CreateDirectory(FileSystemPath path) + { + throw new InvalidOperationException("This is a read-only filesystem."); + } + + public void Delete(FileSystemPath path) + { + throw new InvalidOperationException("This is a read-only filesystem."); + } +} diff --git a/pkNX.Containers/VFS/Util/FileSystemEntity.cs b/pkNX.Containers/VFS/Util/FileSystemEntity.cs new file mode 100644 index 00000000..c3ace916 --- /dev/null +++ b/pkNX.Containers/VFS/Util/FileSystemEntity.cs @@ -0,0 +1,40 @@ +using System; + +namespace pkNX.Containers.VFS; + +public class FileSystemEntity : IEquatable +{ + public IFileSystem FileSystem { get; } + public FileSystemPath Path { get; } + public string Name => Path.EntityName; + + public FileSystemEntity(IFileSystem fileSystem, FileSystemPath path) + { + FileSystem = fileSystem; + Path = path; + } + + public override bool Equals(object? obj) + { + return obj is FileSystemEntity other && ((IEquatable)this).Equals(other); + } + + public override int GetHashCode() + { + return FileSystem.GetHashCode() ^ Path.GetHashCode(); + } + + bool IEquatable.Equals(FileSystemEntity? other) + { + return FileSystem.Equals(other?.FileSystem) && Path.Equals(other.Path); + } + + public static FileSystemEntity Create(IFileSystem fileSystem, FileSystemPath path) + { + if (path.IsFile) + return new VirtualFile(fileSystem, path); + + return new VirtualDirectory(fileSystem, path); + } +} + diff --git a/pkNX.Containers/VFS/Util/FileSystemExtensions.cs b/pkNX.Containers/VFS/Util/FileSystemExtensions.cs new file mode 100644 index 00000000..9f280787 --- /dev/null +++ b/pkNX.Containers/VFS/Util/FileSystemExtensions.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.IO; + +namespace pkNX.Containers.VFS; + +public static class FileSystemExtensions +{ + public static Stream Open(this VirtualFile file, FileAccess access) + { + return file.FileSystem.OpenFile(file.Path, access); + } + + public static void Delete(this FileSystemEntity entity) + { + entity.FileSystem.Delete(entity.Path); + } + + public static IEnumerable GetEntityPaths(this VirtualDirectory directory) + { + return directory.FileSystem.GetEntities(directory.Path); + } + + public static IEnumerable GetEntities(this VirtualDirectory directory) + { + var paths = directory.GetEntityPaths(); + return paths.Select(p => FileSystemEntity.Create(directory.FileSystem, p)); + } + + public static IEnumerable GetEntitiesRecursive(this IFileSystem fileSystem, FileSystemPath path) + { + if (!path.IsDirectory) + throw new ArgumentException("The specified path is not a directory."); + foreach (var entity in fileSystem.GetEntities(path)) + { + yield return entity; + + if (!entity.IsDirectory) + continue; + + foreach (var subEntity in fileSystem.GetEntitiesRecursive(entity)) + yield return subEntity; + } + } + + public static void CreateDirectoryRecursive(this IFileSystem fileSystem, FileSystemPath path) + { + if (!path.IsDirectory) + throw new ArgumentException("The specified path is not a directory."); + var currentDirectoryPath = FileSystemPath.Root; + foreach (var dirName in path.GetDirectorySegments()) + { + currentDirectoryPath = currentDirectoryPath.AppendDirectory(dirName); + if (!fileSystem.Exists(currentDirectoryPath)) + fileSystem.CreateDirectory(currentDirectoryPath); + } + } + + /*#region Move Extensions + public static void Move(this IFileSystem sourceFileSystem, FileSystemPath sourcePath, IFileSystem destinationFileSystem, FileSystemPath destinationPath) + { + if (!EntityMovers.Registration.TryGetSupported(sourceFileSystem.GetType(), destinationFileSystem.GetType(), out IEntityMover mover)) + throw new ArgumentException("The specified combination of file-systems is not supported."); + mover.Move(sourceFileSystem, sourcePath, destinationFileSystem, destinationPath); + } + + public static void MoveTo(this FileSystemEntity entity, IFileSystem destinationFileSystem, FileSystemPath destinationPath) + { + entity.FileSystem.Move(entity.Path, destinationFileSystem, destinationPath); + } + + public static void MoveTo(this VirtualDirectory source, VirtualDirectory destination) + { + source.FileSystem.Move(source.Path, destination.FileSystem, destination.Path.AppendDirectory(source.Path.EntityName)); + } + + public static void MoveTo(this VirtualFile source, VirtualDirectory destination) + { + source.FileSystem.Move(source.Path, destination.FileSystem, destination.Path.AppendFile(source.Path.EntityName)); + } + #endregion + + #region Copy Extensions + public static void Copy(this IFileSystem sourceFileSystem, FileSystemPath sourcePath, IFileSystem destinationFileSystem, FileSystemPath destinationPath) + { + if (!EntityCopiers.Registration.TryGetSupported(sourceFileSystem.GetType(), destinationFileSystem.GetType(), out IEntityCopier copier)) + throw new ArgumentException("The specified combination of file-systems is not supported."); + copier.Copy(sourceFileSystem, sourcePath, destinationFileSystem, destinationPath); + } + + public static void CopyTo(this FileSystemEntity entity, IFileSystem destinationFileSystem, FileSystemPath destinationPath) + { + entity.FileSystem.Copy(entity.Path, destinationFileSystem, destinationPath); + } + + public static void CopyTo(this VirtualDirectory source, VirtualDirectory destination) + { + source.FileSystem.Copy(source.Path, destination.FileSystem, destination.Path.AppendDirectory(source.Path.EntityName)); + } + + public static void CopyTo(this VirtualFile source, VirtualDirectory destination) + { + source.FileSystem.Copy(source.Path, destination.FileSystem, destination.Path.AppendFile(source.Path.EntityName)); + } + #endregion*/ +} diff --git a/pkNX.Containers/VFS/Util/FileSystemPath.cs b/pkNX.Containers/VFS/Util/FileSystemPath.cs new file mode 100644 index 00000000..cc8c88c3 --- /dev/null +++ b/pkNX.Containers/VFS/Util/FileSystemPath.cs @@ -0,0 +1,236 @@ +using System; +using System.Diagnostics; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; + +namespace pkNX.Containers.VFS; + +public readonly struct FileSystemPath : IEquatable, IComparable +{ + public const char DirectorySeparator = '/'; + public static FileSystemPath Root { get; } + + public string Path { get; } = "/"; + + public bool IsDirectory => Path[^1] == DirectorySeparator; + + public bool IsFile => !IsDirectory; + + public bool IsRoot => Path.Length == 1; + + public string EntityName + { + get + { + string name = Path; + if (IsRoot) + return string.Empty; + int endOfName = name.Length; + if (IsDirectory) + endOfName--; + int startOfName = name.LastIndexOf(DirectorySeparator, endOfName - 1, endOfName) + 1; + return name[startOfName..endOfName]; + } + } + + public FileSystemPath ParentPath + { + get + { + string parentPath = Path; + if (IsRoot) + throw new InvalidOperationException("There is no parent of root."); + int lookaheadCount = parentPath.Length; + if (IsDirectory) + lookaheadCount--; + int index = parentPath.LastIndexOf(DirectorySeparator, lookaheadCount - 1, lookaheadCount); + Debug.Assert(index >= 0); + parentPath = parentPath.Remove(index + 1); + return new FileSystemPath(parentPath); + } + } + + static FileSystemPath() + { + Root = new FileSystemPath(DirectorySeparator.ToString()); + } + + private FileSystemPath(string path) + { + Path = path; + } + + public static implicit operator FileSystemPath(string path) + { + var parsed = FileSystemPath.Parse(path); + return parsed; + } + + public static implicit operator string(FileSystemPath path) + { + return path.ToString(); + } + + public static bool IsRooted(string s) + { + if (s.Length == 0) + return false; + return s[0] == DirectorySeparator; + } + + public static FileSystemPath Parse(string s) + { + if (s == null) + throw new ArgumentNullException(nameof(s)); + if (!IsRooted(s)) + throw new UriFormatException($"Could not parse input \"{s}\": Path is not rooted."); + if (s.Contains(string.Concat(DirectorySeparator, DirectorySeparator))) + throw new UriFormatException($"Could not parse input \"{s}\": Path contains double directory-separators."); + return new FileSystemPath(s); + } + + public FileSystemPath AppendPath(string relativePath) + { + if (IsRooted(relativePath)) + throw new ArgumentException("The specified path should be relative.", nameof(relativePath)); + if (!IsDirectory) + throw new InvalidOperationException("This FileSystemPath is not a directory."); + return new FileSystemPath(Path + relativePath); + } + + [Pure] + public FileSystemPath AppendPath(FileSystemPath path) + { + if (!IsDirectory) + throw new InvalidOperationException("This FileSystemPath is not a directory."); + return new FileSystemPath(Path + path.Path[1..]); + } + + [Pure] + public FileSystemPath AppendDirectory(string directoryName) + { + if (directoryName.Contains(DirectorySeparator.ToString())) + throw new ArgumentException("The specified name includes directory-separator(s).", nameof(directoryName)); + if (!IsDirectory) + throw new InvalidOperationException("The specified FileSystemPath is not a directory."); + return new FileSystemPath(Path + directoryName + DirectorySeparator); + } + + [Pure] + public FileSystemPath AppendFile(string fileName) + { + if (fileName.Contains(DirectorySeparator.ToString())) + throw new ArgumentException("The specified name includes directory-separator(s).", nameof(fileName)); + if (!IsDirectory) + throw new InvalidOperationException("The specified FileSystemPath is not a directory."); + return new FileSystemPath(Path + fileName); + } + + [Pure] + public bool IsParentOf(FileSystemPath path) + { + return IsDirectory && Path.Length != path.Path.Length && path.Path.StartsWith(Path); + } + + [Pure] + public bool IsChildOf(FileSystemPath path) + { + return path.IsParentOf(this); + } + + [Pure] + public FileSystemPath RemoveParent(FileSystemPath parent) + { + if (!parent.IsDirectory) + throw new ArgumentException("The specified path can not be the parent of this path: it is not a directory."); + if (!Path.StartsWith(parent.Path)) + throw new ArgumentException("The specified path is not a parent of this path."); + return new FileSystemPath(Path.Remove(0, parent.Path.Length - 1)); + } + + [Pure] + public FileSystemPath RemoveChild(FileSystemPath child) + { + if (!Path.EndsWith(child.Path)) + throw new ArgumentException("The specified path is not a child of this path."); + return new FileSystemPath(Path[..(Path.Length - child.Path.Length + 1)]); + } + + [Pure] + public string GetExtension() + { + if (!IsFile) + throw new ArgumentException("The specified FileSystemPath is not a file."); + string name = EntityName; + int extensionIndex = name.LastIndexOf('.'); + return extensionIndex <= 0 ? string.Empty : name[extensionIndex..]; + } + + [Pure] + public FileSystemPath ChangeExtension(string extension) + { + if (!IsFile) + throw new ArgumentException("The specified FileSystemPath is not a file."); + string name = EntityName; + int extensionIndex = name.LastIndexOf('.'); + if (extensionIndex < 0) + return Parse(Path + extension); + return ParentPath.AppendFile(name[..extensionIndex] + extension); + } + + [Pure] + public IEnumerable GetDirectorySegments() + { + FileSystemPath path = this; + if (IsFile) + path = path.ParentPath; + var segments = new LinkedList(); + while (!path.IsRoot) + { + segments.AddFirst(path.EntityName); + path = path.ParentPath; + } + return segments.ToArray(); + } + + [Pure] + public int CompareTo(FileSystemPath other) + { + return string.Compare(Path, other.Path, StringComparison.Ordinal); + } + + [Pure] + public override string ToString() + { + return Path; + } + + [Pure] + public override bool Equals(object? obj) + { + return obj is FileSystemPath path && Equals(path); + } + + [Pure] + public bool Equals(FileSystemPath other) + { + return other.Path.Equals(Path); + } + + [Pure] + public override int GetHashCode() + { + return Path.GetHashCode(); + } + + public static bool operator ==(FileSystemPath pathA, FileSystemPath pathB) + { + return pathA.Equals(pathB); + } + + public static bool operator !=(FileSystemPath pathA, FileSystemPath pathB) + { + return !(pathA == pathB); + } +} diff --git a/pkNX.Containers/VFS/Util/VirtualDirectory.cs b/pkNX.Containers/VFS/Util/VirtualDirectory.cs new file mode 100644 index 00000000..b813f0ae --- /dev/null +++ b/pkNX.Containers/VFS/Util/VirtualDirectory.cs @@ -0,0 +1,22 @@ +using System; + +namespace pkNX.Containers.VFS; + +public class VirtualDirectory : FileSystemEntity, IEquatable +{ + public VirtualDirectory(IFileSystem fileSystem, FileSystemPath path) : base(fileSystem, path) + { + if (!path.IsDirectory) + throw new ArgumentException("The specified path is no directory.", nameof(path)); + } + + public bool Equals(VirtualDirectory? other) + { + return ((IEquatable)this).Equals(other); + } + + public override bool Equals(object? obj) + { + return Equals(obj as VirtualDirectory); + } +} diff --git a/pkNX.Containers/VFS/Util/VirtualFile.cs b/pkNX.Containers/VFS/Util/VirtualFile.cs new file mode 100644 index 00000000..9f41bdc4 --- /dev/null +++ b/pkNX.Containers/VFS/Util/VirtualFile.cs @@ -0,0 +1,24 @@ +using System; + +namespace pkNX.Containers.VFS; + +public class VirtualFile : FileSystemEntity, IEquatable +{ + public VirtualFile(IFileSystem fileSystem, FileSystemPath path) : + base(fileSystem, path) + { + if (!path.IsFile) + throw new ArgumentException("The specified path is no file.", nameof(path)); + } + + public bool Equals(VirtualFile? other) + { + return ((IEquatable)this).Equals(other); + } + + public override bool Equals(object? obj) + { + return Equals(obj as VirtualFile); + } +} + diff --git a/pkNX.Containers/VFS/VirtualFileSystem.cs b/pkNX.Containers/VFS/VirtualFileSystem.cs new file mode 100644 index 00000000..db86247b --- /dev/null +++ b/pkNX.Containers/VFS/VirtualFileSystem.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace pkNX.Containers.VFS; + +public record MountPoint(FileSystemPath Path, IFileSystem FileSystem) : IComparable +{ + public int CompareTo(MountPoint? other) + { + return other?.Path.CompareTo(Path) ?? 1; + } +} + +public class VirtualFileSystem : IFileSystem +{ + public bool IsReadOnly => Mounts.All(x => x.FileSystem.IsReadOnly); + + public SortedSet Mounts { get; } + + public VirtualFileSystem(IEnumerable mounts) + { + Mounts = new SortedSet(mounts); + } + + public VirtualFileSystem(params MountPoint[] mounts) : + this(mounts.AsEnumerable()) + { } + + protected MountPoint Get(FileSystemPath path) + { + return Mounts.First(pair => pair.Path == path || pair.Path.IsParentOf(path)); + } + + public void Dispose() + { + foreach (var fs in Mounts.Select(x => x.FileSystem)) + fs.Dispose(); + + GC.SuppressFinalize(this); + } + + public IEnumerable GetEntities(FileSystemPath path) + { + MountPoint point = Get(path); + IEnumerable entities = point.FileSystem.GetEntities(path.IsRoot ? path : path.RemoveParent(point.Path)); + return entities.Select(p => point.Path.AppendPath(p)); + } + + public bool Exists(FileSystemPath path) + { + var pair = Get(path); + return pair.FileSystem.Exists(path.RemoveParent(pair.Path)); + } + + public Stream CreateFile(FileSystemPath path) + { + var pair = Get(path); + return pair.FileSystem.CreateFile(path.RemoveParent(pair.Path)); + } + + public Stream OpenFile(FileSystemPath path, FileAccess access) + { + var pair = Get(path); + return pair.FileSystem.OpenFile(path.RemoveParent(pair.Path), access); + } + + public void CreateDirectory(FileSystemPath path) + { + var pair = Get(path); + pair.FileSystem.CreateDirectory(path.RemoveParent(pair.Path)); + } + + public void Delete(FileSystemPath path) + { + var pair = Get(path); + pair.FileSystem.Delete(path.RemoveParent(pair.Path)); + } +} diff --git a/pkNX.Game/GameManagerPLA.cs b/pkNX.Game/GameManagerPLA.cs index 2b8ca9df..67dff32d 100644 --- a/pkNX.Game/GameManagerPLA.cs +++ b/pkNX.Game/GameManagerPLA.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using pkNX.Containers; +using pkNX.Containers.VFS; using pkNX.Structures; using pkNX.Structures.FlatBuffers; @@ -19,14 +20,25 @@ public class GameManagerPLA : GameManager /// public GameData8a Data { get; protected set; } = null!; + public VirtualFileSystem VFS { get; private set; } + protected override void SetMitm() { var basePath = Path.GetDirectoryName(ROM.RomFS); if (basePath is null) throw new InvalidDataException("Invalid RomFS path."); + var tid = ROM.ExeFS != null ? TitleID : "arceus"; var redirect = Path.Combine(basePath, tid); FileMitm.SetRedirect(basePath, redirect); + + // VFS test + var cleanRomFS = new PhysicalFileSystem(basePath + "/romfs/").AsReadOnlyFileSystem(); + var moddedRomFS = new PhysicalFileSystem(redirect + "/romfs/"); + + var layeredFS = new LayeredFileSystem(moddedRomFS, cleanRomFS); + VFS = new VirtualFileSystem(new MountPoint("/romfs/", layeredFS)); + var file = VFS.OpenFile("/romfs/bin/pokemon/data/poke_ai.bin", FileAccess.ReadWrite); } public override void Initialize()