using System.Buffers.Binary;
using System.Text;
namespace XFESystem.Core;
public readonly record struct FatFile(string Name, bool IsDirectory, uint Cluster, int Length);
/// <summary>Reads ordinary files. Runtime writes never change FAT or boot files.</summary>
public sealed class ReadOnlyFat32Volume
{
private readonly IWorkspaceBlockDevice device;
private readonly ulong partition, dataStart, end;
private readonly byte[] fat;
private readonly uint clusterSectors, clusters, root;
public ReadOnlyFat32Volume(IWorkspaceBlockDevice device, ulong start, ulong last)
{
this.device = device; partition = start; end = last;
if (start < 34 || last < start || last >= device.SectorCount) throw new IOException("Invalid USB partition bounds.");
byte[] boot = new byte[512]; device.Read(start,boot);
uint U32(int at) => BinaryPrimitives.ReadUInt32LittleEndian(boot.AsSpan(at));
int reserved = BinaryPrimitives.ReadUInt16LittleEndian(boot.AsSpan(14)); uint sectors = U32(32), fatSectors = U32(36);
clusterSectors = boot[13]; root = U32(44);
if (boot[510] != 85 || boot[511] != 170 || BinaryPrimitives.ReadUInt16LittleEndian(boot.AsSpan(11)) != 512 ||
boot[16] != 2 || clusterSectors == 0 || clusterSectors > 128 || (clusterSectors&(clusterSectors-1)) != 0 ||
reserved < 8 || sectors > last-start+1 || sectors <= (ulong)reserved+2UL*fatSectors || fatSectors == 0 || fatSectors > 16384)
throw new IOException("Unsupported or malformed FAT32 volume.");
clusters = (sectors-(uint)reserved-2*fatSectors)/clusterSectors;
if (clusters < 65525 || clusters >= 0x0ffffff5 || (ulong)(clusters+2)*4 > (ulong)fatSectors*512) throw new IOException("Invalid FAT32 cluster geometry.");
dataStart = start+(uint)reserved+2*fatSectors;
fat = new byte[checked((int)fatSectors*512)]; device.Read(start+(uint)reserved,fat);
byte[] mirror = new byte[fat.Length]; device.Read(start+(uint)reserved+fatSectors,mirror);
if (!fat.AsSpan().SequenceEqual(mirror)) throw new IOException("FAT mirrors differ; writable workspace was refused.");
ValidCluster(root);
}
private void ValidCluster(uint cluster)
{ if (cluster < 2 || cluster >= clusters+2) throw new IOException("FAT chain leaves its partition."); }
private uint Next(uint cluster) { ValidCluster(cluster); return BinaryPrimitives.ReadUInt32LittleEndian(fat.AsSpan(checked((int)cluster*4)))&0x0fffffff; }
private ulong Lba(uint cluster) { ValidCluster(cluster); return dataStart+(cluster-2)*clusterSectors; }
private IEnumerable<uint> Chain(uint first)
{
HashSet<uint> seen = []; uint current = first;
while (current < 0x0ffffff8)
{ ValidCluster(current); if (!seen.Add(current)) throw new IOException("Cyclic FAT chain."); yield return current; current = Next(current); }
}
public FatFile Find(string path)
{
uint cluster = root; var parts = path.Replace('\\','/').Split('/',StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 0) return new("",true,root,0);
if (parts.Length > 16 || parts.Any(p => p is "." or "..")) throw new IOException("Invalid FAT path.");
for (int i = 0; i < parts.Length; i++)
{
var matches = Entries(cluster).Where(e => e.Name.Equals(parts[i],StringComparison.OrdinalIgnoreCase)).ToArray();
if (matches.Length != 1) throw new FileNotFoundException("USB file was not found or is ambiguous.",path);
var entry = matches[0]; if (i == parts.Length-1) return entry;
if (!entry.IsDirectory) throw new DirectoryNotFoundException(path); cluster = entry.Cluster;
}
throw new FileNotFoundException(path);
}
public FatFile[] List(string path)
{ var entry = Find(path); if (!entry.IsDirectory) throw new DirectoryNotFoundException(path); return Entries(entry.Cluster).ToArray(); }
private IEnumerable<FatFile> Entries(uint first)
{
var names = new Dictionary<int,string>(); int checksum = -1, expected = 0, budget = 0;
foreach (uint cluster in Chain(first))
{
byte[] bytes = new byte[clusterSectors*512]; device.Read(Lba(cluster),bytes);
for (int at = 0; at < bytes.Length; at += 32)
{
if (++budget > 32768) throw new IOException("USB directory is too large.");
byte firstByte = bytes[at], attributes = bytes[at+11]; if (firstByte == 0) yield break;
if (firstByte == 0xe5) { names.Clear(); expected = 0; continue; }
if (attributes == 15)
{
int index = firstByte&31;
if ((firstByte&64) != 0) { names.Clear(); expected = index; checksum = bytes[at+13]; }
if (index == 0 || index != expected || checksum != bytes[at+13]) { names.Clear(); expected = 0; continue; }
int[] positions = [1,3,5,7,9,14,16,18,20,22,24,28,30]; var text = new StringBuilder();
foreach (int pos in positions) { char c = (char)BinaryPrimitives.ReadUInt16LittleEndian(bytes.AsSpan(at+pos)); if (c is '\0' or '\uffff') break; text.Append(c); }
names[index] = text.ToString(); expected--; continue;
}
if ((attributes&8) != 0) { names.Clear(); expected = 0; continue; }
string shortName = Encoding.ASCII.GetString(bytes,at,8).TrimEnd(); string extension = Encoding.ASCII.GetString(bytes,at+8,3).TrimEnd();
if (extension.Length != 0) shortName += "."+extension;
int sum = 0; for (int i = 0; i < 11; i++) sum = (((sum&1)<<7)+(sum>>1)+bytes[at+i])&255;
string name = names.Count != 0 && expected == 0 && sum == checksum ? string.Concat(names.OrderBy(p => p.Key).Select(p => p.Value)) : shortName;
names.Clear(); expected = 0; if (name is "." or "..") continue;
uint child = (uint)BinaryPrimitives.ReadUInt16LittleEndian(bytes.AsSpan(at+20))<<16 | BinaryPrimitives.ReadUInt16LittleEndian(bytes.AsSpan(at+26));
uint length = BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan(at+28)); if (length > int.MaxValue) throw new IOException("USB file exceeds supported size.");
yield return new(name,(attributes&16)!=0,child,(int)length);
}
}
}
public byte[] ReadFile(string path)
{
var entry = Find(path); if (entry.IsDirectory || entry.Length > 4*1024*1024) throw new IOException("USB file exceeds the current 4 MiB program/file read limit.");
byte[] data = new byte[entry.Length]; if (data.Length == 0) return data; int offset = 0;
foreach (uint cluster in Chain(entry.Cluster))
{
byte[] chunk = new byte[clusterSectors*512]; device.Read(Lba(cluster),chunk);
int count = Math.Min(chunk.Length,data.Length-offset); chunk.AsSpan(0,count).CopyTo(data.AsSpan(offset)); offset += count;
if (offset == data.Length) return data;
}
throw new IOException("USB file chain is truncated.");
}
public (ulong Lba, uint Sectors) ContiguousWorkspace()
{
var file = Find("Workspace.bin");
if (file.IsDirectory || file.Length != UsbWorkspaceFormat.ContainerBytes) throw new IOException("USB workspace has the wrong size.");
uint needed = (uint)(file.Length/512)/clusterSectors, current = file.Cluster;
for (uint i = 0; i < needed; i++)
{
uint next = Next(current);
if (i+1 == needed ? next < 0x0ffffff8 : next != current+1) throw new IOException("USB workspace must have its original contiguous FAT allocation.");
if (i+1 < needed) current = next;
}
ulong lba = Lba(file.Cluster); uint sectors = (uint)(file.Length/512);
if (lba > end || sectors > end-lba+1) throw new IOException("USB workspace leaves its partition.");
return (lba,sectors);
}
}
using System.Buffers.Binary;
using System.Text;
namespace XFESystem.Core;
public readonly record struct FatFile(string Name, bool IsDirectory, uint Cluster, int Length);
/// <summary>Reads ordinary files. Runtime writes never change FAT or boot files.</summary>
public sealed class ReadOnlyFat32Volume
{
private readonly IWorkspaceBlockDevice device;
private readonly ulong partition, dataStart, end;
private readonly byte[] fat;
private readonly uint clusterSectors, clusters, root;
public ReadOnlyFat32Volume(IWorkspaceBlockDevice device, ulong start, ulong last)
{
this.device = device; partition = start; end = last;
if (start < 34 || last < start || last >= device.SectorCount) throw new IOException("Invalid USB partition bounds.");
byte[] boot = new byte[512]; device.Read(start,boot);
uint U32(int at) => BinaryPrimitives.ReadUInt32LittleEndian(boot.AsSpan(at));
int reserved = BinaryPrimitives.ReadUInt16LittleEndian(boot.AsSpan(14)); uint sectors = U32(32), fatSectors = U32(36);
clusterSectors = boot[13]; root = U32(44);
if (boot[510] != 85 || boot[511] != 170 || BinaryPrimitives.ReadUInt16LittleEndian(boot.AsSpan(11)) != 512 ||
boot[16] != 2 || clusterSectors == 0 || clusterSectors > 128 || (clusterSectors&(clusterSectors-1)) != 0 ||
reserved < 8 || sectors > last-start+1 || sectors <= (ulong)reserved+2UL*fatSectors || fatSectors == 0 || fatSectors > 16384)
throw new IOException("Unsupported or malformed FAT32 volume.");
clusters = (sectors-(uint)reserved-2*fatSectors)/clusterSectors;
if (clusters < 65525 || clusters >= 0x0ffffff5 || (ulong)(clusters+2)*4 > (ulong)fatSectors*512) throw new IOException("Invalid FAT32 cluster geometry.");
dataStart = start+(uint)reserved+2*fatSectors;
fat = new byte[checked((int)fatSectors*512)]; device.Read(start+(uint)reserved,fat);
byte[] mirror = new byte[fat.Length]; device.Read(start+(uint)reserved+fatSectors,mirror);
if (!fat.AsSpan().SequenceEqual(mirror)) throw new IOException("FAT mirrors differ; writable workspace was refused.");
ValidCluster(root);
}
private void ValidCluster(uint cluster)
{ if (cluster < 2 || cluster >= clusters+2) throw new IOException("FAT chain leaves its partition."); }
private uint Next(uint cluster) { ValidCluster(cluster); return BinaryPrimitives.ReadUInt32LittleEndian(fat.AsSpan(checked((int)cluster*4)))&0x0fffffff; }
private ulong Lba(uint cluster) { ValidCluster(cluster); return dataStart+(cluster-2)*clusterSectors; }
private IEnumerable<uint> Chain(uint first)
{
HashSet<uint> seen = []; uint current = first;
while (current < 0x0ffffff8)
{ ValidCluster(current); if (!seen.Add(current)) throw new IOException("Cyclic FAT chain."); yield return current; current = Next(current); }
}
public FatFile Find(string path)
{
uint cluster = root; var parts = path.Replace('\\','/').Split('/',StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 0) return new("",true,root,0);
if (parts.Length > 16 || parts.Any(p => p is "." or "..")) throw new IOException("Invalid FAT path.");
for (int i = 0; i < parts.Length; i++)
{
var matches = Entries(cluster).Where(e => e.Name.Equals(parts[i],StringComparison.OrdinalIgnoreCase)).ToArray();
if (matches.Length != 1) throw new FileNotFoundException("USB file was not found or is ambiguous.",path);
var entry = matches[0]; if (i == parts.Length-1) return entry;
if (!entry.IsDirectory) throw new DirectoryNotFoundException(path); cluster = entry.Cluster;
}
throw new FileNotFoundException(path);
}
public FatFile[] List(string path)
{ var entry = Find(path); if (!entry.IsDirectory) throw new DirectoryNotFoundException(path); return Entries(entry.Cluster).ToArray(); }
private IEnumerable<FatFile> Entries(uint first)
{
var names = new Dictionary<int,string>(); int checksum = -1, expected = 0, budget = 0;
foreach (uint cluster in Chain(first))
{
byte[] bytes = new byte[clusterSectors*512]; device.Read(Lba(cluster),bytes);
for (int at = 0; at < bytes.Length; at += 32)
{
if (++budget > 32768) throw new IOException("USB directory is too large.");
byte firstByte = bytes[at], attributes = bytes[at+11]; if (firstByte == 0) yield break;
if (firstByte == 0xe5) { names.Clear(); expected = 0; continue; }
if (attributes == 15)
{
int index = firstByte&31;
if ((firstByte&64) != 0) { names.Clear(); expected = index; checksum = bytes[at+13]; }
if (index == 0 || index != expected || checksum != bytes[at+13]) { names.Clear(); expected = 0; continue; }
int[] positions = [1,3,5,7,9,14,16,18,20,22,24,28,30]; var text = new StringBuilder();
foreach (int pos in positions) { char c = (char)BinaryPrimitives.ReadUInt16LittleEndian(bytes.AsSpan(at+pos)); if (c is '\0' or '\uffff') break; text.Append(c); }
names[index] = text.ToString(); expected--; continue;
}
if ((attributes&8) != 0) { names.Clear(); expected = 0; continue; }
string shortName = Encoding.ASCII.GetString(bytes,at,8).TrimEnd(); string extension = Encoding.ASCII.GetString(bytes,at+8,3).TrimEnd();
if (extension.Length != 0) shortName += "."+extension;
int sum = 0; for (int i = 0; i < 11; i++) sum = (((sum&1)<<7)+(sum>>1)+bytes[at+i])&255;
string name = names.Count != 0 && expected == 0 && sum == checksum ? string.Concat(names.OrderBy(p => p.Key).Select(p => p.Value)) : shortName;
names.Clear(); expected = 0; if (name is "." or "..") continue;
uint child = (uint)BinaryPrimitives.ReadUInt16LittleEndian(bytes.AsSpan(at+20))<<16 | BinaryPrimitives.ReadUInt16LittleEndian(bytes.AsSpan(at+26));
uint length = BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan(at+28)); if (length > int.MaxValue) throw new IOException("USB file exceeds supported size.");
yield return new(name,(attributes&16)!=0,child,(int)length);
}
}
}
public byte[] ReadFile(string path)
{
var entry = Find(path); if (entry.IsDirectory || entry.Length > 4*1024*1024) throw new IOException("USB file exceeds the current 4 MiB program/file read limit.");
byte[] data = new byte[entry.Length]; if (data.Length == 0) return data; int offset = 0;
foreach (uint cluster in Chain(entry.Cluster))
{
byte[] chunk = new byte[clusterSectors*512]; device.Read(Lba(cluster),chunk);
int count = Math.Min(chunk.Length,data.Length-offset); chunk.AsSpan(0,count).CopyTo(data.AsSpan(offset)); offset += count;
if (offset == data.Length) return data;
}
throw new IOException("USB file chain is truncated.");
}
public (ulong Lba, uint Sectors) ContiguousWorkspace()
{
var file = Find("Workspace.bin");
if (file.IsDirectory || file.Length != UsbWorkspaceFormat.ContainerBytes) throw new IOException("USB workspace has the wrong size.");
uint needed = (uint)(file.Length/512)/clusterSectors, current = file.Cluster;
for (uint i = 0; i < needed; i++)
{
uint next = Next(current);
if (i+1 == needed ? next < 0x0ffffff8 : next != current+1) throw new IOException("USB workspace must have its original contiguous FAT allocation.");
if (i+1 < needed) current = next;
}
ulong lba = Lba(file.Cluster); uint sectors = (uint)(file.Length/512);
if (lba > end || sectors > end-lba+1) throw new IOException("USB workspace leaves its partition.");
return (lba,sectors);
}
}