using System.Buffers.Binary; using System.Text; using XFESystem.Core; using XFESystem.Desktop; using Xunit; namespace XFESystem.Core.Tests; public sealed class UsbWorkspaceTests { private sealed class Disk : IWorkspaceBlockDevice { public const ulong Partition = 2048, Last = Partition+180000-1, Data = Partition+32+2800, Workspace = Data+1; private readonly Dictionary sectors; private ulong allowed, count; public ulong? FailAt; public int Flushes, Writes; public ulong SectorCount => 200000; public Disk(Dictionary? existing=null) { sectors=existing??new(); if(existing!=null)return; byte[] boot=new byte[512]; U16(boot,11,512);boot[13]=1;U16(boot,14,32);boot[16]=2; U32(boot,32,180000);U32(boot,36,1400);U32(boot,44,2);boot[510]=85;boot[511]=170;Raw(Partition,boot); byte[] fat=new byte[1400*512];U32(fat,0,0x0ffffff8);U32(fat,4,0x0fffffff);U32(fat,8,0x0fffffff); for(uint cluster=3;cluster<3+UsbWorkspaceFormat.ContainerSectors;cluster++)U32(fat,checked((int)cluster*4),cluster+1==3+UsbWorkspaceFormat.ContainerSectors?0x0fffffffu:cluster+1); U32(fat,131075*4,0x0fffffff);U32(fat,131076*4,0x0fffffff); Raw(Partition+32,fat);Raw(Partition+32+1400,fat); byte[] root=new byte[512];LongEntry(root,0,"Workspace.bin","WORKSPACBIN",false,3,UsbWorkspaceFormat.ContainerBytes);LongEntry(root,64,"UserFiles","USERFILE ",true,131075,0);Raw(Data,root); byte[] folder=new byte[512];Entry(folder,0,"TEST DLL",false,131076,4);Raw(Data+131073,folder); byte[] program=new byte[512];program[0]=1;program[1]=2;program[2]=3;program[3]=4;Raw(Data+131074,program); Raw(Workspace,UsbWorkspaceFormat.CreateDescriptor()); } public Disk Reopen()=>new(sectors); public ReadOnlyFat32Volume Fat()=>new(this,Partition,Last); public UsbWorkspaceJournal Journal()=>new(this,Workspace); public void Read(ulong lba,Span target) { if(target.Length%512!=0||lba>=SectorCount||(ulong)target.Length/512>SectorCount-lba)throw new IOException(); for(int i=0;i source) { if(source.Length%512!=0||lba=count||(ulong)source.Length/512>count-(lba-allowed)||FailAt==lba)throw new IOException("Simulated write failure or forbidden extent.");Writes++;Raw(lba,source); } private void Raw(ulong lba,ReadOnlySpan source) { for(int i=0;iFlushes++; public void AllowWorkspace(ulong lba,uint size) { if(lba!=Workspace||size!=UsbWorkspaceFormat.ContainerSectors)throw new IOException();allowed=lba+1;count=2*UsbWorkspaceFormat.BankSectors; } public void Corrupt(ulong lba) { if(!sectors.TryGetValue(lba,out var data))sectors[lba]=data=new byte[512];data[0]^=255; } public void ChangeFat(uint cluster,uint next) { ulong offset=(ulong)cluster*4;byte[] bytes=sectors[Partition+32+offset/512];U32(bytes,(int)(offset%512),next); } private static void Entry(byte[] data,int at,string name,bool directory,uint cluster,int length) { Encoding.ASCII.GetBytes(name.PadRight(11)).CopyTo(data,at);data[at+11]=directory?(byte)16:(byte)32;U16(data,at+20,cluster>>16);U16(data,at+26,cluster&65535);U32(data,at+28,(uint)length); } private static void LongEntry(byte[] data,int at,string name,string shortName,bool directory,uint cluster,int length) { Entry(data,at+32,shortName,directory,cluster,length);data[at]=0x41;data[at+11]=15; byte checksum=0;foreach(byte b in Encoding.ASCII.GetBytes(shortName.PadRight(11)))checksum=(byte)(((checksum&1)<<7)+(checksum>>1)+b);data[at+13]=checksum; int[] positions=[1,3,5,7,9,14,16,18,20,22,24,28,30]; for(int i=0;i<13;i++)U16(data,at+positions[i],iBinaryPrimitives.WriteUInt16LittleEndian(b.AsSpan(at),(ushort)n); private static void U32(byte[] b,int at,uint n)=>BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(at),n); } [Fact] public void OrdinaryFatFileIsReadFromMediaAndWorkspaceExtentIsContiguous() { var disk=new Disk();var fat=disk.Fat();Assert.Equal(new byte[]{1,2,3,4},fat.ReadFile("UserFiles/TEST.DLL")); Assert.Equal((Disk.Workspace,(uint)UsbWorkspaceFormat.ContainerSectors),fat.ContiguousWorkspace()); } [Fact] public void NewJournalIsEmptyAndSuccessfulSaveFlushesDataAndCommit() { var disk=new Disk();var journal=disk.Journal();Assert.Null(journal.Load());journal.Save("first"u8); Assert.Equal(2,disk.Flushes);Assert.Equal(1ul,journal.Generation); Assert.Equal("first",Encoding.UTF8.GetString(disk.Reopen().Journal().Load()!)); } [Fact] public void InterruptedNewCommitKeepsThePreviousGeneration() { var disk=new Disk();var journal=disk.Journal();journal.Load();journal.Save("previous"u8); disk.FailAt=Disk.Workspace+1+UsbWorkspaceFormat.BankSectors; Assert.Throws(()=>journal.Save("new data without a committed header"u8)); Assert.Equal("previous",Encoding.UTF8.GetString(disk.Reopen().Journal().Load()!)); } [Fact] public void DamagedNewestPayloadFallsBackToPreviousValidBank() { var disk=new Disk();var journal=disk.Journal();journal.Load();journal.Save("old"u8);journal.Save("new"u8); disk.Corrupt(Disk.Workspace+2+UsbWorkspaceFormat.BankSectors); var reopened=disk.Reopen().Journal();Assert.Equal("old",Encoding.UTF8.GetString(reopened.Load()!));Assert.Equal(1ul,reopened.Generation); } [Fact] public void TwoDamagedCommitRecordsAreNeverSilentlyReinitialized() { var disk=new Disk();disk.Corrupt(Disk.Workspace+1);disk.Corrupt(Disk.Workspace+1+UsbWorkspaceFormat.BankSectors); Assert.Throws(()=>disk.Reopen().Journal().Load());Assert.Equal(0,disk.Writes); } [Fact] public void DescriptorAndBootSectorsCannotBeWrittenThroughWorkspaceWindow() { var disk=new Disk();disk.Journal(); foreach(ulong lba in new[]{0ul,Disk.Partition,Disk.Workspace,Disk.Workspace+UsbWorkspaceFormat.ContainerSectors-1}) Assert.Throws(()=>disk.Write(lba,new byte[512])); } [Fact] public void BadDescriptorOrFatMirrorRefusesMountBeforeWriting() { var disk=new Disk();disk.Corrupt(Disk.Workspace);Assert.Throws(()=>disk.Journal());Assert.Equal(0,disk.Writes); disk=new Disk();disk.ChangeFat(3,3);Assert.Throws(()=>disk.Fat());Assert.Equal(0,disk.Writes); } [Fact] public void TextBinaryMoveDeleteAndAppearanceSurviveNewWorkspaceInstance() { var disk=new Disk();var workspace=new PersistentUsbWorkspace(disk.Journal(),disk.Fat()); workspace.WriteText(SystemPaths.Combine(SystemPaths.Documents,"Keep.txt"),"saved on USB"); workspace.WriteBytes(SystemPaths.Combine(SystemPaths.Documents,"Keep.dll"),new byte[]{0x4d,0x5a,3,4}); workspace.Move(SystemPaths.Combine(SystemPaths.Documents,"Keep.txt"),SystemPaths.Combine(SystemPaths.Documents,"Moved.txt")); workspace.WriteText(SystemPaths.Combine(SystemPaths.Documents,"Delete.txt"),"gone");workspace.Delete(SystemPaths.Combine(SystemPaths.Documents,"Delete.txt")); new DesktopTheme(DesktopThemeMode.Dark,0xff0033).Save(workspace); disk=disk.Reopen();workspace=new PersistentUsbWorkspace(disk.Journal(),disk.Fat()); Assert.Equal("saved on USB",workspace.ReadText(SystemPaths.Combine(SystemPaths.Documents,"Moved.txt"))); Assert.Equal(new byte[]{0x4d,0x5a,3,4},workspace.ReadBytes(SystemPaths.Combine(SystemPaths.Documents,"Keep.dll"))); Assert.False(workspace.Exists(SystemPaths.Combine(SystemPaths.Documents,"Delete.txt"))); var theme=DesktopTheme.Load(workspace,out var error);Assert.Null(error);Assert.Equal(DesktopThemeMode.Dark,theme.Mode);Assert.True(workspace.IsPersistent); } [Fact] public void FailedSaveRollsBackTheLiveCacheAndDiagnosticsDoNotWriteMedia() { var disk=new Disk();var workspace=new PersistentUsbWorkspace(disk.Journal(),disk.Fat());string path=SystemPaths.Combine(SystemPaths.Documents,"A.txt"); workspace.WriteText(path,"old");disk.FailAt=Disk.Workspace+1+UsbWorkspaceFormat.BankSectors; Assert.Throws(()=>workspace.WriteText(path,"unsaved"));Assert.Equal("old",workspace.ReadText(path)); int writes=disk.Writes;workspace.CreateDirectory("X:\\System\\Diagnostics");workspace.WriteText("X:\\System\\Diagnostics\\Usb.txt","temporary report");Assert.Equal(writes,disk.Writes); } [Fact] public void ImportFolderReadsFatBytesAndRejectsRuntimeFatMutation() { var disk=new Disk();var workspace=new PersistentUsbWorkspace(disk.Journal(),disk.Fat()); Assert.Equal(new byte[]{1,2,3,4},workspace.ReadBytes("X:\\UsbFiles\\TEST.DLL")); Assert.Throws(()=>workspace.WriteBytes("X:\\UsbFiles\\TEST.DLL",new byte[]{9})); Assert.Throws(()=>workspace.Delete("X:\\UsbFiles\\TEST.DLL"));Assert.Equal(0,disk.Writes); } }