using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Text; namespace pkNX.Containers.VFS; public class LayeredFileSystem : IFileSystem { public IReadOnlyList FileSystems { get; } public LayeredFileSystem(IReadOnlyList fileSystems) { FileSystems = fileSystems; Debug.Assert(FileSystems.Any(), "No filesystems provided."); Debug.Assert(FileSystems.Any(fs => !fs.IsReadOnly), "Should contain at least one writable filesystem."); } public LayeredFileSystem(params IFileSystem[] fileSystems) : this(fileSystems.AsReadOnly()) { } public void Dispose() { foreach (var fs in FileSystems) fs.Dispose(); GC.SuppressFinalize(this); } public IEnumerable GetEntityPaths(FileSystemPath path, Func? filter = null) { var entities = new HashSet(); foreach (var fs in FileSystems.Where(fs => fs.Exists(path))) entities.UnionWith(fs.GetEntityPaths(path, filter)); return entities; } public IEnumerable GetDirectoryPaths(FileSystemPath path, Func? filter = null) { var directories = new HashSet(); foreach (var fs in FileSystems.Where(fs => fs.Exists(path))) directories.UnionWith(fs.GetDirectoryPaths(path, filter)); return directories; } public IEnumerable GetFilePaths(FileSystemPath path, Func? filter = null) { var files = new HashSet(); foreach (var fs in FileSystems.Where(fs => fs.Exists(path))) files.UnionWith(fs.GetFilePaths(path, filter)); return files; } public IFileSystem GetFirst() { return FileSystems.First(); } public IFileSystem GetFirstWritable() { return FileSystems.First(fs => !fs.IsReadOnly); } public bool Exists(FileSystemPath path) { return FileSystems.Any(fs => fs.Exists(path)); } public IFileSystem? GetFirstWhereExists(FileSystemPath path) { return FileSystems.FirstOrDefault(fs => fs.Exists(path)); } public IFileSystem? GetFirstWritableWhereExists(FileSystemPath path) { return FileSystems.FirstOrDefault(fs => !fs.IsReadOnly && fs.Exists(path)); } private bool ValidateOpenMode(FileMode mode = FileMode.Open, FileAccess access = FileAccess.Read) { if (!access.HasFlag(FileAccess.Write) && mode is FileMode.Create or FileMode.CreateNew or FileMode.Truncate or FileMode.Append) { throw new ArgumentException($"File mode '{mode}' requires files to be accessed with write permission, but the access mode was '{access}'", nameof(access)); } if (access.HasFlag(FileAccess.Read) && mode == FileMode.Append) { throw new ArgumentException("File mode 'Append' requires files to be accessed with in read/write permission.", nameof(access)); } return true; } public Stream OpenFile(FileSystemPath path, FileMode mode = FileMode.Open, FileAccess access = FileAccess.Read) { if (!ValidateOpenMode(mode, access)) return Stream.Null; switch (mode) { case FileMode.Open: { // just read from top layer IFileSystem? fs = GetFirstWhereExists(path); if (fs == null) throw new FileNotFoundException($"Could not find the file at the specified path: {path}", nameof(path)); return fs.OpenFile(path, FileMode.Open, access); } case FileMode.OpenOrCreate: { // - Specifies that the operating system should open a file if it exists; otherwise, a new file should be created. // If the file is opened with FileAccess.Read, Read permission is required. // If the file access is FileAccess.Write, Write permission is required. // If the file is opened with FileAccess.ReadWrite, both Read and Write permissions are required. IFileSystem? fs = GetFirstWhereExists(path); if (fs != null) { // The file exists, now we need to check if we can open it with the requested access // readonly fs requires special handling for write requests. // if readonly access is requested, we can open it if the fs is readonly var readStream = fs.OpenFile(path); if (!fs.IsReadOnly || access == FileAccess.Read) return readStream; // For write-only access, we can just create a new empty file IFileSystem writableFs = GetFirstWritable(); writableFs.CreateDirectoryRecursive(path.ParentPath); var writeStream = writableFs.CreateFile(path); if (access == FileAccess.Write) return writeStream; // For read-write access, we need to first copy the file to a writable fs readStream.CopyTo(writeStream); writeStream.Seek(0, SeekOrigin.Begin); readStream.Dispose(); return writeStream; } // The file does not exist, create it on the first writable fs return GetFirstWritable().CreateFile(path); } case FileMode.CreateNew: { // Specifies that the operating system should create a new file. // - Requires Write permission. // - Requires the file does not already exist. if (Exists(path)) throw new IOException($"File {path.Path} already exists."); return GetFirstWritable().CreateFile(path); } case FileMode.Create: { // - This requires Write permission. // - FileMode.Create is equivalent to requesting that if the file does not exist, use CreateNew; otherwise, use Truncate. // Specifies that the operating system should create a new file. // If the file already exists, it will be overwritten. IFileSystem? fs = GetFirstWhereExists(path); if (fs != null) return fs.OpenFile(path, FileMode.Truncate, access); return GetFirstWritable().CreateFile(path); } case FileMode.Truncate: { // - Requires Write permission. // - Requires no Read access requested // - Requires existing file // When the file is opened, it should be truncated so that its size is zero bytes. IFileSystem? fs = GetFirstWhereExists(path); if (fs == null) throw new FileNotFoundException($"Could not find the file at the specified path: {path}", nameof(path)); return fs.OpenFile(path, FileMode.Truncate, access); } case FileMode.Append: { // Opens the file if it exists and seeks to the end of the file, or creates a new file. // This requires R/W permission. // Trying to seek to a position before the end of the file throws an IOException exception, and any attempt to read fails and throws a NotSupportedException exception. IFileSystem? fs = GetFirstWhereExists(path); if (fs == null) throw new FileNotFoundException($"Could not find the file at the specified path: {path}", nameof(path)); return fs.OpenFile(path, FileMode.Append, access); } default: throw new ArgumentOutOfRangeException(nameof(mode), mode, null); } } public void CreateDirectory(FileSystemPath path) { if (Exists(path)) throw new ArgumentException("The specified directory already exists."); IFileSystem? fs = GetFirstWhereExists(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); } }