using System.Buffers.Binary; using System.Text; namespace XFESystem.Core; public readonly record struct FatFile(string Name, bool IsDirectory, uint Cluster, int Length); /// Reads ordinary files. Runtime writes never change FAT or boot files. 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 Chain(uint first) { HashSet 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 Entries(uint first) { var names = new Dictionary(); 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); } }