using System.Buffers.Binary; namespace XFESystem.Core; public interface IWorkspaceBlockDevice { ulong SectorCount { get; } void Read(ulong lba, Span destination); void Write(ulong lba, ReadOnlySpan source); void Flush(); void AllowWorkspace(ulong lba, uint sectors); } /// Immutable FAT allocation, two independent data banks, sector-sized commit records. public static class UsbWorkspaceFormat { public const int SectorBytes = 512, ContainerSectors = 131072, BankSectors = 65535; public const int ContainerBytes = ContainerSectors * SectorBytes; public const int MaxPayloadBytes = (BankSectors - 1) * SectorBytes; public const ulong Magic = 0x5357425355454658, BankMagic = 0x4b4e414257454658; public static uint Crc(ReadOnlySpan bytes) { uint crc = uint.MaxValue; foreach (byte value in bytes) { crc ^= value; for (int i = 0; i < 8; i++) crc = (crc >> 1) ^ ((crc & 1) != 0 ? 0xedb88320u : 0); } return ~crc; } public static byte[] CreateDescriptor() { var data = new byte[SectorBytes]; BinaryPrimitives.WriteUInt64LittleEndian(data, Magic); BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(8), 1); BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(12), ContainerSectors); BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(16), BankSectors); BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(20), Crc(data.AsSpan(0,20))); return data; } public static bool ValidDescriptor(ReadOnlySpan data) => data.Length == SectorBytes && data.SequenceEqual(CreateDescriptor()); } public sealed class UsbWorkspaceJournal { private readonly IWorkspaceBlockDevice device; private readonly ulong start; private int active = -1; public ulong Generation { get; private set; } public UsbWorkspaceJournal(IWorkspaceBlockDevice device, ulong start) { this.device = device; this.start = start; if (start >= device.SectorCount || UsbWorkspaceFormat.ContainerSectors > device.SectorCount - start) throw new IOException("USB workspace extent is outside the device."); byte[] descriptor = new byte[512]; device.Read(start, descriptor); if (!UsbWorkspaceFormat.ValidDescriptor(descriptor)) throw new IOException("USB workspace descriptor is invalid."); device.AllowWorkspace(start, UsbWorkspaceFormat.ContainerSectors); } public byte[]? Load() { var candidates = new List<(int Bank, ulong Generation, int Length, uint Crc)>(); bool nonzero = false; for (int bank = 0; bank < 2; bank++) { byte[] header = new byte[512]; device.Read(Bank(bank), header); nonzero |= header.Any(b => b != 0); uint crc = BinaryPrimitives.ReadUInt32LittleEndian(header.AsSpan(32)); header.AsSpan(32,4).Clear(); int length = BinaryPrimitives.ReadInt32LittleEndian(header.AsSpan(24)); ulong generation = BinaryPrimitives.ReadUInt64LittleEndian(header.AsSpan(16)); if (BinaryPrimitives.ReadUInt64LittleEndian(header) != UsbWorkspaceFormat.BankMagic || BinaryPrimitives.ReadUInt32LittleEndian(header.AsSpan(8)) != 1 || BinaryPrimitives.ReadUInt32LittleEndian(header.AsSpan(12)) != 512 || generation == 0 || length <= 0 || length > UsbWorkspaceFormat.MaxPayloadBytes || UsbWorkspaceFormat.Crc(header) != crc) continue; candidates.Add((bank, generation, length, BinaryPrimitives.ReadUInt32LittleEndian(header.AsSpan(28)))); } foreach (var candidate in candidates.OrderByDescending(c => c.Generation)) { byte[] data = new byte[(candidate.Length+511)&~511]; device.Read(Bank(candidate.Bank)+1, data); if (UsbWorkspaceFormat.Crc(data.AsSpan(0,candidate.Length)) != candidate.Crc) continue; active = candidate.Bank; Generation = candidate.Generation; return data.AsSpan(0,candidate.Length).ToArray(); } if (nonzero) throw new IOException("Both USB workspace records are damaged; automatic overwrite was refused."); return null; } public void Save(ReadOnlySpan payload) { if (payload.IsEmpty || payload.Length > UsbWorkspaceFormat.MaxPayloadBytes || Generation == ulong.MaxValue) throw new IOException("USB workspace capacity or generation limit reached."); int next = active == 0 ? 1 : 0; byte[] data = new byte[(payload.Length+511)&~511]; payload.CopyTo(data); byte[] header = new byte[512]; BinaryPrimitives.WriteUInt64LittleEndian(header, UsbWorkspaceFormat.BankMagic); BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(8),1); BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(12),512); BinaryPrimitives.WriteUInt64LittleEndian(header.AsSpan(16),Generation+1); BinaryPrimitives.WriteInt32LittleEndian(header.AsSpan(24),payload.Length); BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(28),UsbWorkspaceFormat.Crc(payload)); BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(32),UsbWorkspaceFormat.Crc(header)); // Never overwrite the current valid bank. Old commit remains usable if // the new data, cache flush or final commit record is interrupted. device.Write(Bank(next)+1,data); device.Flush(); device.Write(Bank(next),header); device.Flush(); active = next; Generation++; } private ulong Bank(int bank) => start+1+(ulong)bank*UsbWorkspaceFormat.BankSectors; }