namespace XFESystem.Core; /// Explicit RAM-live storage. This implementation never opens a physical disk or a host file. public sealed class MemoryWorkspace : IWorkspaceBinaryStorage { private readonly Dictionary _entries = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _binary = new(StringComparer.OrdinalIgnoreCase); private const int MaxFileCharacters = 256 * 1024; private const int MaxEntries = 1024; public bool IsPersistent => false; public MemoryWorkspace(bool seed = true) { _entries.Add(SystemPaths.Root, null); foreach (var path in new[] { SystemPaths.Configuration, SystemPaths.Programs, SystemPaths.Desktop, SystemPaths.Documents, SystemPaths.Downloads, SystemPaths.Temporary }) CreateDirectory(path); if (seed) { WriteText(SystemPaths.Combine(SystemPaths.Desktop, "Welcome.md"), "# XFESystem | XFEstudio\n\nC# native desktop development.\n\nThis is a RAM-live workspace: files disappear after shutdown.\n\n- Open Files or Editor from the desktop.\n- Ctrl+S saves the current file to this session.\n- CoreCLR and independent .NET DLL loading are still being ported.\n"); WriteText(SystemPaths.Combine(SystemPaths.Documents, "Hello.js"), "const greeting = 'XFESystem';\nconsole.log(`Hello from ${greeting}!`);\n"); WriteText(SystemPaths.Combine(SystemPaths.Documents, "Hello.cs"), "using System;\n\nConsole.ForegroundColor = ConsoleColor.Cyan;\nConsole.WriteLine(\"Hello XFESystem!\");\nConsole.ResetColor();\n"); } } public bool Exists(string path) => _entries.ContainsKey(SystemPaths.Normalize(path)); public void CreateDirectory(string path) { path = SystemPaths.Normalize(path); if (_entries.TryGetValue(path, out var old)) { if (old is not null) throw new IOException("A file occupies this directory path."); return; } CreateDirectory(SystemPaths.Parent(path)); CheckCapacity(); _entries.Add(path, null); } public WorkspaceEntry[] List(string directory) { directory = SystemPaths.Normalize(directory); if (!_entries.TryGetValue(directory, out var content) || content is not null) throw new DirectoryNotFoundException(directory); var result = new List(); foreach (var item in _entries) if (!item.Key.Equals(directory, StringComparison.OrdinalIgnoreCase) && SystemPaths.Parent(item.Key).Equals(directory, StringComparison.OrdinalIgnoreCase)) result.Add(new(item.Key, item.Value is null, _binary.TryGetValue(item.Key, out var bytes) ? bytes.Length : item.Value?.Length ?? 0)); result.Sort((a, b) => a.IsDirectory != b.IsDirectory ? (a.IsDirectory ? -1 : 1) : StringComparer.OrdinalIgnoreCase.Compare(a.Name, b.Name)); return result.ToArray(); } public string ReadText(string path) { path = SystemPaths.Normalize(path); if (_binary.ContainsKey(path)) throw new IOException("Binary files must be opened by their application handler."); if (!_entries.TryGetValue(path, out var result) || result is null) throw new FileNotFoundException("Text file was not found.", path); return result; } public void WriteText(string path, string content) { ArgumentNullException.ThrowIfNull(content); path = SystemPaths.Normalize(path); if (content.Length > MaxFileCharacters) throw new IOException("The RAM-live file size limit was reached."); int used = 0; foreach (var item in _entries) if (!_binary.ContainsKey(item.Key)) used += item.Value?.Length ?? 0; int previous = !_binary.ContainsKey(path) && _entries.TryGetValue(path, out var existing) ? existing?.Length ?? 0 : 0; if (used - previous + content.Length > 4 * 1024 * 1024) throw new IOException("RAM-live text quota reached."); var parent = SystemPaths.Parent(path); if (!_entries.TryGetValue(parent, out var directory) || directory is not null) throw new DirectoryNotFoundException(parent); if (_entries.TryGetValue(path, out var old) && old is null) throw new IOException("Cannot overwrite a directory."); if (!_entries.ContainsKey(path)) CheckCapacity(); _entries[path] = content; _binary.Remove(path); } public byte[] ReadBytes(string path) { path = SystemPaths.Normalize(path); if (_binary.TryGetValue(path, out var bytes)) return (byte[])bytes.Clone(); return System.Text.Encoding.UTF8.GetBytes(ReadText(path)); } public void WriteBytes(string path, ReadOnlySpan content) { if (content.Length > 4 * 1024 * 1024) throw new IOException("Binary file size limit reached."); int used = 0; foreach (var item in _binary) used += item.Value.Length; path = SystemPaths.Normalize(path); int previous = _binary.TryGetValue(path, out var old) ? old.Length : 0; if (used - previous + content.Length > 8 * 1024 * 1024) throw new IOException("Binary workspace quota reached."); byte[] copy = content.ToArray(); WriteText(path, ""); _binary[path] = copy; } public void Delete(string path) { path = SystemPaths.Normalize(path); if (path == SystemPaths.Root) throw new IOException("Cannot delete the system root."); if (!_entries.ContainsKey(path)) throw new FileNotFoundException(path); foreach (var entry in _entries.Keys) if (entry.StartsWith(path + "\\", StringComparison.OrdinalIgnoreCase)) throw new IOException("The directory is not empty."); _entries.Remove(path); _binary.Remove(path); } public void Move(string source, string destination) { source = SystemPaths.Normalize(source); destination = SystemPaths.Normalize(destination); if (source == SystemPaths.Root || destination.StartsWith(source + "\\", StringComparison.OrdinalIgnoreCase)) throw new IOException("Invalid move destination."); if (!_entries.TryGetValue(source, out var item)) throw new FileNotFoundException(source); if (_entries.ContainsKey(destination)) throw new IOException("The destination already exists."); var parent = SystemPaths.Parent(destination); if (!_entries.TryGetValue(parent, out var parentData) || parentData is not null) throw new DirectoryNotFoundException(parent); var descendants = new List>(); foreach (var entry in _entries) if (entry.Key.Equals(source, StringComparison.OrdinalIgnoreCase) || entry.Key.StartsWith(source + "\\", StringComparison.OrdinalIgnoreCase)) descendants.Add(entry); var targets = new List(); foreach (var entry in descendants) targets.Add(SystemPaths.Normalize(destination + entry.Key[source.Length..])); foreach (var entry in descendants) _entries.Remove(entry.Key); for (int i = 0; i < descendants.Count; i++) { var entry = descendants[i]; var target = targets[i]; _entries.Add(target, entry.Value); if (_binary.TryGetValue(entry.Key, out var bytes)) { _binary.Remove(entry.Key); _binary.Add(target, bytes); } } } private void CheckCapacity() { if (_entries.Count >= MaxEntries) throw new IOException("The RAM-live entry limit was reached."); } }