using AssetRipper.IO.Files.Streams.Smart; using System.Buffers; using System.Diagnostics; using System.Text; namespace AssetRipper.IO.Files; public partial class VirtualFileSystem : FileSystem { private readonly HashSet directories = ["/"]; private readonly Dictionary files = new(); /// /// The number of virtual files and directories. /// public int Count => files.Count + directories.Count; /// /// Clears the virtual file system. /// public void Clear() { directories.Clear(); files.Clear(); } private string GetFullDirectoryName(string path) { string directory = Path.GetDirectoryName(path); return Path.GetFullPath(directory); } public partial class VirtualFileImplementation { public override SmartStream Create(string path) { string directory = fileSystem.GetFullDirectoryName(path); string fullPath = Path.GetFullPath(path); if (!fileSystem.directories.Contains(directory)) { throw new DirectoryNotFoundException($"Directory '{directory}' not found."); } if (!fileSystem.files.TryGetValue(fullPath, out SmartStream? stream)) { stream = SmartStream.CreateMemory(); fileSystem.files.Add(fullPath, stream); } else { stream.SetLength(0); } return stream.CreateReference(); } public SmartStream Open(string path) { string directory = fileSystem.GetFullDirectoryName(path); string fullPath = Path.GetFullPath(path); if (!fileSystem.directories.Contains(directory)) { throw new DirectoryNotFoundException($"Directory '{directory}' not found."); } if (!fileSystem.files.TryGetValue(fullPath, out SmartStream? stream)) { throw new FileNotFoundException($"File '{path}' not found."); } return stream.CreateReference(); } public override SmartStream OpenRead(string path) => Open(path); public override SmartStream OpenWrite(string path) => Open(path); public override void Delete(string path) { string directory = fileSystem.GetFullDirectoryName(path); string fullPath = Path.GetFullPath(path); if (!fileSystem.directories.Contains(directory)) { throw new DirectoryNotFoundException($"Directory '{directory}' not found."); } if (fileSystem.files.Remove(fullPath, out SmartStream? stream)) { stream.Dispose(); } } public override bool Exists(string path) => fileSystem.files.ContainsKey(path); public override string ReadAllText(string path) => ReadAllText(path, Encoding.UTF8); public override string ReadAllText(string path, Encoding encoding) => encoding.GetString(ReadAllBytes(path)); public override void WriteAllText(string path, ReadOnlySpan contents) => WriteAllText(path, contents, Encoding.UTF8); public override void WriteAllText(string path, ReadOnlySpan contents, Encoding encoding) { int byteCount = encoding.GetByteCount(contents); byte[] array = ArrayPool.Shared.Rent(byteCount); Span span = array.AsSpan(0, byteCount); int bytesWritten = encoding.GetBytes(contents, span); Debug.Assert(bytesWritten == byteCount); WriteAllBytes(path, span); ArrayPool.Shared.Return(array); } public override byte[] ReadAllBytes(string path) { using SmartStream stream = Open(path); byte[] buffer = new byte[stream.Length]; stream.Position = 0; stream.ReadExactly(buffer); return buffer; } public override void WriteAllBytes(string path, ReadOnlySpan bytes) { using SmartStream stream = Create(path); stream.SetLength(bytes.Length); stream.Position = 0; stream.Write(bytes); } } public partial class VirtualDirectoryImplementation { public override void Create(string path) { string fullPath = GetFullPath(path); while (fileSystem.directories.Add(fullPath)) { int index = fullPath.LastIndexOf('/'); if (index > 0) { fullPath = fullPath[..index]; } else { break; } } } public override IEnumerable EnumerateDirectories(string path, string searchPattern, SearchOption searchOption) { throw new NotImplementedException(); } public override IEnumerable EnumerateFiles(string path, string searchPattern, SearchOption searchOption) { throw new NotImplementedException(); } public override bool Exists(string? path) => fileSystem.directories.Contains(GetFullPath(path)); private string GetFullPath(string? path) => Path.GetFullPath(path); } public partial class VirtualPathImplementation { public override string GetFullPath(string? path) { // The "current directory" is always the root directory. // We also don't support ".." and ".". if (path is null or "" or "/" or "\\") { return "/"; } string normalizedPath = path.Replace('\\', '/'); if (normalizedPath[^1] is '/') { normalizedPath = normalizedPath[..^1]; } return normalizedPath[0] is '/' ? normalizedPath : $"/{normalizedPath}"; } public override bool IsPathRooted(ReadOnlySpan path) { return path.Length > 0 && path[0] is '/' or '\\'; } } }