using System.Text;
namespace XFESystem.Core;
/// <summary>USB-backed workspace with a RAM cache; every successful persistent mutation commits to media.</summary>
public sealed class PersistentUsbWorkspace : IWorkspaceBinaryStorage
{
public const string ImportedFiles = "X:\\UsbFiles";
private readonly object gate = new();
private readonly UsbWorkspaceJournal journal;
private readonly ReadOnlyFat32Volume files;
private MemoryWorkspace memory;
public bool IsPersistent => true;
public ulong Generation => journal.Generation;
public PersistentUsbWorkspace(UsbWorkspaceJournal journal, ReadOnlyFat32Volume files)
{
this.journal = journal; this.files = files;
memory = journal.Load() is { } payload ? Decode(payload) : new MemoryWorkspace(false);
memory.CreateDirectory(ImportedFiles);
}
private static bool Transient(string path) => Under(path,SystemPaths.Temporary) || Under(path,"X:\\System\\Diagnostics");
private static bool Under(string path,string root) => path.Equals(root,StringComparison.OrdinalIgnoreCase) || path.StartsWith(root+"\\",StringComparison.OrdinalIgnoreCase);
private static bool Imported(string path) => Under(path,ImportedFiles);
private static string FatPath(string path) => "UserFiles"+path[ImportedFiles.Length..].Replace('\\','/');
public bool Exists(string path)
{
path = SystemPaths.Normalize(path); lock(gate) {
if (!Imported(path) || path.Equals(ImportedFiles,StringComparison.OrdinalIgnoreCase)) return memory.Exists(path);
try { files.Find(FatPath(path)); return true; } catch(FileNotFoundException) { return false; }
}
}
public WorkspaceEntry[] List(string directory)
{
directory = SystemPaths.Normalize(directory); lock(gate) {
if (!Imported(directory)) return memory.List(directory);
return files.List(FatPath(directory)).Select(e => new WorkspaceEntry(SystemPaths.Combine(directory,e.Name),e.IsDirectory,e.Length)).ToArray();
}
}
public string ReadText(string path)
{
path = SystemPaths.Normalize(path); lock(gate) {
if (!Imported(path)) return memory.ReadText(path);
if (path.EndsWith(".dll",StringComparison.OrdinalIgnoreCase)||path.EndsWith(".exe",StringComparison.OrdinalIgnoreCase)) throw new IOException("Binary USB file.");
return new UTF8Encoding(false,true).GetString(files.ReadFile(FatPath(path)));
}
}
public byte[] ReadBytes(string path)
{ path = SystemPaths.Normalize(path); lock(gate) return Imported(path) ? files.ReadFile(FatPath(path)) : memory.ReadBytes(path); }
public void CreateDirectory(string path)
{ path = SystemPaths.Normalize(path); lock(gate) { if(memory.Exists(path)) return; Mutate(path,m => m.CreateDirectory(path)); } }
public void WriteText(string path,string content)
{ path = SystemPaths.Normalize(path); lock(gate) { if(!Imported(path)&&memory.Exists(path)) { try { if(memory.ReadText(path)==content)return; }catch(IOException){} } Mutate(path,m => m.WriteText(path,content)); } }
public void WriteBytes(string path,ReadOnlySpan<byte> content)
{ path = SystemPaths.Normalize(path); byte[] copy = content.ToArray(); lock(gate) Mutate(path,m => m.WriteBytes(path,copy)); }
public void Delete(string path)
{ path = SystemPaths.Normalize(path); lock(gate) Mutate(path,m => m.Delete(path)); }
public void Move(string source,string destination)
{
source = SystemPaths.Normalize(source); destination = SystemPaths.Normalize(destination);
if(Imported(destination)||Transient(source)!=Transient(destination)) throw new IOException("Unsupported cross-volume move; use copy instead.");
lock(gate) Mutate(source,m => m.Move(source,destination));
}
private void Mutate(string path,Action<MemoryWorkspace> action)
{
if(Imported(path)) throw new IOException("UsbFiles is an import folder. Copy files into the workspace before editing them.");
if(Transient(path)) { action(memory); return; }
var before = memory.Clone();
try { action(memory); journal.Save(Encode(memory)); }
catch { memory = before; throw; }
}
private static byte[] Encode(MemoryWorkspace workspace)
{
using var stream = new MemoryStream(); using var writer = new BinaryWriter(stream,Encoding.UTF8,true);
var records = workspace.Export().Where(r => !Transient(r.Path)).ToArray();
writer.Write(0x31575358u); writer.Write(records.Length);
foreach(var record in records) { writer.Write(record.Path); writer.Write(record.Kind); writer.Write(record.Content.Length); writer.Write(record.Content); }
if(stream.Length>UsbWorkspaceFormat.MaxPayloadBytes)throw new IOException("USB workspace is full.");
return stream.ToArray();
}
private static MemoryWorkspace Decode(byte[] payload)
{
using var stream = new MemoryStream(payload,false); using var reader = new BinaryReader(stream,Encoding.UTF8,true);
if(reader.ReadUInt32()!=0x31575358u)throw new IOException("Unknown USB workspace format.");
int count = reader.ReadInt32(); if(count<0||count>1024)throw new IOException("USB workspace entry count is invalid.");
var result = new MemoryWorkspace(false); var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for(int i=0;i<count;i++)
{
string path = SystemPaths.Normalize(reader.ReadString()); byte kind = reader.ReadByte(); int length = reader.ReadInt32();
if(path==SystemPaths.Root||!seen.Add(path)||kind>2||length<0||length>4*1024*1024||length>stream.Length-stream.Position||kind==0&&length!=0||Transient(path))throw new IOException("Invalid USB workspace record.");
byte[] content = reader.ReadBytes(length); result.CreateDirectory(SystemPaths.Parent(path));
if(kind==0)result.CreateDirectory(path);else if(kind==1)result.WriteText(path,new UTF8Encoding(false,true).GetString(content));else result.WriteBytes(path,content);
}
if(stream.Position!=stream.Length)throw new IOException("USB workspace has trailing data.");
return result;
}
}
using System.Text;
namespace XFESystem.Core;
/// <summary>USB-backed workspace with a RAM cache; every successful persistent mutation commits to media.</summary>
public sealed class PersistentUsbWorkspace : IWorkspaceBinaryStorage
{
public const string ImportedFiles = "X:\\UsbFiles";
private readonly object gate = new();
private readonly UsbWorkspaceJournal journal;
private readonly ReadOnlyFat32Volume files;
private MemoryWorkspace memory;
public bool IsPersistent => true;
public ulong Generation => journal.Generation;
public PersistentUsbWorkspace(UsbWorkspaceJournal journal, ReadOnlyFat32Volume files)
{
this.journal = journal; this.files = files;
memory = journal.Load() is { } payload ? Decode(payload) : new MemoryWorkspace(false);
memory.CreateDirectory(ImportedFiles);
}
private static bool Transient(string path) => Under(path,SystemPaths.Temporary) || Under(path,"X:\\System\\Diagnostics");
private static bool Under(string path,string root) => path.Equals(root,StringComparison.OrdinalIgnoreCase) || path.StartsWith(root+"\\",StringComparison.OrdinalIgnoreCase);
private static bool Imported(string path) => Under(path,ImportedFiles);
private static string FatPath(string path) => "UserFiles"+path[ImportedFiles.Length..].Replace('\\','/');
public bool Exists(string path)
{
path = SystemPaths.Normalize(path); lock(gate) {
if (!Imported(path) || path.Equals(ImportedFiles,StringComparison.OrdinalIgnoreCase)) return memory.Exists(path);
try { files.Find(FatPath(path)); return true; } catch(FileNotFoundException) { return false; }
}
}
public WorkspaceEntry[] List(string directory)
{
directory = SystemPaths.Normalize(directory); lock(gate) {
if (!Imported(directory)) return memory.List(directory);
return files.List(FatPath(directory)).Select(e => new WorkspaceEntry(SystemPaths.Combine(directory,e.Name),e.IsDirectory,e.Length)).ToArray();
}
}
public string ReadText(string path)
{
path = SystemPaths.Normalize(path); lock(gate) {
if (!Imported(path)) return memory.ReadText(path);
if (path.EndsWith(".dll",StringComparison.OrdinalIgnoreCase)||path.EndsWith(".exe",StringComparison.OrdinalIgnoreCase)) throw new IOException("Binary USB file.");
return new UTF8Encoding(false,true).GetString(files.ReadFile(FatPath(path)));
}
}
public byte[] ReadBytes(string path)
{ path = SystemPaths.Normalize(path); lock(gate) return Imported(path) ? files.ReadFile(FatPath(path)) : memory.ReadBytes(path); }
public void CreateDirectory(string path)
{ path = SystemPaths.Normalize(path); lock(gate) { if(memory.Exists(path)) return; Mutate(path,m => m.CreateDirectory(path)); } }
public void WriteText(string path,string content)
{ path = SystemPaths.Normalize(path); lock(gate) { if(!Imported(path)&&memory.Exists(path)) { try { if(memory.ReadText(path)==content)return; }catch(IOException){} } Mutate(path,m => m.WriteText(path,content)); } }
public void WriteBytes(string path,ReadOnlySpan<byte> content)
{ path = SystemPaths.Normalize(path); byte[] copy = content.ToArray(); lock(gate) Mutate(path,m => m.WriteBytes(path,copy)); }
public void Delete(string path)
{ path = SystemPaths.Normalize(path); lock(gate) Mutate(path,m => m.Delete(path)); }
public void Move(string source,string destination)
{
source = SystemPaths.Normalize(source); destination = SystemPaths.Normalize(destination);
if(Imported(destination)||Transient(source)!=Transient(destination)) throw new IOException("Unsupported cross-volume move; use copy instead.");
lock(gate) Mutate(source,m => m.Move(source,destination));
}
private void Mutate(string path,Action<MemoryWorkspace> action)
{
if(Imported(path)) throw new IOException("UsbFiles is an import folder. Copy files into the workspace before editing them.");
if(Transient(path)) { action(memory); return; }
var before = memory.Clone();
try { action(memory); journal.Save(Encode(memory)); }
catch { memory = before; throw; }
}
private static byte[] Encode(MemoryWorkspace workspace)
{
using var stream = new MemoryStream(); using var writer = new BinaryWriter(stream,Encoding.UTF8,true);
var records = workspace.Export().Where(r => !Transient(r.Path)).ToArray();
writer.Write(0x31575358u); writer.Write(records.Length);
foreach(var record in records) { writer.Write(record.Path); writer.Write(record.Kind); writer.Write(record.Content.Length); writer.Write(record.Content); }
if(stream.Length>UsbWorkspaceFormat.MaxPayloadBytes)throw new IOException("USB workspace is full.");
return stream.ToArray();
}
private static MemoryWorkspace Decode(byte[] payload)
{
using var stream = new MemoryStream(payload,false); using var reader = new BinaryReader(stream,Encoding.UTF8,true);
if(reader.ReadUInt32()!=0x31575358u)throw new IOException("Unknown USB workspace format.");
int count = reader.ReadInt32(); if(count<0||count>1024)throw new IOException("USB workspace entry count is invalid.");
var result = new MemoryWorkspace(false); var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for(int i=0;i<count;i++)
{
string path = SystemPaths.Normalize(reader.ReadString()); byte kind = reader.ReadByte(); int length = reader.ReadInt32();
if(path==SystemPaths.Root||!seen.Add(path)||kind>2||length<0||length>4*1024*1024||length>stream.Length-stream.Position||kind==0&&length!=0||Transient(path))throw new IOException("Invalid USB workspace record.");
byte[] content = reader.ReadBytes(length); result.CreateDirectory(SystemPaths.Parent(path));
if(kind==0)result.CreateDirectory(path);else if(kind==1)result.WriteText(path,new UTF8Encoding(false,true).GetString(content));else result.WriteBytes(path,content);
}
if(stream.Position!=stream.Length)throw new IOException("USB workspace has trailing data.");
return result;
}
}