From a6e81bf5d387656b34c9088461eefb824524d22c Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 15 Mar 2026 14:02:17 +0000 Subject: [PATCH 01/21] VFS improvements, IO fixes, IO performance improvements. --- src/app/app.iotest.pas | 235 +++ src/app/app.mgr.pas | 3 +- .../ctl/ahci/driver.storage.ctl.ahci.pas | 2 + .../storage/ctl/driver.storage.ctl.ram.pas | 43 +- src/driver/storage/driver.storage.fdtable.pas | 27 +- .../storage/driver.storage.filedispatch.pas | 5 +- src/driver/storage/driver.storage.mgr.pas | 64 +- src/driver/storage/driver.storage.types.pas | 25 +- src/driver/storage/driver.storage.vfs.pas | 555 ++--- .../storage/fs/driver.storage.fs.fat32.pas | 1828 +++++++++++------ .../storage/fs/driver.storage.fs.flatfs.pas | 94 + .../storage/fs/driver.storage.fs.iso9660.pas | 249 +-- .../storage/vol/driver.storage.vol.mgr.pas | 8 +- 13 files changed, 1921 insertions(+), 1217 deletions(-) create mode 100644 src/app/app.iotest.pas diff --git a/src/app/app.iotest.pas b/src/app/app.iotest.pas new file mode 100644 index 00000000..b610baa0 --- /dev/null +++ b/src/app/app.iotest.pas @@ -0,0 +1,235 @@ +{ + App->IOTest - Write and read-back a 10 MB file to exercise offset-based I/O. + + Usage: IOTEST [path] + Default path: /disk/vol0/IOTEST.BIN + + Writes 10 MB in 4 KB chunks with a rotating byte pattern, then reads + the file back and verifies every byte. Reports throughput (KB/s) + for both the write and the read phase. + + @author(Aaron Hance ) +} +unit app.iotest; + +interface + +uses + io.stdio, debug.tracer; + +procedure init(); + +implementation + +uses + driver.storage.vfs, driver.storage.types, + memory.heap, core.util, arch.x86.util, + core.strings, arch.x86.bda; + +const + FILE_SIZE = 10 * 1024 * 1024; { 10 MB } + CHUNK_SIZE = 4096*64; { MB per is 4k*64 = 256 KB, so 40 chunks total } + TICK_HZ = 1024; { timer ISR frequency } + DEFAULT_PATH : pchar = '/disk/vol0/IOTEST.BIN'; + +{ Fill buf[0..CHUNK_SIZE-1] with a deterministic pattern seeded by chunkIdx } +procedure fillPattern(buf : puint8; chunkIdx : uint32); +var + i : uint32; +begin + if CHUNK_SIZE > 0 then + for i := 0 to CHUNK_SIZE - 1 do + buf[i] := uint8((chunkIdx + i) and $FF); +end; + +procedure run(Params : PParamList; stdin_buf, stdout_buf, stderr_buf : POutBuf); +var + path : pchar; + fh : TFileHandle; + fErr : TError; + buf : puint8; + verBuf : puint8; + offset : uint32; + chunkIdx : uint32; + written : uint32; + readBack : uint32; + totalChunks: uint32; + ok : boolean; + i : uint32; + t0, t1 : uint32; + elapsed : uint32; + kbps : uint32; + tmp : pchar; + tLast : uint32; { tick of last per-second report } + bytesInSec : uint32; { bytes transferred since last report } +begin + { Determine target path } + path := getParam(0, Params); + if path = nil then + path := DEFAULT_PATH; + + totalChunks := FILE_SIZE div CHUNK_SIZE; + + { Allocate two 4 KB heap buffers } + buf := puint8(kalloc(CHUNK_SIZE)); + verBuf := puint8(kalloc(CHUNK_SIZE)); + + { ==================== WRITE PHASE ==================== } + io.stdio.bufWriteStr(stdout_buf, 'Writing 10 MB to '); + io.stdio.bufWriteStrLn(stdout_buf, path); + + fh := OpenFile(path, omCreate, @fErr); + if (fh = 0) or (fErr <> eNone) then begin + io.stdio.bufWriteStrLn(stderr_buf, 'ERROR: Could not create file.'); + kfree(puint32(buf)); + kfree(puint32(verBuf)); + exit; + end; + + t0 := arch.x86.bda.Counters.c32; + tLast := t0; + bytesInSec := 0; + offset := 0; + ok := true; + if totalChunks > 0 then begin + for chunkIdx := 0 to totalChunks - 1 do begin + fillPattern(buf, chunkIdx); + written := WriteFile(fh, offset, buf, CHUNK_SIZE); + if written <> CHUNK_SIZE then begin + io.stdio.bufWriteStr(stderr_buf, 'ERROR: Short write at offset '); + io.stdio.bufWriteIntLn(stderr_buf, offset); + ok := false; + break; + end; + offset := offset + CHUNK_SIZE; + bytesInSec := bytesInSec + CHUNK_SIZE; + t1 := arch.x86.bda.Counters.c32; + if (t1 - tLast) >= TICK_HZ then begin + kbps := (bytesInSec div 1024) * TICK_HZ div (t1 - tLast); + io.stdio.bufWriteStr(stdout_buf, ' W '); + io.stdio.bufWriteInt(stdout_buf, offset div 1024); + io.stdio.bufWriteStr(stdout_buf, ' KB '); + io.stdio.bufWriteInt(stdout_buf, kbps); + io.stdio.bufWriteStrLn(stdout_buf, ' KB/s'); + tLast := t1; + bytesInSec := 0; + end; + end; + end; + t1 := arch.x86.bda.Counters.c32; + CloseFile(fh); + + if ok then begin + elapsed := t1 - t0; + io.stdio.bufWriteStr(stdout_buf, 'Write complete: '); + if elapsed > 0 then begin + kbps := ((FILE_SIZE div 1024) * TICK_HZ) div elapsed; + io.stdio.bufWriteInt(stdout_buf, kbps); + io.stdio.bufWriteStr(stdout_buf, ' KB/s ('); + io.stdio.bufWriteInt(stdout_buf, (elapsed * 1000) div TICK_HZ); + io.stdio.bufWriteStrLn(stdout_buf, ' ms)'); + end else + io.stdio.bufWriteStrLn(stdout_buf, '< 1 tick'); + end; + + if not ok then begin + kfree(puint32(buf)); + kfree(puint32(verBuf)); + exit; + end; + + { ==================== READ + VERIFY PHASE ==================== } + io.stdio.bufWriteStrLn(stdout_buf, 'Reading back and verifying...'); + + fh := OpenFile(path, omRead, @fErr); + if (fh = 0) or (fErr <> eNone) then begin + io.stdio.bufWriteStrLn(stderr_buf, 'ERROR: Could not open file for reading.'); + kfree(puint32(buf)); + kfree(puint32(verBuf)); + exit; + end; + + t0 := arch.x86.bda.Counters.c32; + tLast := t0; + bytesInSec := 0; + offset := 0; + ok := true; + if totalChunks > 0 then begin + for chunkIdx := 0 to totalChunks - 1 do begin + core.util.memset(uint32(verBuf), 0, CHUNK_SIZE); + readBack := ReadFile(fh, offset, verBuf, CHUNK_SIZE); + if readBack <> CHUNK_SIZE then begin + io.stdio.bufWriteStr(stderr_buf, 'ERROR: Short read at offset '); + io.stdio.bufWriteInt(stderr_buf, offset); + io.stdio.bufWriteStr(stderr_buf, ' (got '); + io.stdio.bufWriteInt(stderr_buf, readBack); + io.stdio.bufWriteStrLn(stderr_buf, ')'); + ok := false; + break; + end; + + { Verify pattern } + fillPattern(buf, chunkIdx); + for i := 0 to CHUNK_SIZE - 1 do begin + if verBuf[i] <> buf[i] then begin + io.stdio.bufWriteStr(stderr_buf, 'ERROR: Mismatch at offset '); + io.stdio.bufWriteInt(stderr_buf, offset + i); + io.stdio.bufWriteStr(stderr_buf, ' expected '); + io.stdio.bufWriteHexPair(stderr_buf, buf[i]); + io.stdio.bufWriteStr(stderr_buf, ' got '); + io.stdio.bufWriteHexPair(stderr_buf, verBuf[i]); + io.stdio.bufWriteNewLine(stderr_buf); + ok := false; + break; + end; + end; + if not ok then break; + + offset := offset + CHUNK_SIZE; + bytesInSec := bytesInSec + CHUNK_SIZE; + t1 := arch.x86.bda.Counters.c32; + if (t1 - tLast) >= TICK_HZ then begin + kbps := (bytesInSec div 1024) * TICK_HZ div (t1 - tLast); + io.stdio.bufWriteStr(stdout_buf, ' R '); + io.stdio.bufWriteInt(stdout_buf, offset div 1024); + io.stdio.bufWriteStr(stdout_buf, ' KB '); + io.stdio.bufWriteInt(stdout_buf, kbps); + io.stdio.bufWriteStrLn(stdout_buf, ' KB/s'); + tLast := t1; + bytesInSec := 0; + end; + end; + end; + t1 := arch.x86.bda.Counters.c32; + CloseFile(fh); + + if ok then begin + elapsed := t1 - t0; + io.stdio.bufWriteStr(stdout_buf, 'Read+verify complete: '); + if elapsed > 0 then begin + kbps := ((FILE_SIZE div 1024) * TICK_HZ) div elapsed; + io.stdio.bufWriteInt(stdout_buf, kbps); + io.stdio.bufWriteStr(stdout_buf, ' KB/s ('); + io.stdio.bufWriteInt(stdout_buf, (elapsed * 1000) div TICK_HZ); + io.stdio.bufWriteStrLn(stdout_buf, ' ms)'); + end else + io.stdio.bufWriteStrLn(stdout_buf, '< 1 tick'); + io.stdio.bufWriteStrLn(stdout_buf, 'PASS: All 10 MB verified OK.'); + end else begin + io.stdio.bufWriteStrLn(stderr_buf, 'FAIL: Verification failed.'); + end; + + { ==================== CLEANUP ==================== } + DeleteFile(path, @fErr); + + kfree(puint32(buf)); + kfree(puint32(verBuf)); +end; + +procedure init(); +begin + debug.tracer.push_trace('iotest.init'); + io.stdio.registerCommand('IOTEST', @run, 'Write and read-verify a 10 MB file.'); +end; + +end. diff --git a/src/app/app.mgr.pas b/src/app/app.mgr.pas index d32a3a4b..348dbdd6 100644 --- a/src/app/app.mgr.pas +++ b/src/app/app.mgr.pas @@ -26,7 +26,7 @@ uses boot.mgr, debug.tracer, io.stdio, proc.mgr, //progs - app.base64, app.md5sum, app.dhclient, app.vbeinfo, app.testcmd, app.ping, app.meminfo, app.setres, + app.base64, app.md5sum, app.dhclient, app.vbeinfo, app.testcmd, app.ping, app.meminfo, app.setres, app.iotest, //drivers driver.storage.ctl.ram, driver.mgr, //network @@ -92,6 +92,7 @@ begin app.bsod.init(); app.vterminal.init; app.filebrowser.init(); + app.iotest.init(); end; Initialization diff --git a/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas b/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas index 8468e214..22e0d77a 100644 --- a/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas +++ b/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas @@ -450,11 +450,13 @@ begin storageDev^.writable := false; storageDev^.dispatchRead := TDriverDispatch(@ahci_dispatch_atapi_read); storageDev^.dispatchWrite := TDriverDispatch(nil); + storageDev^.maxActive := 1; end else begin storageDev^.controller := TControllerType.ControllerAHCI; storageDev^.writable := true; storageDev^.dispatchRead := TDriverDispatch(@ahci_dispatch_read); storageDev^.dispatchWrite := TDriverDispatch(@ahci_dispatch_write); + storageDev^.maxActive := 32; { AHCI supports up to 32 command slots } end; driver.storage.mgr.register_device(storageDev); diff --git a/src/driver/storage/ctl/driver.storage.ctl.ram.pas b/src/driver/storage/ctl/driver.storage.ctl.ram.pas index a7ebe107..cdbc0c42 100644 --- a/src/driver/storage/ctl/driver.storage.ctl.ram.pas +++ b/src/driver/storage/ctl/driver.storage.ctl.ram.pas @@ -82,33 +82,6 @@ end; { ---- Filesystem hook callbacks ---- } -{ PPReadHook — load entire file into a new kalloc'd buffer. - Sets buffer^ to the data pointer, bytecount^ to the size. - Returns 0 on success, 1 on failure. } -function rd_Read(volume : PStorage_Volume; directory : pchar; - fileName : pchar; buffer : puint32; - bytecount : puint32) : uint32; -var - clean : pchar; - idx : sint32; - dest : puint8; -begin - rd_Read := 1; - clean := stripLeadingSlash(fileName); - if clean = nil then exit; - idx := findFileByName(clean); - if idx < 0 then exit; - - { VFS expects a fresh buffer it can own } - dest := puint8(kalloc(Files[idx].Size)); - if dest = nil then exit; - memcpy(uint32(Files[idx].Data), uint32(dest), Files[idx].Size); - - buffer^ := uint32(dest); - bytecount^ := Files[idx].Size; - rd_Read := 0; -end; - { PPReadOffsetHook — read byteCount bytes at offset into caller buffer. Returns actual bytes copied. } function rd_ReadOffset(volume : PStorage_Volume; directory : pchar; @@ -133,6 +106,20 @@ begin rd_ReadOffset := copyLen; end; +function rd_FileSize(volume : PStorage_Volume; directory : pchar; + fileName : pchar) : uint32; +var + clean : pchar; + idx : sint32; +begin + rd_FileSize := 0; + clean := stripLeadingSlash(fileName); + if clean = nil then exit; + idx := findFileByName(clean); + if idx < 0 then exit; + rd_FileSize := Files[idx].Size; +end; + { PPReadDirHook — return a linked list of TDirectory_Entry for all files. VFS owns the returned list and frees each entry^.fileName. } function rd_ReadDir(volume : PStorage_Volume; directory : pchar; @@ -269,9 +256,9 @@ begin { Set up a minimal filesystem descriptor with our callbacks } memset(uint32(@RAMFS), 0, sizeof(TFilesystem)); RAMFS.sName := 'ramfs'; - RAMFS.readCallback := @rd_Read; RAMFS.readDirCallback := @rd_ReadDir; RAMFS.readOffsetCallback := @rd_ReadOffset; + RAMFS.fileSizeCallback := @rd_FileSize; RAMFS.identifyCallback := @rd_Identify; { Set up a virtual volume backed by our filesystem } diff --git a/src/driver/storage/driver.storage.fdtable.pas b/src/driver/storage/driver.storage.fdtable.pas index 91cfb76a..71067b32 100644 --- a/src/driver/storage/driver.storage.fdtable.pas +++ b/src/driver/storage/driver.storage.fdtable.pas @@ -42,10 +42,7 @@ type FileName : pchar; { filename within that directory } VFSDir : pchar; { full VFS parent directory path, e.g. '/sys' } OpenMode : uint8; { TOpenMode ordinal — avoids VFS type dependency } - DataBuffer : puint32; { pre-loaded data for small files, nil otherwise } - DataSize : uint32; { size of pre-loaded data in bytes } - Loaded : boolean; { true if pre-loaded into DataBuffer } - StreamOff : uint32; { current byte offset for streaming / on-demand reads } + DataSize : uint32; { cached file size in bytes (from fileSizeCallback) } DeviceOps : pointer; { PVFSDeviceOps — non-nil for device FDs } DeviceData : pointer; { opaque data passed to device callbacks } end; @@ -81,6 +78,10 @@ function fd_close(table : PFDTable; handle : uint32) : boolean; Used during process cleanup. } procedure fd_close_all(table : PFDTable); +{ Close all file descriptors in the table that reference a specific volume. + Called when a volume is removed to prevent dangling pointers. } +procedure fd_close_for_volume(table : PFDTable; vol : PStorage_Volume); + implementation function fd_table_new : PFDTable; @@ -98,10 +99,6 @@ begin if entry = nil then exit; if not entry^.InUse then exit; - if entry^.DataBuffer <> nil then begin - kfree(entry^.DataBuffer); - entry^.DataBuffer := nil; - end; if entry^.Directory <> nil then begin kfree(void(entry^.Directory)); entry^.Directory := nil; @@ -116,9 +113,7 @@ begin end; entry^.InUse := false; - entry^.Loaded := false; entry^.DataSize := 0; - entry^.StreamOff := 0; entry^.DeviceOps := nil; entry^.DeviceData := nil; end; @@ -174,4 +169,16 @@ begin end; end; +procedure fd_close_for_volume(table : PFDTable; vol : PStorage_Volume); +var + i : uint32; +begin + if table = nil then exit; + if vol = nil then exit; + for i := 0 to MAX_FDS - 1 do begin + if table^.Entries[i].InUse and (table^.Entries[i].Volume = vol) then + fd_free_entry(@table^.Entries[i]); + end; +end; + end. diff --git a/src/driver/storage/driver.storage.filedispatch.pas b/src/driver/storage/driver.storage.filedispatch.pas index 74ea79d1..8c99a8ac 100644 --- a/src/driver/storage/driver.storage.filedispatch.pas +++ b/src/driver/storage/driver.storage.filedispatch.pas @@ -219,10 +219,9 @@ begin validity := driver.storage.vfs.PathValid(absPath); if validity <> pvFile then exit; - { 2. Open the file in stream mode — only reads the bytes we ask for, - avoids pre-loading the entire file just to check a few magic bytes. } + { 2. Open the file for reading — reads the magic bytes on demand. } err := eNone; - fh := driver.storage.vfs.OpenFile(absPath, omStream, @err); + fh := driver.storage.vfs.OpenFile(absPath, omRead, @err); if fh = 0 then exit; memset(uint32(@headerBuf[0]), 0, MAX_MAGIC_LEN); diff --git a/src/driver/storage/driver.storage.mgr.pas b/src/driver/storage/driver.storage.mgr.pas index 0ab06d60..e6712bf9 100644 --- a/src/driver/storage/driver.storage.mgr.pas +++ b/src/driver/storage/driver.storage.mgr.pas @@ -185,7 +185,8 @@ begin { Initialise new Phase 2 fields } device^.removed := false; device^.requestQueue := CFIFO_New(SizeOf(TIORequest), 32); - device^.activeRequest := nil; + device^.activeCount := 0; + if device^.maxActive = 0 then device^.maxActive := 1; device^.cachedMBR := nil; device^.isBootDevice := false; { dispatchRead/dispatchWrite: left as-is — driver sets them before @@ -343,36 +344,36 @@ var reqBuf : TIORequest; heapReq : PIORequest; begin - if device^.activeRequest <> nil then exit; { hardware busy } - if not CFIFO_Dequeue(device^.requestQueue, @reqBuf) then exit; { queue empty } + { Dispatch queued requests up to maxActive hardware slots } + while device^.activeCount < device^.maxActive do begin + if not CFIFO_Dequeue(device^.requestQueue, @reqBuf) then break; { queue empty } - { Heap copy for ISR safety — driver completion may fire on any stack } - heapReq := ioreq_copy(@reqBuf); - if heapReq = nil then begin - { Out of memory — notify caller via callback } - if reqBuf.Callback <> nil then - reqBuf.Callback(eOutOfMemory, reqBuf.CallbackData); - exit; - end; - - device^.activeRequest := heapReq; - heapReq^.State := iosDispatched; - - case heapReq^.RequestType of - ioRead: begin - if device^.dispatchRead <> nil then - device^.dispatchRead(device, heapReq) - else begin - { No new dispatch — shouldn't happen if submit_io chose this path, - but handle defensively } - complete_io(heapReq, false, eDeviceNotFound); - end; + { Heap copy for ISR safety — driver completion may fire on any stack } + heapReq := ioreq_copy(@reqBuf); + if heapReq = nil then begin + { Out of memory — notify caller via callback } + if reqBuf.Callback <> nil then + reqBuf.Callback(eOutOfMemory, reqBuf.CallbackData); + break; end; - ioWrite: begin - if device^.dispatchWrite <> nil then - device^.dispatchWrite(device, heapReq) - else - complete_io(heapReq, false, eDeviceNotFound); + + device^.activeCount := device^.activeCount + 1; + heapReq^.State := iosDispatched; + + case heapReq^.RequestType of + ioRead: begin + if device^.dispatchRead <> nil then + device^.dispatchRead(device, heapReq) + else begin + complete_io(heapReq, false, eDeviceNotFound); + end; + end; + ioWrite: begin + if device^.dispatchWrite <> nil then + device^.dispatchWrite(device, heapReq) + else + complete_io(heapReq, false, eDeviceNotFound); + end; end; end; end; @@ -472,9 +473,10 @@ begin request^.Error := error; end; - { Clear active slot on the device } + { Decrement active count on the device } if device <> nil then - device^.activeRequest := nil; + if device^.activeCount > 0 then + device^.activeCount := device^.activeCount - 1; { Invoke completion callback (sync path uses sync_io_callback, async path uses caller's callback) } if request^.Callback <> nil then diff --git a/src/driver/storage/driver.storage.types.pas b/src/driver/storage/driver.storage.types.pas index c630b1f6..c76108ca 100644 --- a/src/driver/storage/driver.storage.types.pas +++ b/src/driver/storage/driver.storage.types.pas @@ -89,7 +89,7 @@ type { VFS / FD errors } eTooManyOpenFiles, { per-process FD table full } eInvalidHandle, { TFileHandle does not refer to an open FD } - eNotStreamMode, { SeekFile called on a non-omStream handle } + eFileNotLoaded, { ReadFile on a pre-load handle whose data is not yet loaded } eAlreadyExists, { generic: target name already exists (symlink, file, etc.) } @@ -140,8 +140,6 @@ type TIOCallback = procedure(error : TError; userdata : pointer); { Filesystem callback core.types } - PPWriteHook = procedure(volume : PStorage_volume; directory : pchar; entry : PDirectory_Entry; byteCount : uint32; buffer : puint32; statusOut : puint32); - PPReadHook = function(volume : PStorage_Volume; directory : pchar; fileName : pchar; buffer : puint32; bytecount : puint32) : uint32; PPCreateHook = procedure(volume : PStorage_volume; start : uint32; size : uint32; config : puint32); PPCreateAsyncHook = procedure(volume : PStorage_volume; start : uint32; size : uint32; config : puint32; callback : TIOCallback; callbackData : pointer); PPDetectHook = procedure(disk : PStorage_Device); @@ -152,23 +150,22 @@ type PPIdentifyHook = function(volume : PStorage_Volume) : boolean; PPRenameFileHook = procedure(volume : PStorage_Volume; filePath : pchar; newName : pchar; status : puint32); { Offset-based read: reads byteCount bytes starting at byte offset into the file. - Returns the number of bytes actually read. - Filesystems that do not implement this leave the field nil and VFS falls back - to the load-all readCallback behaviour. } + Returns the number of bytes actually read. } PPReadOffsetHook = function(volume : PStorage_Volume; directory : pchar; fileName : pchar; offset : uint32; buffer : puint32; byteCount : uint32) : uint32; { Offset-based write: writes byteCount bytes starting at byte offset into the file. Returns the number of bytes actually written. Filesystems that do not implement this leave the field nil; VFS returns 0 for stream-mode writes on volume FDs. } PPWriteOffsetHook = function(volume : PStorage_Volume; directory : pchar; fileName : pchar; offset : uint32; buffer : puint32; byteCount : uint32) : uint32; + { File size query: returns the size in bytes of the named file. + Returns 0 if the file does not exist or has zero length. } + PPFileSizeHook = function(volume : PStorage_Volume; directory : pchar; fileName : pchar) : uint32; { === Async filesystem hook core.types === Each mirrors the corresponding sync hook but receives a TIOCallback + callbackData. The implementation must return immediately; the callback is fired when the operation completes from worker-process context (IF=1, safe to block). } PPLinkedListBase = ^PLinkedListBase; { needed for PPReadDirAsyncHook parameter } - PPWriteAsyncHook = procedure(volume : PStorage_Volume; directory : pchar; entry : PDirectory_Entry; byteCount : uint32; buffer : puint32; callback : TIOCallback; callbackData : pointer); - PPReadAsyncHook = procedure(volume : PStorage_Volume; directory : pchar; fileName : pchar; buffer : puint32; byteCount : puint32; callback : TIOCallback; callbackData : pointer); PPCreateDirAsyncHook = procedure(volume : PStorage_Volume; directory : pchar; dirname : pchar; attributes : uint32; status : puint32; callback : TIOCallback; callbackData : pointer); PPReadDirAsyncHook = procedure(volume : PStorage_Volume; directory : pchar; resultList : PPLinkedListBase; status : puint32; callback : TIOCallback; callbackData : pointer); PPDeleteFileAsyncHook = procedure(volume : PStorage_Volume; filePath : pchar; status : puint32; callback : TIOCallback; callbackData : pointer); @@ -211,7 +208,8 @@ type dispatchRead : TDriverDispatch; { nil if driver not yet migrated } dispatchWrite : TDriverDispatch; { nil if driver not yet migrated } requestQueue : PCFIFOQueue; { per-device I/O request CFIFO } - activeRequest : PIORequest; { currently dispatched to HW, or nil } + activeCount : uint32; { number of commands currently dispatched to HW } + maxActive : uint32; { max concurrent commands (NCQ depth) } maxSectorCount : uint32; sectorSize : uint32; //in bytes @@ -224,8 +222,6 @@ type TFilesystem = record sName : pchar; system_id : uint8; - writeCallback : PPWriteHook; - readCallback : PPReadHook; createCallback : PPCreateHook; detectCallback : PPDetectHook; createDirCallback : PPCreateDirHook; @@ -237,12 +233,10 @@ type readOffsetCallback : PPReadOffsetHook; { Offset-based write for streaming mode — nil if not implemented } writeOffsetCallback : PPWriteOffsetHook; + { File size query — returns size in bytes without loading the file } + fileSizeCallback : PPFileSizeHook; { Async format — nil if FS only supports synchronous create } createAsyncCallback : PPCreateAsyncHook; - { Async variants of the main I/O hooks — nil if FS only supports sync. - Sync VFS functions will call these + spin-wait when available. } - writeAsyncCallback : PPWriteAsyncHook; - readAsyncCallback : PPReadAsyncHook; createDirAsyncCallback : PPCreateDirAsyncHook; readDirAsyncCallback : PPReadDirAsyncHook; deleteFileAsyncCallback : PPDeleteFileAsyncHook; @@ -260,6 +254,7 @@ type freeSectors : uint32; filesystem : PFilesystem; isBootDrive : boolean; + fsPrivate : pointer; { FS-driver private data (e.g. PFATCache for FAT32) } end; { Generic directory entry } diff --git a/src/driver/storage/driver.storage.vfs.pas b/src/driver/storage/driver.storage.vfs.pas index 26da5737..ebbb68f1 100644 --- a/src/driver/storage/driver.storage.vfs.pas +++ b/src/driver/storage/driver.storage.vfs.pas @@ -34,7 +34,7 @@ uses debug.tracer; type - TOpenMode = (omRead, omWrite, omCreate, omReadWrite, omStream); + TOpenMode = (omRead, omWrite, omCreate, omReadWrite); TIsPathValid = (pvInvalid, pvFile, pvDirectory); TRegError = (pvUnknown, pvNotRegistered, pvRegistered, pvUnregistered); @@ -119,7 +119,6 @@ function DeleteFile(Path : pchar; Error : PError) : TError; function DeleteDirectory(Path : pchar; Error : PError) : TError; function CreateDirectory(Path : pchar; Error : PError) : TError; function RenameFile(OldPath : pchar; NewName : pchar; Error : PError) : TError; -function SeekFile(Handle : TFileHandle; Offset : uint32) : TError; function FileSizeFromHandle(Handle : TFileHandle) : uint32; { Symlinks } @@ -132,6 +131,11 @@ function RegisterDevice(Path : pchar; Ops : PVFSDeviceOps; DevData : pointer) : function WatchDirectory(Path : pchar; Callback : TVFSWatchCallback; UserData : pointer) : uint32; procedure UnwatchDirectory(WatchID : uint32); +{ Volume invalidation — must be called before freeing a volume. + Closes all open FDs referencing the volume across all processes, + evicts directory cache entries, and removes VFS mount points. } +procedure InvalidateVolume(vol : PStorage_Volume); + { VFS Functions } function newVirtualDirectory(Path : pchar) : TError; procedure RemoveVirtualTree(Path : pchar); @@ -164,18 +168,6 @@ const pointer-to-pointer aliases. =========================================================================== } type - { Used by OpenFileAsync to update the FD when the async read finishes } - TVFSOpenAsyncCtx = record - FD : PFileDescriptor; - DataBuf : puint32; { holder for data pointer — freed by vfs_open_complete } - DataSize : puint32; { holder for byte count — freed by vfs_open_complete } - OpenMode : TOpenMode; - UserError : PError; - UserCallback: TIOCallback; - UserData : pointer; - end; - PVFSOpenAsyncCtx = ^TVFSOpenAsyncCtx; - { GetDirectoryListingAsync context } TVFSDirAsyncCtx = record Path : pchar; { absolute path to list (freed by completion) } @@ -215,46 +207,6 @@ var DirCacheTick : uint32; DirCacheReady : boolean; -{ ===================== Async helpers ======================================== - Used by OpenFileAsync to stitch results back into the file descriptor. - =========================================================================== } - -{ Completion callback for OpenFileAsync: stitches data into the FD, - frees temporary holders, then fires the caller's callback. } -procedure vfs_open_complete(error : TError; userdata : pointer); -var - ctx : PVFSOpenAsyncCtx; -begin - ctx := PVFSOpenAsyncCtx(userdata); - if error = eNone then begin - ctx^.FD^.DataBuffer := puint32(ctx^.DataBuf^); - ctx^.FD^.DataSize := ctx^.DataSize^; - ctx^.FD^.Loaded := true; - if ctx^.UserError <> nil then ctx^.UserError^ := eNone; - end else begin - { File not found — OK for ReadWrite mode, error for ReadOnly } - if ctx^.OpenMode = omReadWrite then begin - if ctx^.UserError <> nil then ctx^.UserError^ := eNone; - end else begin - if ctx^.UserError <> nil then ctx^.UserError^ := eFileDoesNotExist; - ctx^.FD^.InUse := false; - if ctx^.FD^.Directory <> nil then begin - kfree(void(ctx^.FD^.Directory)); - ctx^.FD^.Directory := nil; - end; - if ctx^.FD^.FileName <> nil then begin - kfree(void(ctx^.FD^.FileName)); - ctx^.FD^.FileName := nil; - end; - end; - end; - kfree(puint32(ctx^.DataBuf)); - kfree(puint32(ctx^.DataSize)); - if ctx^.UserCallback <> nil then - ctx^.UserCallback(error, ctx^.UserData); - kfree(void(ctx)); -end; - { ===================== Watch/Notify infrastructure ========================= } procedure InitWatchTable; @@ -460,6 +412,87 @@ end; { =========================================================================== } +{ ===================== Volume Invalidation ================================= } +{ Called before a volume is freed. Closes all open FDs referencing the volume + across every process, evicts directory cache entries, and removes VFS mount + point(s) so no dangling pointers remain. } + +{ Forward declarations } +function GetObjectFromPath(path : pchar) : PVFSObject; forward; +function GetObjectFromPathEx(path : pchar; var volRelPath : pchar) : PVFSObject; forward; + +procedure InvalidateVolume(vol : PStorage_Volume); +var + pCount : uint32; + pi : uint32; + pCtx : proc.types.PProcessContext; + tbl : PFDTable; + diskObj : PVFSObject; + bootObj : PVFSObject; + ht : PHashMap; + si : uint32; + item : PHashItem; + child : PVFSObject; +begin + debug.tracer.push_trace('driver.storage.vfs.InvalidateVolume.enter'); + if vol = nil then exit; + + { 1. Evict all directory cache entries for this volume } + DirCache_Invalidate(vol, nil); + + { 2. Close all FDs referencing this volume across every process } + pCount := proc.mgr.processCount(); + if pCount > 0 then begin + for pi := 0 to pCount - 1 do begin + pCtx := proc.mgr.getProcessByIndex(pi); + if pCtx = nil then continue; + tbl := PFDTable(pCtx^.FDTable); + fd_close_for_volume(tbl, vol); + end; + end; + + { 3. Remove VFS mount objects referencing this volume. + Volumes are mounted as otDRIVE children of /disk/ and optionally /boot. } + diskObj := GetObjectFromPath('/disk'); + if (diskObj <> nil) and (diskObj^.ObjectType = otVDIRECTORY) then begin + ht := PHashMap(diskObj^.Reference); + if (ht <> nil) and (ht^.Size > 0) and (ht^.Table <> nil) then begin + for si := 0 to ht^.Size - 1 do begin + item := ht^.Table[si]; + while item <> nil do begin + child := PVFSObject(item^.Data); + if (child <> nil) and (child^.ObjectType = otDRIVE) + and (PStorage_Volume(child^.Reference) = vol) then begin + core.ds.hashmap.delete(ht, item^.Key, false); + kfree(void(child^.ObjectName)); + kfree(void(child)); + break; { hashmap modified, stop iterating } + end; + item := item^.Next; + end; + end; + end; + end; + + { Check /boot mount } + bootObj := GetObjectFromPath('/boot'); + if (bootObj <> nil) and (bootObj^.ObjectType = otDRIVE) + and (PStorage_Volume(bootObj^.Reference) = vol) then begin + diskObj := bootObj^.Parent; + if diskObj <> nil then begin + ht := PHashMap(diskObj^.Reference); + if ht <> nil then + core.ds.hashmap.delete(ht, bootObj^.ObjectName, false); + end; + kfree(void(bootObj^.ObjectName)); + kfree(void(bootObj)); + end; + + debug.tracer.push_trace('driver.storage.vfs.InvalidateVolume.exit'); +end; + +{ =========================================================================== } + { Return a heap-allocated copy of the parent directory portion of an absolute VFS path. '/sys/hello.txt' -> '/sys', '/hello.txt' -> '/'. } function vfsParentDir(path : pchar) : pchar; @@ -1112,9 +1145,6 @@ var vol : PStorage_Volume; dir : pchar; fname : pchar; - dataBuf : puint32; - dataSize : puint32; - readErr : uint32; absPath : pchar; vfsObj : PVFSObject; devInfo : PVFSDevice; @@ -1156,10 +1186,7 @@ begin fd^.FileName := stringCopy(vfsObj^.ObjectName); fd^.VFSDir := nil; { devices have no parent directory } fd^.OpenMode := uint8(ord(OpenMode)); - fd^.DataBuffer := nil; fd^.DataSize := 0; - fd^.Loaded := false; - fd^.StreamOff := 0; fd^.DeviceOps := pointer(devInfo^.Ops); fd^.DeviceData := devInfo^.DevData; if (devInfo^.Ops <> nil) and (devInfo^.Ops^.Size <> nil) then @@ -1192,34 +1219,16 @@ begin fd^.FileName := fname; fd^.VFSDir := vfsParentDir(Filename); fd^.OpenMode := uint8(ord(OpenMode)); - fd^.DataBuffer := nil; fd^.DataSize := 0; - fd^.Loaded := false; - fd^.StreamOff := 0; - { If reading, load the file data now } + { Query file size for read modes; for write-only just leave DataSize = 0 } if (OpenMode = omRead) or (OpenMode = omReadWrite) then begin if vol^.filesystem <> nil then begin - dataBuf := puint32(kalloc(4)); - dataBuf^ := 0; - dataSize := puint32(kalloc(4)); - dataSize^ := 0; - - if vol^.filesystem^.readCallback <> nil then begin - { Sync path: driver.storage.fs.fat32 uses submit_io_wait which parks the calling - process via psAwaiting until the driver.storage.ctl.ahci ISR completes the I/O. } - readErr := vol^.filesystem^.readCallback(vol, dir, fname, dataBuf, dataSize); - end else begin - readErr := 1; - end; - - if readErr = 0 then begin - fd^.DataBuffer := puint32(dataBuf^); - fd^.DataSize := dataSize^; - fd^.Loaded := true; + if vol^.filesystem^.fileSizeCallback <> nil then begin + fd^.DataSize := vol^.filesystem^.fileSizeCallback(vol, dir, fname); if Error <> nil then Error^ := eNone; end else begin - { File doesn't exist on disk — OK for write mode } + { No fileSizeCallback — treat as file-not-found for read-only } if OpenMode = omReadWrite then begin if Error <> nil then Error^ := eNone; end else begin @@ -1229,17 +1238,11 @@ begin kfree(void(fname)); fd^.Directory := nil; fd^.FileName := nil; - kfree(puint32(dataBuf)); - kfree(puint32(dataSize)); exit; end; end; - - kfree(puint32(dataBuf)); - kfree(puint32(dataSize)); end; end else begin - { Write-only or streaming mode — no pre-load } if Error <> nil then Error^ := eNone; end; @@ -1251,10 +1254,6 @@ function WriteFile(FileHandle : TFileHandle; Position : uint32; Buffer : puint8; var fd : PFileDescriptor; vol : PStorage_Volume; - dirEntry : TDirectory_Entry; - status : puint32; - padBuf : puint32; - padSize : uint32; begin debug.tracer.push_trace('driver.storage.vfs.WriteFile.enter'); WriteFile := 0; @@ -1262,32 +1261,13 @@ begin fd := fd_get(currentFDTable, FileHandle); if fd = nil then exit; - if (TOpenMode(fd^.OpenMode) <> omWrite) and (TOpenMode(fd^.OpenMode) <> omCreate) and (TOpenMode(fd^.OpenMode) <> omReadWrite) and (TOpenMode(fd^.OpenMode) <> omStream) then exit; + if (TOpenMode(fd^.OpenMode) <> omWrite) and (TOpenMode(fd^.OpenMode) <> omCreate) and (TOpenMode(fd^.OpenMode) <> omReadWrite) then exit; if Buffer = nil then exit; { Device dispatch: use the device write callback } if fd^.DeviceOps <> nil then begin - if PVFSDeviceOps(fd^.DeviceOps)^.Write <> nil then begin - if TOpenMode(fd^.OpenMode) = omStream then begin - WriteFile := PVFSDeviceOps(fd^.DeviceOps)^.Write(fd^.DeviceData, fd^.StreamOff, Buffer, Length); - fd^.StreamOff := fd^.StreamOff + WriteFile; - end else - WriteFile := PVFSDeviceOps(fd^.DeviceOps)^.Write(fd^.DeviceData, Position, Buffer, Length); - end; - debug.tracer.push_trace('driver.storage.vfs.WriteFile.exit'); - exit; - end; - - { Streaming mode for volume FDs: use writeOffsetCallback } - if TOpenMode(fd^.OpenMode) = omStream then begin - vol := fd^.Volume; - if vol = nil then exit; - if vol^.filesystem = nil then exit; - if vol^.filesystem^.writeOffsetCallback = nil then exit; - WriteFile := vol^.filesystem^.writeOffsetCallback( - vol, fd^.Directory, fd^.FileName, - fd^.StreamOff, puint32(Buffer), Length); - fd^.StreamOff := fd^.StreamOff + WriteFile; + if PVFSDeviceOps(fd^.DeviceOps)^.Write <> nil then + WriteFile := PVFSDeviceOps(fd^.DeviceOps)^.Write(fd^.DeviceData, Position, Buffer, Length); debug.tracer.push_trace('driver.storage.vfs.WriteFile.exit'); exit; end; @@ -1295,41 +1275,16 @@ begin vol := fd^.Volume; if vol = nil then exit; if vol^.filesystem = nil then exit; + if vol^.filesystem^.writeOffsetCallback = nil then exit; - { Build a TDirectory_Entry for the write callback } - dirEntry.fileName := fd^.FileName; - dirEntry.entryType := fileEntry; - dirEntry.fileSize := Length; - dirEntry.modifiedDate := 0; - dirEntry.modifiedTime := 0; - dirEntry.attributes := 0; - - { Filesystem writeFile may read full sectors from the buffer regardless of Length. - Pad to at least 4096 bytes to prevent reading past the allocation. } - padSize := Length; - if padSize < 4096 then padSize := 4096; - padBuf := puint32(kalloc(padSize)); - memset(uint32(padBuf), 0, padSize); - if Length > 0 then - core.util.memcpy(uint32(Buffer), uint32(padBuf), Length); - - if vol^.filesystem^.writeCallback <> nil then begin - { Sync path: driver.storage.fs.fat32 uses submit_io_wait which parks the calling process - via psAwaiting until the driver.storage.ctl.ahci ISR completes the I/O. } - status := puint32(kalloc(4)); - status^ := 0; - vol^.filesystem^.writeCallback(vol, fd^.Directory, @dirEntry, Length, padBuf, status); - if status^ = 0 then begin - WriteFile := Length; - { Invalidate cache + fire watch notification so open file - browsers see the updated size / new entry. } - VFS_OnMutation(weModified, fd^.VFSDir, fd^.VFSDir, vol, fd^.Directory); - end else - WriteFile := 0; - kfree(puint32(status)); - kfree(padBuf); - end else begin - kfree(padBuf); + WriteFile := vol^.filesystem^.writeOffsetCallback( + vol, fd^.Directory, fd^.FileName, + Position, puint32(Buffer), Length); + if WriteFile > 0 then begin + { Update cached file size if write extended the file } + if Position + WriteFile > fd^.DataSize then + fd^.DataSize := Position + WriteFile; + VFS_OnMutation(weModified, fd^.VFSDir, fd^.VFSDir, vol, fd^.Directory); end; debug.tracer.push_trace('driver.storage.vfs.WriteFile.exit'); @@ -1350,46 +1305,28 @@ begin if fd^.DeviceOps <> nil then begin if PVFSDeviceOps(fd^.DeviceOps)^.Read = nil then exit; if Buffer = nil then exit; - if TOpenMode(fd^.OpenMode) = omStream then begin - ReadFile := PVFSDeviceOps(fd^.DeviceOps)^.Read(fd^.DeviceData, fd^.StreamOff, Buffer, Length); - fd^.StreamOff := fd^.StreamOff + ReadFile; - end else begin - ReadFile := PVFSDeviceOps(fd^.DeviceOps)^.Read(fd^.DeviceData, Position, Buffer, Length); - end; + ReadFile := PVFSDeviceOps(fd^.DeviceOps)^.Read(fd^.DeviceData, Position, Buffer, Length); debug.tracer.push_trace('driver.storage.vfs.ReadFile.exit'); exit; end; - { Streaming mode: call readOffsetCallback on demand — no pre-loaded buffer needed } - if TOpenMode(fd^.OpenMode) = omStream then begin - if Buffer = nil then exit; - if fd^.Volume = nil then exit; - if fd^.Volume^.filesystem = nil then exit; - if fd^.Volume^.filesystem^.readOffsetCallback = nil then exit; - ReadFile := fd^.Volume^.filesystem^.readOffsetCallback( - fd^.Volume, fd^.Directory, fd^.FileName, - fd^.StreamOff, puint32(Buffer), Length); - fd^.StreamOff := fd^.StreamOff + ReadFile; - debug.tracer.push_trace('driver.storage.vfs.ReadFile.exit'); - exit; - end; - - if not fd^.Loaded then exit; - if fd^.DataBuffer = nil then exit; + { On-demand read via readOffsetCallback } if Buffer = nil then exit; - - { Copy from loaded buffer at Position into caller's buffer } + if fd^.Volume = nil then exit; + if fd^.Volume^.filesystem = nil then exit; + if fd^.Volume^.filesystem^.readOffsetCallback = nil then exit; if Position >= fd^.DataSize then exit; - { Clamp copyLen without relying on (Position + copyLen) which can overflow } + { Clamp read length to remaining file size } copyLen := fd^.DataSize - Position; if Length < copyLen then copyLen := Length; if copyLen > 0 then - core.util.memcpy(uint32(fd^.DataBuffer) + Position, uint32(Buffer), copyLen); + ReadFile := fd^.Volume^.filesystem^.readOffsetCallback( + fd^.Volume, fd^.Directory, fd^.FileName, + Position, puint32(Buffer), copyLen); - ReadFile := copyLen; debug.tracer.push_trace('driver.storage.vfs.ReadFile.exit'); end; @@ -1433,8 +1370,6 @@ procedure WriteFileAsync(FileHandle : TFileHandle; Position : uint32; Buffer : p var fd : PFileDescriptor; vol : PStorage_Volume; - dirEntry : TDirectory_Entry; - padBuf : puint32; padSize : uint32; begin debug.tracer.push_trace('driver.storage.vfs.WriteFileAsync.enter'); @@ -1445,7 +1380,7 @@ begin exit; end; - if (TOpenMode(fd^.OpenMode) <> omWrite) and (TOpenMode(fd^.OpenMode) <> omCreate) and (TOpenMode(fd^.OpenMode) <> omReadWrite) and (TOpenMode(fd^.OpenMode) <> omStream) then begin + if (TOpenMode(fd^.OpenMode) <> omWrite) and (TOpenMode(fd^.OpenMode) <> omCreate) and (TOpenMode(fd^.OpenMode) <> omReadWrite) then begin if Callback <> nil then Callback(eReadOnly, CallbackData); exit; end; @@ -1457,36 +1392,15 @@ begin { Device dispatch: sync call + immediate callback } if fd^.DeviceOps <> nil then begin - if PVFSDeviceOps(fd^.DeviceOps)^.Write <> nil then begin - if TOpenMode(fd^.OpenMode) = omStream then begin - padSize := PVFSDeviceOps(fd^.DeviceOps)^.Write(fd^.DeviceData, fd^.StreamOff, Buffer, Length); - fd^.StreamOff := fd^.StreamOff + padSize; - end else - padSize := PVFSDeviceOps(fd^.DeviceOps)^.Write(fd^.DeviceData, Position, Buffer, Length); - end else + if PVFSDeviceOps(fd^.DeviceOps)^.Write <> nil then + padSize := PVFSDeviceOps(fd^.DeviceOps)^.Write(fd^.DeviceData, Position, Buffer, Length) + else padSize := 0; if Callback <> nil then Callback(eNone, CallbackData); debug.tracer.push_trace('driver.storage.vfs.WriteFileAsync.exit'); exit; end; - { Streaming mode for volume FDs: use writeOffsetCallback } - if TOpenMode(fd^.OpenMode) = omStream then begin - vol := fd^.Volume; - if (vol = nil) or (vol^.filesystem = nil) - or (vol^.filesystem^.writeOffsetCallback = nil) then begin - if Callback <> nil then Callback(eNotSupported, CallbackData); - exit; - end; - padSize := vol^.filesystem^.writeOffsetCallback( - vol, fd^.Directory, fd^.FileName, - fd^.StreamOff, puint32(Buffer), Length); - fd^.StreamOff := fd^.StreamOff + padSize; - if Callback <> nil then Callback(eNone, CallbackData); - debug.tracer.push_trace('driver.storage.vfs.WriteFileAsync.exit'); - exit; - end; - vol := fd^.Volume; if (vol = nil) or (vol^.filesystem = nil) then begin if Callback <> nil then Callback(eInvalidArgument, CallbackData); @@ -1499,39 +1413,14 @@ begin finished and the fresh read will return the updated listing. } VFS_OnMutation(weModified, fd^.VFSDir, fd^.VFSDir, vol, fd^.Directory); - { Prefer async hook; fall back to sync if not available } - if vol^.filesystem^.writeAsyncCallback <> nil then begin - dirEntry.fileName := fd^.FileName; - dirEntry.entryType := fileEntry; - dirEntry.fileSize := Length; - dirEntry.modifiedDate := 0; - dirEntry.modifiedTime := 0; - dirEntry.attributes := 0; - padSize := Length; - if padSize < 4096 then padSize := 4096; - padBuf := puint32(kalloc(padSize)); - core.util.memset(uint32(padBuf), 0, padSize); - if Length > 0 then - core.util.memcpy(uint32(Buffer), uint32(padBuf), Length); - { driver.storage.fs.fat32 writeFile_async deep-copies padBuf synchronously before returning } - vol^.filesystem^.writeAsyncCallback(vol, fd^.Directory, @dirEntry, Length, padBuf, Callback, CallbackData); - kfree(padBuf); - end else if vol^.filesystem^.writeCallback <> nil then begin - { Sync fallback — call directly and fake immediate completion } - dirEntry.fileName := fd^.FileName; - dirEntry.entryType := fileEntry; - dirEntry.fileSize := Length; - dirEntry.modifiedDate := 0; - dirEntry.modifiedTime := 0; - dirEntry.attributes := 0; - padSize := Length; - if padSize < 4096 then padSize := 4096; - padBuf := puint32(kalloc(padSize)); - core.util.memset(uint32(padBuf), 0, padSize); - if Length > 0 then - core.util.memcpy(uint32(Buffer), uint32(padBuf), Length); - vol^.filesystem^.writeCallback(vol, fd^.Directory, @dirEntry, Length, padBuf, nil); - kfree(padBuf); + if vol^.filesystem^.writeOffsetCallback <> nil then begin + padSize := vol^.filesystem^.writeOffsetCallback( + vol, fd^.Directory, fd^.FileName, + Position, puint32(Buffer), Length); + if padSize > 0 then begin + if Position + padSize > fd^.DataSize then + fd^.DataSize := Position + padSize; + end; if Callback <> nil then Callback(eNone, CallbackData); end else begin if Callback <> nil then Callback(eNotSupported, CallbackData); @@ -1540,9 +1429,9 @@ begin debug.tracer.push_trace('driver.storage.vfs.WriteFileAsync.exit'); end; -{ OpenFileAsync — resolve the path, allocate an FD, kick off async data load. +{ OpenFileAsync — resolve the path, allocate an FD, query file size. OutHandle is set before returning; the FD is not usable until Callback fires. - If OpenMode is write-only or stream, Callback fires immediately (no disk read). } + If OpenMode is write-only, Callback fires immediately (no disk read). } procedure OpenFileAsync(Filename : pchar; OpenMode : TOpenMode; var OutHandle : TFileHandle; Error : PError; Callback : TIOCallback; CallbackData : pointer); var tbl : PFDTable; @@ -1551,9 +1440,6 @@ var vol : PStorage_Volume; dir : pchar; fname : pchar; - dataBuf : puint32; - dataSize: puint32; - octx : PVFSOpenAsyncCtx; begin debug.tracer.push_trace('driver.storage.vfs.OpenFileAsync.enter'); OutHandle := 0; @@ -1599,61 +1485,28 @@ begin fd^.FileName := fname; fd^.VFSDir := vfsParentDir(Filename); fd^.OpenMode := uint8(ord(OpenMode)); - fd^.DataBuffer:= nil; fd^.DataSize := 0; - fd^.Loaded := false; - fd^.StreamOff := 0; OutHandle := slot; if (OpenMode = omRead) or (OpenMode = omReadWrite) then begin - if (vol^.filesystem <> nil) and (vol^.filesystem^.readAsyncCallback <> nil) then begin - { True async: build completion context; callback fires when data is loaded } - dataBuf := puint32(kalloc(4)); - dataBuf^ := 0; - dataSize := puint32(kalloc(4)); - dataSize^ := 0; - octx := PVFSOpenAsyncCtx(kalloc(sizeof(TVFSOpenAsyncCtx))); - octx^.FD := fd; - octx^.DataBuf := dataBuf; - octx^.DataSize := dataSize; - octx^.OpenMode := OpenMode; - octx^.UserError := Error; - octx^.UserCallback := Callback; - octx^.UserData := CallbackData; - vol^.filesystem^.readAsyncCallback(vol, dir, fname, dataBuf, dataSize, @vfs_open_complete, octx); - { Return immediately — caller must not use OutHandle until Callback fires } - end else if (vol^.filesystem <> nil) and (vol^.filesystem^.readCallback <> nil) then begin - { Sync fallback: load now, then fire callback } - dataBuf := puint32(kalloc(4)); - dataBuf^ := 0; - dataSize := puint32(kalloc(4)); - dataSize^ := 0; - if vol^.filesystem^.readCallback(vol, dir, fname, dataBuf, dataSize) = 0 then begin - fd^.DataBuffer := puint32(dataBuf^); - fd^.DataSize := dataSize^; - fd^.Loaded := true; + if (vol^.filesystem <> nil) and (vol^.filesystem^.fileSizeCallback <> nil) then begin + fd^.DataSize := vol^.filesystem^.fileSizeCallback(vol, dir, fname); + if Error <> nil then Error^ := eNone; + end else begin + if OpenMode = omReadWrite then begin if Error <> nil then Error^ := eNone; end else begin - if OpenMode = omReadWrite then begin - if Error <> nil then Error^ := eNone; - end else begin - if Error <> nil then Error^ := eFileDoesNotExist; - fd^.InUse := false; - kfree(void(dir)); - kfree(void(fname)); - fd^.Directory := nil; - fd^.FileName := nil; - OutHandle := 0; - end; + if Error <> nil then Error^ := eFileDoesNotExist; + fd^.InUse := false; + kfree(void(dir)); + kfree(void(fname)); + fd^.Directory := nil; + fd^.FileName := nil; + OutHandle := 0; end; - kfree(puint32(dataBuf)); - kfree(puint32(dataSize)); - if Callback <> nil then Callback(Error^, CallbackData); - end else begin - if Callback <> nil then Callback(eNone, CallbackData); end; + if Callback <> nil then Callback(Error^, CallbackData); end else begin - { Write-only or streaming mode — no file data read needed } if Error <> nil then Error^ := eNone; if Callback <> nil then Callback(eNone, CallbackData); end; @@ -2285,30 +2138,6 @@ begin debug.tracer.push_trace('driver.storage.vfs.CreateDirectory.exit'); end; -function SeekFile(Handle : TFileHandle; Offset : uint32) : TError; -var - fd : PFileDescriptor; -begin - debug.tracer.push_trace('driver.storage.vfs.SeekFile.enter'); - - fd := fd_get(currentFDTable, Handle); - if fd = nil then begin - SeekFile := eInvalidHandle; - debug.tracer.push_trace('driver.storage.vfs.SeekFile.exit'); - exit; - end; - - if TOpenMode(fd^.OpenMode) <> omStream then begin - SeekFile := eNotStreamMode; - debug.tracer.push_trace('driver.storage.vfs.SeekFile.exit'); - exit; - end; - - fd^.StreamOff := Offset; - SeekFile := eNone; - debug.tracer.push_trace('driver.storage.vfs.SeekFile.exit'); -end; - function FileSizeFromHandle(Handle : TFileHandle) : uint32; var fd : PFileDescriptor; @@ -2358,11 +2187,7 @@ begin { Device dispatch: sync call + immediate callback } if fd^.DeviceOps <> nil then begin if PVFSDeviceOps(fd^.DeviceOps)^.Read <> nil then begin - if TOpenMode(fd^.OpenMode) = omStream then begin - copyLen := PVFSDeviceOps(fd^.DeviceOps)^.Read(fd^.DeviceData, fd^.StreamOff, Buffer, Length); - fd^.StreamOff := fd^.StreamOff + copyLen; - end else - copyLen := PVFSDeviceOps(fd^.DeviceOps)^.Read(fd^.DeviceData, Position, Buffer, Length); + copyLen := PVFSDeviceOps(fd^.DeviceOps)^.Read(fd^.DeviceData, Position, Buffer, Length); if BytesRead <> nil then BytesRead^ := copyLen; end else begin if BytesRead <> nil then BytesRead^ := 0; @@ -2372,34 +2197,11 @@ begin exit; end; - { Streaming mode: sync readOffsetCallback, then fire callback } - if TOpenMode(fd^.OpenMode) = omStream then begin - if (fd^.Volume = nil) or (fd^.Volume^.filesystem = nil) - or (fd^.Volume^.filesystem^.readOffsetCallback = nil) then begin - if BytesRead <> nil then BytesRead^ := 0; - if Callback <> nil then Callback(eNotSupported, CallbackData); - exit; - end; - copyLen := fd^.Volume^.filesystem^.readOffsetCallback( - fd^.Volume, fd^.Directory, fd^.FileName, - fd^.StreamOff, puint32(Buffer), Length); - fd^.StreamOff := fd^.StreamOff + copyLen; - if BytesRead <> nil then BytesRead^ := copyLen; - if Callback <> nil then Callback(eNone, CallbackData); - debug.tracer.push_trace('driver.storage.vfs.ReadFileAsync.exit'); - exit; - end; - - { Pre-loaded mode: memcpy from buffer — instant } - if not fd^.Loaded then begin + { On-demand read via readOffsetCallback } + if (fd^.Volume = nil) or (fd^.Volume^.filesystem = nil) + or (fd^.Volume^.filesystem^.readOffsetCallback = nil) then begin if BytesRead <> nil then BytesRead^ := 0; - if Callback <> nil then Callback(eFileNotLoaded, CallbackData); - exit; - end; - - if fd^.DataBuffer = nil then begin - if BytesRead <> nil then BytesRead^ := 0; - if Callback <> nil then Callback(eNone, CallbackData); + if Callback <> nil then Callback(eNotSupported, CallbackData); exit; end; @@ -2414,7 +2216,9 @@ begin copyLen := Length; if copyLen > 0 then - core.util.memcpy(uint32(fd^.DataBuffer) + Position, uint32(Buffer), copyLen); + copyLen := fd^.Volume^.filesystem^.readOffsetCallback( + fd^.Volume, fd^.Directory, fd^.FileName, + Position, puint32(Buffer), copyLen); if BytesRead <> nil then BytesRead^ := copyLen; if Callback <> nil then Callback(eNone, CallbackData); @@ -3399,8 +3203,6 @@ var volNumStr : pchar; mountPath : pchar; prefix : pchar; - dataBuf : puint32; - dataSize : puint32; readErr : uint32; mntPath : pchar; mntLen : uint32; @@ -3472,27 +3274,23 @@ begin if vol = nil then continue; if vol^.filesystem = nil then continue; if not vol^.isBootDrive then continue; - if vol^.filesystem^.readCallback = nil then continue; + if vol^.filesystem^.fileSizeCallback = nil then continue; + if vol^.filesystem^.readOffsetCallback = nil then continue; { Build the boot device path: /disk/vol } volNumStr := intToString(i); volName := stringConcat('/disk/vol', volNumStr); kfree(void(volNumStr)); - dataBuf := puint32(kalloc(4)); - dataBuf^ := 0; - dataSize := puint32(kalloc(4)); - dataSize^ := 0; + mntLen := vol^.filesystem^.fileSizeCallback(vol, '', 'MOUNT.ASR'); - readErr := vol^.filesystem^.readCallback(vol, '', 'MOUNT.ASR', dataBuf, dataSize); - - if (readErr = 0) and (dataSize^ > 0) and (dataBuf^ <> 0) then begin - { Parse the file line by line } - mntLen := dataSize^; + if mntLen > 0 then begin mntPath := pchar(kalloc(mntLen + 1)); - core.util.memcpy(dataBuf^, uint32(mntPath), mntLen); + readErr := vol^.filesystem^.readOffsetCallback(vol, '', 'MOUNT.ASR', 0, puint32(mntPath), mntLen); mntPath[mntLen] := char(0); + if readErr > 0 then begin + lineStart := 0; while lineStart < mntLen do begin { Find end of line } @@ -3561,12 +3359,11 @@ begin lineStart := lineEnd; end; + end; { readErr > 0 } + kfree(void(mntPath)); - kfree(puint32(dataBuf^)); end; - kfree(puint32(dataBuf)); - kfree(puint32(dataSize)); kfree(void(volName)); break; { only one boot volume } end; @@ -4034,15 +3831,15 @@ begin Assert(devBuf[63] = 0, '/dev/zero byte 63 = 0'); CloseFile(devHandle); - { /dev/zero — stream mode: two sequential reads advance offset } - devHandle := OpenFile('/dev/zero', omStream, @devErr); - Assert((devHandle <> 0) and (devErr = eNone), 'OpenFile /dev/zero stream ok'); + { /dev/zero — two reads at different offsets } + devHandle := OpenFile('/dev/zero', omRead, @devErr); + Assert((devHandle <> 0) and (devErr = eNone), 'OpenFile /dev/zero read2 ok'); core.util.memset(uint32(devBuf), $FF, 64); devRead := ReadFile(devHandle, 0, devBuf, 16); - Assert(devRead = 16, '/dev/zero stream read1 = 16'); - Assert(devBuf[0] = 0, '/dev/zero stream read1 byte 0 = 0'); - devRead := ReadFile(devHandle, 0, devBuf, 16); - Assert(devRead = 16, '/dev/zero stream read2 = 16'); + Assert(devRead = 16, '/dev/zero read at 0 = 16'); + Assert(devBuf[0] = 0, '/dev/zero read at 0 byte 0 = 0'); + devRead := ReadFile(devHandle, 16, devBuf, 16); + Assert(devRead = 16, '/dev/zero read at 16 = 16'); CloseFile(devHandle); { /dev/zero — ReadFileAsync device dispatch } @@ -4071,28 +3868,28 @@ begin FreeDirectoryListing(map); end; - { /dev/null — stream write: accepted, advances offset } - devHandle := OpenFile('/dev/null', omStream, @devErr); - Assert((devHandle <> 0) and (devErr = eNone), 'OpenFile /dev/null stream ok'); + { /dev/null — write: accepted } + devHandle := OpenFile('/dev/null', omWrite, @devErr); + Assert((devHandle <> 0) and (devErr = eNone), 'OpenFile /dev/null write ok'); core.util.memset(uint32(devBuf), $AA, 64); devRead := WriteFile(devHandle, 0, devBuf, 20); - Assert(devRead = 20, '/dev/null stream write1 = 20'); - devRead := WriteFile(devHandle, 0, devBuf, 12); - Assert(devRead = 12, '/dev/null stream write2 = 12'); + Assert(devRead = 20, '/dev/null write1 = 20'); + devRead := WriteFile(devHandle, 20, devBuf, 12); + Assert(devRead = 12, '/dev/null write2 = 12'); CloseFile(devHandle); - { /dev/null — stream write via WriteFileAsync } - devHandle := OpenFile('/dev/null', omStream, @devErr); - Assert((devHandle <> 0) and (devErr = eNone), 'OpenFile /dev/null stream async ok'); + { /dev/null — write via WriteFileAsync } + devHandle := OpenFile('/dev/null', omWrite, @devErr); + Assert((devHandle <> 0) and (devErr = eNone), 'OpenFile /dev/null write async ok'); WriteFileAsync(devHandle, 0, devBuf, 16, nil, nil); CloseFile(devHandle); - { /dev/zero — stream write: accepted (discards data) } - devHandle := OpenFile('/dev/zero', omStream, @devErr); - Assert((devHandle <> 0) and (devErr = eNone), 'OpenFile /dev/zero stream write ok'); + { /dev/zero — write: accepted (discards data) } + devHandle := OpenFile('/dev/zero', omWrite, @devErr); + Assert((devHandle <> 0) and (devErr = eNone), 'OpenFile /dev/zero write ok'); core.util.memset(uint32(devBuf), $BB, 64); devRead := WriteFile(devHandle, 0, devBuf, 24); - Assert(devRead = 24, '/dev/zero stream write = 24'); + Assert(devRead = 24, '/dev/zero write = 24'); CloseFile(devHandle); kfree(puint32(devBuf)); diff --git a/src/driver/storage/fs/driver.storage.fs.fat32.pas b/src/driver/storage/fs/driver.storage.fs.fat32.pas index 3b805751..beb91b98 100644 --- a/src/driver/storage/fs/driver.storage.fs.fat32.pas +++ b/src/driver/storage/fs/driver.storage.fs.fat32.pas @@ -107,6 +107,24 @@ type end; PFatVolumeInfo = ^TFatVolumeInfo; + { FAT sector cache — direct-mapped, 64 lines, 32 KB data } + TFATCacheLine = record + SectorIdx : uint32; { which FAT sector is stored here } + Dirty : boolean; { true if written but not flushed to disk } + Valid : boolean; { true if line contains valid data } + end; + + PFATCache = ^TFATCache; + TFATCache = record + Lines : array[0..63] of TFATCacheLine; { 64 cache line tags } + Data : puint32; { kalloc'd 64*512 = 32768 bytes of FAT sector data } + EvictBuf : puint32; { kalloc'd 512-byte scratch for eviction writes } + Busy : boolean; { spinlock: serializes miss-path eviction+load } + FatStart : uint32; { LBA of first FAT sector on disk } + Device : PStorage_Device; { device pointer for I/O } + BootRecord : PBootRecord; { cached boot record (read once) } + end; + { Async format state machine steps } TFmtStep = ( fmtBootSector, @@ -149,6 +167,23 @@ procedure detect_volumes(disk : PStorage_Device); implementation +{ ---- Module-level I/O caches ---- } +var + + { Sequential I/O chain-position cache: stores the last visited + (cluster-index, cluster-number) pair so the next call can resume + instead of walking the entire chain from cluster 0. } + sioc_vol : PStorage_Volume = nil; + sioc_fileClust : uint32 = 0; + sioc_chainIdx : uint32 = 0; + sioc_chainClust : uint32 = 0; + sioc_valid : boolean = false; + + { Free-cluster allocation hint: next findFreeClusters scan starts here + instead of cluster 2, making sequential allocations O(1). } + sioc_allocHint : uint32 = 2; + sioc_allocVol : PStorage_Volume = nil; + { Validate a FAT32 8.3 filename. fullName should be in "NAME.EXT" form. Returns true if valid: name part 1-8 chars, extension 0-3 chars, @@ -307,64 +342,249 @@ begin end; -function readBootRecord(volume : PStorage_volume) : PBootRecord; // need write functions for boot record! +function readBootRecord(volume : PStorage_volume) : PBootRecord; var buffer : puint32; begin - push_trace('driver.storage.fs.fat32.readBootRecord.enter'); - io.syslog.logln('FAT32', 'readBootRecord: enter'); buffer:= puint32(kalloc(512)); memset(uint32(buffer), 0, 512); driver.storage.mgr.storage_read(volume^.device, volume^.sectorStart + 1, 1, buffer); - io.syslog.logln('FAT32', 'readBootRecord: done'); readBootRecord:= PBootRecord(buffer); end; -//TODO fat starts after reserved secotrs +{ ---- FAT sector cache ---- } -function readFat(volume : PStorage_volume; cluster : uint32; bootRecord : PBootRecord) : uint32; +{ Get or lazily create the per-volume FAT cache. + Stores the cache pointer in volume^.fsPrivate. } +function fatCacheGet(volume : PStorage_Volume) : PFATCache; var - buffer : puint32; - fatEntriesPerSector : uint32; - sectorLocation : uint32; - dataStart : uint32; + cache : PFATCache; + br : PBootRecord; + i : uint32; begin - push_trace('driver.storage.fs.fat32.readFat.enter'); - io.syslog.logln('FAT32', 'readFat: enter'); - buffer:= puint32(kalloc(bootRecord^.sectorsize)); - memset(uint32(buffer), 0, bootRecord^.sectorsize); - fatEntriesPerSector:= bootRecord^.sectorsize div 4; - sectorLocation:= cluster div fatEntriesPerSector; - dataStart:= (volume^.sectorStart + 1 + bootRecord^.rsvSectors); - - driver.storage.mgr.storage_read(volume^.device, datastart + sectorLocation, 1, buffer); - readFat:= buffer[cluster - (sectorLocation * fatEntriesPerSector)]; - - kfree(buffer); - io.syslog.logln('FAT32', 'readFat: done'); + if volume^.fsPrivate <> nil then begin + fatCacheGet := PFATCache(volume^.fsPrivate); + exit; + end; + cache := PFATCache(kalloc(sizeof(TFATCache))); + memset(uint32(cache), 0, sizeof(TFATCache)); + cache^.Data := puint32(kalloc(64 * 512)); + memset(uint32(cache^.Data), 0, 64 * 512); + cache^.EvictBuf := puint32(kalloc(512)); + memset(uint32(cache^.EvictBuf), 0, 512); + cache^.Busy := false; + for i := 0 to 63 do begin + cache^.Lines[i].Valid := false; + cache^.Lines[i].Dirty := false; + end; + br := readBootRecord(volume); + cache^.BootRecord := br; + cache^.FatStart := volume^.sectorStart + 1 + br^.rsvSectors; + cache^.Device := volume^.device; + volume^.fsPrivate := pointer(cache); + fatCacheGet := cache; end; +{ Flush all dirty cache lines to disk. + CLI/STI protects cache metadata; disk I/O runs with interrupts enabled. } +procedure fatCacheFlush(cache : PFATCache; volume : PStorage_Volume); +var + i : uint32; + lba : uint32; + needFlush : boolean; +begin + { Acquire Busy lock — protects EvictBuf and prevents concurrent evictions } + while true do begin + asm pushf; cli end; + if not cache^.Busy then begin + cache^.Busy := true; + asm popf end; + break; + end; + asm popf end; + asm hlt end; + end; + + for i := 0 to 63 do begin + asm pushf; cli end; + needFlush := cache^.Lines[i].Valid and cache^.Lines[i].Dirty; + if needFlush then begin + lba := cache^.FatStart + cache^.Lines[i].SectorIdx; + cache^.Lines[i].Dirty := false; + core.util.memcpy(uint32(cache^.Data) + (i * 512), + uint32(cache^.EvictBuf), 512); + end; + asm popf end; + + if needFlush then + driver.storage.mgr.storage_write( + cache^.Device, lba, 1, cache^.EvictBuf); + end; + + { Release lock } + asm pushf; cli end; + cache^.Busy := false; + asm popf end; +end; + +{ Invalidate all cache lines (e.g. on unmount). } +procedure fatCacheInvalidate(cache : PFATCache); +var + i : uint32; +begin + for i := 0 to 63 do begin + cache^.Lines[i].Valid := false; + cache^.Lines[i].Dirty := false; + end; +end; + +{ Read a single FAT entry using the sector cache. + 128 FAT entries per 512-byte sector. Direct-mapped: slot = fatSector mod 64. + CLI/STI protects cache metadata; disk I/O runs with interrupts enabled. } +function readFat(volume : PStorage_volume; cluster : uint32; bootRecord : PBootRecord) : uint32; +var + cache : PFATCache; + fatSecIdx : uint32; { which FAT sector this cluster lives in } + slot : uint32; { cache line index } + entryOff : uint32; { offset within that sector (0..127) } + dataPtr : puint32; { pointer into cache data for this slot } + lba : uint32; + doEvict : boolean; + evictLBA : uint32; +begin + cache := fatCacheGet(volume); + fatSecIdx := cluster div 128; + slot := fatSecIdx and 63; { mod 64 } + entryOff := cluster mod 128; + dataPtr := puint32(uint32(cache^.Data) + (slot * 512)); + + { Fast path: cache hit under CLI — no lock needed } + asm pushf; cli end; + if cache^.Lines[slot].Valid and (cache^.Lines[slot].SectorIdx = fatSecIdx) then begin + readFat := dataPtr[entryOff] and $0FFFFFFF; + asm popf end; + exit; + end; + asm popf end; + + { Miss path: acquire Busy spinlock to serialize eviction+load } + while true do begin + asm pushf; cli end; + if not cache^.Busy then begin + { Re-check for hit — another process may have loaded this slot } + if cache^.Lines[slot].Valid and (cache^.Lines[slot].SectorIdx = fatSecIdx) then begin + readFat := dataPtr[entryOff] and $0FFFFFFF; + asm popf end; + exit; + end; + cache^.Busy := true; + doEvict := cache^.Lines[slot].Valid and cache^.Lines[slot].Dirty; + if doEvict then begin + evictLBA := cache^.FatStart + cache^.Lines[slot].SectorIdx; + core.util.memcpy(uint32(dataPtr), uint32(cache^.EvictBuf), 512); + end; + cache^.Lines[slot].Valid := false; + asm popf end; + break; + end; + asm popf end; + asm hlt end; + end; + + { Disk I/O with interrupts enabled — exclusive via Busy flag } + if doEvict then + driver.storage.mgr.storage_write(cache^.Device, evictLBA, 1, cache^.EvictBuf); + lba := cache^.FatStart + fatSecIdx; + driver.storage.mgr.storage_read(cache^.Device, lba, 1, dataPtr); + + { Update cache metadata and release lock under CLI } + asm pushf; cli end; + cache^.Lines[slot].SectorIdx := fatSecIdx; + cache^.Lines[slot].Valid := true; + cache^.Lines[slot].Dirty := false; + readFat := dataPtr[entryOff] and $0FFFFFFF; + cache^.Busy := false; + asm popf end; +end; + +{ Write a single FAT entry into cache. No immediate disk I/O — call fatCacheFlush later. + CLI/STI protects cache metadata; disk I/O runs with interrupts enabled. } procedure writeFat(volume : PStorage_volume; cluster : uint32; value : uint32; bootRecord : PBootRecord); var - buffer : puint32; - fatEntriesPerSector : uint32; - sectorLocation : uint32; - dataStart : uint32; + cache : PFATCache; + fatSecIdx : uint32; + slot : uint32; + entryOff : uint32; + dataPtr : puint32; + lba : uint32; + doEvict : boolean; + evictLBA : uint32; begin - push_trace('driver.storage.fs.fat32.writeFat.enter'); - io.syslog.logln('FAT32', 'writeFat: enter'); - buffer:= puint32(kalloc(bootRecord^.sectorsize)); - memset(uint32(buffer), 0, bootRecord^.sectorsize); - fatEntriesPerSector:= bootRecord^.sectorsize div 4; - sectorLocation:= cluster div fatEntriesPerSector; - dataStart:= (volume^.sectorStart + 1 + bootRecord^.rsvSectors); + cache := fatCacheGet(volume); + fatSecIdx := cluster div 128; + slot := fatSecIdx and 63; + entryOff := cluster mod 128; + dataPtr := puint32(uint32(cache^.Data) + (slot * 512)); - driver.storage.mgr.storage_read(volume^.device, dataStart + sectorLocation, 1, buffer); - buffer[cluster - (sectorLocation * fatEntriesPerSector)]:= value; - driver.storage.mgr.storage_write(volume^.device, dataStart + sectorLocation, 1, buffer); + { Fast path: cache hit — no lock needed } + asm pushf; cli end; + if cache^.Lines[slot].Valid and (cache^.Lines[slot].SectorIdx = fatSecIdx) then begin + if value = 0 then + dataPtr[entryOff] := 0 + else + dataPtr[entryOff] := (dataPtr[entryOff] and $F0000000) or (value and $0FFFFFFF); + cache^.Lines[slot].Dirty := true; + asm popf end; + exit; + end; + asm popf end; - kfree(buffer); - io.syslog.logln('FAT32', 'writeFat: done'); + { Miss path: acquire Busy spinlock } + while true do begin + asm pushf; cli end; + if not cache^.Busy then begin + { Re-check for hit after acquiring lock } + if cache^.Lines[slot].Valid and (cache^.Lines[slot].SectorIdx = fatSecIdx) then begin + if value = 0 then + dataPtr[entryOff] := 0 + else + dataPtr[entryOff] := (dataPtr[entryOff] and $F0000000) or (value and $0FFFFFFF); + cache^.Lines[slot].Dirty := true; + asm popf end; + exit; + end; + cache^.Busy := true; + doEvict := cache^.Lines[slot].Valid and cache^.Lines[slot].Dirty; + if doEvict then begin + evictLBA := cache^.FatStart + cache^.Lines[slot].SectorIdx; + core.util.memcpy(uint32(dataPtr), uint32(cache^.EvictBuf), 512); + end; + cache^.Lines[slot].Valid := false; + asm popf end; + break; + end; + asm popf end; + asm hlt end; + end; + + { Disk I/O with interrupts enabled — exclusive via Busy flag } + if doEvict then + driver.storage.mgr.storage_write(cache^.Device, evictLBA, 1, cache^.EvictBuf); + lba := cache^.FatStart + fatSecIdx; + driver.storage.mgr.storage_read(cache^.Device, lba, 1, dataPtr); + + { Update cache, write value, release lock under CLI } + asm pushf; cli end; + cache^.Lines[slot].SectorIdx := fatSecIdx; + cache^.Lines[slot].Valid := true; + cache^.Lines[slot].Dirty := false; + if value = 0 then + dataPtr[entryOff] := 0 + else + dataPtr[entryOff] := (dataPtr[entryOff] and $F0000000) or (value and $0FFFFFFF); + cache^.Lines[slot].Dirty := true; + cache^.Busy := false; + asm popf end; end; function getFatChain(volume : PStorage_volume; cluster : uint32; bootRecord : PBootRecord) : PLinkedListBase; @@ -374,8 +594,6 @@ var clusters : PLinkedListBase; dirElm : puint32; begin - push_trace('driver.storage.fs.fat32.getFatChain.enter'); - io.syslog.logln('FAT32', 'getFatChain: enter'); clusters:= LL_New(sizeof(uint32)); currentCluster:= cluster; currentClusterValue:= cluster; @@ -384,16 +602,12 @@ begin currentClusterValue:= readFat(volume, currentClusterValue, bootRecord); if (currentClusterValue and $0FFFFFFF) = $0FFFFFF7 then begin - push_trace('driver.storage.fs.fat32.getFatChain.badCluster'); - io.syslog.logln('FAT32', 'getFatChain: bad cluster, stopping'); break; end else if (currentClusterValue and $0FFFFFFF) >= $0FFFFFF8 then begin dirElm:= LL_add(clusters); dirElm^:= currentCluster; break; end else if currentClusterValue = 0 then begin - push_trace('driver.storage.fs.fat32.getFatChain.freeCluster'); - io.syslog.logln('FAT32', 'getFatChain: free cluster hit, stopping'); break; end else begin dirElm:= LL_add(clusters); @@ -404,21 +618,18 @@ begin end; getFatChain:= clusters; - io.syslog.logln('FAT32', 'getFatChain: done'); end; //TODO improve with FSINFO function findFreeClusters(volume : PStorage_volume; amount : uint32; bootRecord : PBootRecord) : PLinkedListBase; var - i : uint32 = 2; + i : uint32; currentClusterValue : uint32; currentAmount : uint32 = 0; maxCluster : uint32; clusters : PLinkedListBase; dirElm : puint32; begin - push_trace('driver.storage.fs.fat32.findFreeClusters.enter'); - io.syslog.logln('FAT32', 'findFreeClusters: enter'); clusters := LL_New(8); { Calculate total number of data clusters on this volume } @@ -427,30 +638,50 @@ begin else maxCluster := 0; + { Start from allocation hint if valid for this volume } + if (sioc_allocVol = volume) and (sioc_allocHint >= 2) and (sioc_allocHint < maxCluster) then + i := sioc_allocHint + else + i := 2; + while (i < maxCluster) do begin - if currentAmount = amount then break; - currentClusterValue:= readFat(volume, i, bootRecord); - if currentClusterValue = 0 then begin dirElm:= LL_add(clusters); dirElm^:= i; currentAmount+=1; end; - i+=1; end; + { Fallback: if hint caused us to miss free clusters, scan from 2 } + if (currentAmount < amount) and (sioc_allocVol = volume) and (sioc_allocHint > 2) then begin + i := 2; + while (i < sioc_allocHint) and (currentAmount < amount) do begin + currentClusterValue := readFat(volume, i, bootRecord); + if currentClusterValue = 0 then begin + dirElm := LL_add(clusters); + dirElm^ := i; + currentAmount += 1; + end; + i += 1; + end; + end; + + { Update allocation hint for next call } + if currentAmount > 0 then begin + sioc_allocHint := puint32(LL_Get(clusters, currentAmount - 1))^ + 1; + sioc_allocVol := volume; + end; + { If we couldn't find enough free clusters, the disk is full } if currentAmount < amount then begin - io.syslog.logln('FAT32', 'findFreeClusters: not enough free clusters - disk full'); LL_Free(clusters); findFreeClusters := nil; exit; end; - io.syslog.logln('FAT32', 'findFreeClusters: done'); findFreeClusters:= clusters; end; @@ -468,11 +699,9 @@ var dirElm : puint32; begin push_trace('driver.storage.fs.fat32.getDirEntries.enter'); - io.syslog.logln('FAT32', 'getDirEntries: enter'); directories:= LL_New(sizeof(TDirectory)); clusters:= PLinkedListBase(getFatChain(volume, cluster, bootRecord)); - io.syslog.logln('FAT32', 'getDirEntries: got FAT chain'); push_trace('driver.storage.fs.fat32.getDirEntries.allocBuffer'); buffer:= puint32(kalloc( (bootRecord^.sectorSize * bootRecord^.spc) * LL_size(clusters) )); memset(uint32(buffer), 0, (bootRecord^.sectorSize * bootRecord^.spc) * LL_size(clusters) ); @@ -481,9 +710,9 @@ begin push_trace('driver.storage.fs.fat32.getDirEntries.readSectors'); for i:=0 to LL_size(clusters) - 1 do begin - sectorLocation:= bootRecord^.spc * (i + cluster); + sectorLocation:= bootRecord^.spc * puint32(LL_Get(clusters, i))^; bufferI:= puint32(uint32(buffer) + uint32(i * bootRecord^.spc * bootRecord^.sectorSize)); - driver.storage.mgr.storage_read(volume^.device, datastart + sectorLocation, bootRecord^.spc, bufferI); //datastart + spc(i + cluster) + driver.storage.mgr.storage_read(volume^.device, datastart + sectorLocation, bootRecord^.spc, bufferI); end; i:=0; @@ -506,7 +735,6 @@ begin LL_Free(clusters); kfree(buffer); push_trace('driver.storage.fs.fat32.getDirEntries.exit'); - io.syslog.logln('FAT32', 'getDirEntries: exit'); end; function compareByteArray8(str1 : byteArray8; str2 : byteArray8) : boolean; @@ -606,7 +834,6 @@ var begin status:= puint32(kalloc(sizeof(uint32))); push_trace('driver.storage.fs.fat32.readDirectory.enter'); - io.syslog.logln('FAT32', 'readDirectory: enter'); status^:= ord(eNone); bootRecord:= readBootRecord(volume); directoryStrings:= LL_fromString(directory, '/'); @@ -619,7 +846,6 @@ begin while true do begin if ii > LL_Size(directories) - 1 then begin status^:= ord(eDirectoryDoesNotExist); - io.syslog.logln('FAT32', 'readDirectory: component not found'); break; end; dirEntry:= PDirectory(LL_Get(directories, ii)); @@ -654,7 +880,6 @@ begin LL_Free(directoryStrings); kfree(puint32(bootRecord)); push_trace('driver.storage.fs.fat32.readDirectory.exit'); - io.syslog.logln('FAT32', 'readDirectory: exit'); end; function readDirectoryGen(volume : PStorage_volume; directory : pchar; status : puint32) : PLinkedListBase; //returns: 0 = success, 1 = dir not exsist, 2 = not directory, 3 = invalid name, 4= already exists @@ -887,17 +1112,35 @@ begin end; end; -{ Free a FAT chain starting from the given cluster — marks all clusters as free (0). } +{ Free a FAT chain starting from the given cluster — marks all clusters as free (0). + Includes loop protection: aborts if iteration count exceeds maxCluster or cluster + is out of range (< 2 or >= maxCluster). } procedure freeFatChain(volume : PStorage_volume; startCluster : uint32; bootRecord : PBootRecord); var currentCluster : uint32; nextCluster : uint32; + maxCluster : uint32; + iter : uint32; begin push_trace('driver.storage.fs.fat32.freeFatChain.enter'); + if (bootRecord^.FATSize > 0) and (bootRecord^.sectorsize > 0) then + maxCluster := (bootRecord^.FATSize * bootRecord^.sectorsize) div 4 + else + maxCluster := 0; currentCluster := startCluster; + iter := 0; while true do begin + if (currentCluster < 2) or (currentCluster >= maxCluster) then begin + io.syslog.logln('FAT32', 'freeFatChain: cluster out of range, aborting'); + break; + end; + if iter >= maxCluster then begin + io.syslog.logln('FAT32', 'freeFatChain: iteration limit hit, possible cycle'); + break; + end; nextCluster := readFat(volume, currentCluster, bootRecord); writeFat(volume, currentCluster, 0, bootRecord); + iter := iter + 1; if (nextCluster and $0FFFFFFF) >= $0FFFFFF8 then break; { end of chain } if (nextCluster and $0FFFFFFF) = $0FFFFFF7 then break; { bad cluster } @@ -911,26 +1154,31 @@ end; { Delete a file from the volume. filePath is relative to volume root, e.g. 'SYSTEM/FILE.TXT' or 'FILE.TXT' } procedure deleteFile(volume : PStorage_Volume; filePath : pchar; statusOut : puint32); var - parentDir : pchar; - fileName : pchar; - namePart : pchar; - extPart : pchar; - bootRecord : PBootRecord; - directories : PLinkedListBase; - parentEntry : PDirectory; - parentCluster : uint32; - status : puint32; - dataStart : uint32; - spc : uint32; - sectorLocation : uint32; - buffer : puint32; + parentDir : pchar; + fileName : pchar; + namePart : pchar; + extPart : pchar; + bootRecord : PBootRecord; + directories : PLinkedListBase; + parentEntry : PDirectory; + parentCluster : uint32; + status : puint32; + dataStart : uint32; + spc : uint32; + buffer : puint32; EntriesPerSector : uint32; - totalEntries : uint32; - dir : PDirectory; - found : boolean; - rawIdx : uint32; - cluster : uint32; - sectorOffset : uint32; + dir : PDirectory; + found : boolean; + done : boolean; + entryIdx : uint32; + cluster : uint32; + sectorOffset : uint32; + cleanFileName : byteArray8; + dirChain : PLinkedListBase; + dc : uint32; + ds : uint32; + curCluster : uint32; + clusterLBA : uint32; begin push_trace('driver.storage.fs.fat32.deleteFile.enter'); io.syslog.logln('FAT32', 'deleteFile: enter'); @@ -977,80 +1225,86 @@ begin kfree(void(fileName)); exit; end; - { Entry 0 is '.' which holds the directory's own cluster } parentEntry := PDirectory(LL_Get(directories, 0)); parentCluster := uint32(parentEntry^.clusterLow) or uint32(parentEntry^.clusterHigh shl 16); LL_Free(directories); end; - { Split filename for 8.3 matching } + { Split filename for 8.3 matching — compute cleanFileName once before loop } splitFileNameParts(fileName, namePart, extPart); + cleanFileName := cleanString(namePart, status); - { Read raw sectors from parent cluster (handles $E5 gaps correctly) } + { Walk the full parent directory FAT chain to find the target entry } dataStart := volume^.sectorStart + 1 + bootRecord^.rsvSectors + bootRecord^.FATSize; EntriesPerSector := uint32(bootRecord^.sectorSize) div uint32(sizeof(TDirectory)); - sectorLocation := parentCluster * spc; - buffer := puint32(kalloc(bootRecord^.sectorSize * spc)); - driver.storage.mgr.storage_read(volume^.device, dataStart + sectorLocation, spc, buffer); - totalEntries := EntriesPerSector * spc; - - { Search raw entries for the matching file } + buffer := puint32(kalloc(bootRecord^.sectorSize)); + dirChain := getFatChain(volume, parentCluster, bootRecord); found := false; - if totalEntries > 0 then - for rawIdx := 0 to totalEntries - 1 do begin - dir := @PDirectory(buffer)[rawIdx]; - if dir^.fileName[0] = char(0) then break; - if dir^.fileName[0] = char($E5) then continue; - if compareByteArray8(dir^.fileName, cleanString(namePart, status)) - and matchExtension(dir^.fileExtension, extPart) then begin - { Don't allow deleting directories via deleteFile } - if (dir^.attributes and $10) = $10 then begin - io.syslog.logln('FAT32', 'deleteFile: target is a directory, not a file'); - if statusOut <> nil then statusOut^ := ord(eNotADirectory); - kfree(buffer); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(fileName)); - kfree(void(namePart)); - kfree(void(extPart)); - exit; + done := false; + + dc := 0; + while (dc < LL_size(dirChain)) and (not done) do begin + curCluster := puint32(LL_Get(dirChain, dc))^; + clusterLBA := dataStart + (curCluster * spc); + ds := 0; + while (ds < spc) and (not done) do begin + driver.storage.mgr.storage_read(volume^.device, clusterLBA + ds, 1, buffer); + if EntriesPerSector > 0 then + for entryIdx := 0 to EntriesPerSector - 1 do begin + dir := @PDirectory(buffer)[entryIdx]; + if dir^.fileName[0] = char(0) then begin + done := true; + break; + end; + if dir^.fileName[0] = char($E5) then continue; + if compareByteArray8(dir^.fileName, cleanFileName) + and matchExtension(dir^.fileExtension, extPart) then begin + { Don't allow deleting directories via deleteFile } + if (dir^.attributes and $10) = $10 then begin + io.syslog.logln('FAT32', 'deleteFile: target is a directory, not a file'); + if statusOut <> nil then statusOut^ := ord(eNotADirectory); + kfree(buffer); + kfree(status); + kfree(puint32(bootRecord)); + kfree(void(parentDir)); + kfree(void(fileName)); + kfree(void(namePart)); + kfree(void(extPart)); + LL_Free(dirChain); + exit; + end; + found := true; + done := true; + cluster := uint32(dir^.clusterLow) or uint32(dir^.clusterHigh shl 16); + + { Free the FAT chain for this file } + io.syslog.logln('FAT32', 'deleteFile: freeing FAT chain'); + if cluster >= 2 then + freeFatChain(volume, cluster, bootRecord); + + { Mark entry deleted and write back this one sector } + dir^.fileName[0] := char($E5); + driver.storage.mgr.storage_write(volume^.device, clusterLBA + ds, 1, buffer); + break; + end; end; - found := true; - cluster := uint32(dir^.clusterLow) or uint32(dir^.clusterHigh shl 16); - break; + ds := ds + 1; end; + dc := dc + 1; end; + LL_Free(dirChain); + kfree(buffer); kfree(void(namePart)); kfree(void(extPart)); if not found then begin io.syslog.logln('FAT32', 'deleteFile: file not found'); if statusOut <> nil then statusOut^ := ord(eFileDoesNotExist); - kfree(buffer); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(fileName)); - exit; + end else begin + if statusOut <> nil then statusOut^ := ord(eNone); end; - io.syslog.logln('FAT32', 'deleteFile: freeing FAT chain'); - { Free the FAT chain for this file } - if cluster >= 2 then - freeFatChain(volume, cluster, bootRecord); - - { Mark the raw entry as deleted ($E5) and write back the affected sector } - PDirectory(buffer)[rawIdx].fileName[0] := char($E5); - sectorOffset := rawIdx div EntriesPerSector; - driver.storage.mgr.storage_write(volume^.device, - dataStart + sectorLocation + sectorOffset, 1, - puint32(uint32(buffer) + sectorOffset * bootRecord^.sectorSize)); - - if statusOut <> nil then statusOut^ := ord(eNone); - - kfree(buffer); kfree(status); kfree(puint32(bootRecord)); kfree(void(parentDir)); @@ -1062,28 +1316,32 @@ end; { Delete a directory from the volume. path is relative to volume root. Directory must be empty. } procedure deleteDir(volume : PStorage_Volume; path : pchar; statusOut : puint32); var - parentDir : pchar; - dirName : pchar; - namePart : pchar; - extPart : pchar; - bootRecord : PBootRecord; - directories : PLinkedListBase; - childDirs : PLinkedListBase; - parentEntry : PDirectory; - parentCluster : uint32; - status : puint32; - dataStart : uint32; - spc : uint32; - sectorLocation : uint32; - buffer : puint32; + parentDir : pchar; + dirName : pchar; + namePart : pchar; + extPart : pchar; + bootRecord : PBootRecord; + directories : PLinkedListBase; + childDirs : PLinkedListBase; + parentEntry : PDirectory; + parentCluster : uint32; + status : puint32; + dataStart : uint32; + spc : uint32; + buffer : puint32; EntriesPerSector : uint32; - totalEntries : uint32; - dir : PDirectory; - found : boolean; - rawIdx : uint32; - cluster : uint32; - childCount : uint32; - sectorOffset : uint32; + dir : PDirectory; + found : boolean; + done : boolean; + entryIdx : uint32; + cluster : uint32; + childCount : uint32; + cleanFileName : byteArray8; + dirChain : PLinkedListBase; + dc : uint32; + ds : uint32; + curCluster : uint32; + clusterLBA : uint32; begin push_trace('driver.storage.fs.fat32.deleteDir.enter'); io.syslog.logln('FAT32', 'deleteDir: enter'); @@ -1133,88 +1391,99 @@ begin LL_Free(directories); end; - { Split dirName for 8.3 matching } + { Split dirName for 8.3 matching — compute cleanFileName once before loop } splitFileNameParts(dirName, namePart, extPart); + cleanFileName := cleanString(namePart, status); - { Read raw sectors from parent cluster } + { Walk the full parent directory FAT chain to find the target entry } dataStart := volume^.sectorStart + 1 + bootRecord^.rsvSectors + bootRecord^.FATSize; EntriesPerSector := uint32(bootRecord^.sectorSize) div uint32(sizeof(TDirectory)); - sectorLocation := parentCluster * spc; - buffer := puint32(kalloc(bootRecord^.sectorSize * spc)); - driver.storage.mgr.storage_read(volume^.device, dataStart + sectorLocation, spc, buffer); - totalEntries := EntriesPerSector * spc; - - { Search raw entries for the matching directory } + buffer := puint32(kalloc(bootRecord^.sectorSize)); + dirChain := getFatChain(volume, parentCluster, bootRecord); found := false; - if totalEntries > 0 then - for rawIdx := 0 to totalEntries - 1 do begin - dir := @PDirectory(buffer)[rawIdx]; - if dir^.fileName[0] = char(0) then break; - if dir^.fileName[0] = char($E5) then continue; - if compareByteArray8(dir^.fileName, cleanString(namePart, status)) - and matchExtension(dir^.fileExtension, extPart) then begin - { Must be a directory } - if (dir^.attributes and $10) <> $10 then begin - if statusOut <> nil then statusOut^ := ord(eNotADirectory); - kfree(buffer); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(dirName)); - kfree(void(namePart)); - kfree(void(extPart)); - exit; + done := false; + cluster := 0; + + dc := 0; + while (dc < LL_size(dirChain)) and (not done) do begin + curCluster := puint32(LL_Get(dirChain, dc))^; + clusterLBA := dataStart + (curCluster * spc); + ds := 0; + while (ds < spc) and (not done) do begin + driver.storage.mgr.storage_read(volume^.device, clusterLBA + ds, 1, buffer); + if EntriesPerSector > 0 then + for entryIdx := 0 to EntriesPerSector - 1 do begin + dir := @PDirectory(buffer)[entryIdx]; + if dir^.fileName[0] = char(0) then begin + done := true; + break; + end; + if dir^.fileName[0] = char($E5) then continue; + if compareByteArray8(dir^.fileName, cleanFileName) + and matchExtension(dir^.fileExtension, extPart) then begin + { Must be a directory } + if (dir^.attributes and $10) <> $10 then begin + if statusOut <> nil then statusOut^ := ord(eNotADirectory); + kfree(buffer); + kfree(status); + kfree(puint32(bootRecord)); + kfree(void(parentDir)); + kfree(void(dirName)); + kfree(void(namePart)); + kfree(void(extPart)); + LL_Free(dirChain); + exit; + end; + found := true; + done := true; + cluster := uint32(dir^.clusterLow) or uint32(dir^.clusterHigh shl 16); + + { Check if directory is empty — should contain only '.' and '..' } + childDirs := getDirEntries(volume, cluster, bootRecord); + childCount := LL_size(childDirs); + LL_Free(childDirs); + + if childCount > 2 then begin + io.syslog.logln('FAT32', 'deleteDir: directory not empty, refusing to delete'); + if statusOut <> nil then statusOut^ := ord(eDirectoryNotEmpty); + kfree(buffer); + kfree(status); + kfree(puint32(bootRecord)); + kfree(void(parentDir)); + kfree(void(dirName)); + kfree(void(namePart)); + kfree(void(extPart)); + LL_Free(dirChain); + exit; + end; + + { Free the FAT chain for this directory's data } + if cluster >= 2 then + freeFatChain(volume, cluster, bootRecord); + + { Mark entry deleted and write back this one sector } + dir^.fileName[0] := char($E5); + driver.storage.mgr.storage_write(volume^.device, clusterLBA + ds, 1, buffer); + break; + end; end; - found := true; - cluster := uint32(dir^.clusterLow) or uint32(dir^.clusterHigh shl 16); - break; + ds := ds + 1; end; + dc := dc + 1; end; + LL_Free(dirChain); + kfree(buffer); kfree(void(namePart)); kfree(void(extPart)); if not found then begin io.syslog.logln('FAT32', 'deleteDir: directory not found'); if statusOut <> nil then statusOut^ := ord(eDirectoryDoesNotExist); - kfree(buffer); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(dirName)); - exit; + end else begin + if statusOut <> nil then statusOut^ := ord(eNone); end; - { Check if directory is empty — should contain only '.' and '..' } - childDirs := getDirEntries(volume, cluster, bootRecord); - childCount := LL_size(childDirs); - LL_Free(childDirs); - - if childCount > 2 then begin - io.syslog.logln('FAT32', 'deleteDir: directory not empty, refusing to delete'); - if statusOut <> nil then statusOut^ := ord(eDirectoryNotEmpty); - kfree(buffer); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(dirName)); - exit; - end; - - { Free the FAT chain for this directory's data } - if cluster >= 2 then - freeFatChain(volume, cluster, bootRecord); - - { Mark the raw entry as deleted ($E5) and write back the affected sector } - PDirectory(buffer)[rawIdx].fileName[0] := char($E5); - sectorOffset := rawIdx div EntriesPerSector; - driver.storage.mgr.storage_write(volume^.device, - dataStart + sectorLocation + sectorOffset, 1, - puint32(uint32(buffer) + sectorOffset * bootRecord^.sectorSize)); - - if statusOut <> nil then statusOut^ := ord(eNone); - - kfree(buffer); kfree(status); kfree(puint32(bootRecord)); kfree(void(parentDir)); @@ -1239,18 +1508,22 @@ var parentEntry : PDirectory; parentCluster : uint32; found : boolean; - rawIdx : uint32; j : uint32; status : puint32; dataStart : uint32; spc : uint32; - sectorLocation : uint32; buffer : puint32; EntriesPerSector: uint32; - totalEntries : uint32; dir : PDirectory; cleanName : byteArray8; - sectorOffset : uint32; + newCleanName : byteArray8; + done : boolean; + dirChain : PLinkedListBase; + dc : uint32; + ds : uint32; + entryIdx : uint32; + curCluster : uint32; + clusterLBA : uint32; begin push_trace('driver.storage.fs.fat32.renameFile.enter'); io.syslog.logln('FAT32', 'renameFile: enter'); @@ -1311,75 +1584,77 @@ begin LL_Free(directories); end; - { Split current filename for 8.3 matching } + { Split current filename for 8.3 matching — compute once before loop } splitFileNameParts(fileName, namePart, extPart); + cleanName := cleanString(namePart, status); - { Read raw sectors from parent cluster } + { Walk the full parent directory FAT chain to find the target entry } dataStart := volume^.sectorStart + 1 + bootRecord^.rsvSectors + bootRecord^.FATSize; EntriesPerSector := uint32(bootRecord^.sectorSize) div uint32(sizeof(TDirectory)); - sectorLocation := parentCluster * spc; - buffer := puint32(kalloc(bootRecord^.sectorSize * spc)); - driver.storage.mgr.storage_read(volume^.device, dataStart + sectorLocation, spc, buffer); - totalEntries := EntriesPerSector * spc; - - { Search raw entries for the matching file } + buffer := puint32(kalloc(bootRecord^.sectorSize)); + dirChain := getFatChain(volume, parentCluster, bootRecord); found := false; - if totalEntries > 0 then - for rawIdx := 0 to totalEntries - 1 do begin - dir := @PDirectory(buffer)[rawIdx]; - if dir^.fileName[0] = char(0) then break; - if dir^.fileName[0] = char($E5) then continue; - if compareByteArray8(dir^.fileName, cleanString(namePart, status)) - and matchExtension(dir^.fileExtension, extPart) then begin - found := true; - break; + done := false; + + dc := 0; + while (dc < LL_size(dirChain)) and (not done) do begin + curCluster := puint32(LL_Get(dirChain, dc))^; + clusterLBA := dataStart + (curCluster * spc); + ds := 0; + while (ds < spc) and (not done) do begin + driver.storage.mgr.storage_read(volume^.device, clusterLBA + ds, 1, buffer); + if EntriesPerSector > 0 then + for entryIdx := 0 to EntriesPerSector - 1 do begin + dir := @PDirectory(buffer)[entryIdx]; + if dir^.fileName[0] = char(0) then begin + done := true; + break; + end; + if dir^.fileName[0] = char($E5) then continue; + if compareByteArray8(dir^.fileName, cleanName) + and matchExtension(dir^.fileExtension, extPart) then begin + found := true; + done := true; + + { Apply new 8.3 name in-place } + splitFileNameParts(newName, newNamePart, newExtPart); + newCleanName := cleanString(newNamePart, status); + for j := 0 to 7 do + dir^.fileName[j] := newCleanName[j]; + for j := 0 to 2 do begin + if (newExtPart <> nil) and (j < stringSize(newExtPart)) then begin + if (newExtPart[j] >= 'a') and (newExtPart[j] <= 'z') then + dir^.fileExtension[j] := char(uint8(newExtPart[j]) - 32) + else + dir^.fileExtension[j] := newExtPart[j]; + end else + dir^.fileExtension[j] := ' '; + end; + kfree(void(newNamePart)); + kfree(void(newExtPart)); + + { Write back this one sector } + driver.storage.mgr.storage_write(volume^.device, clusterLBA + ds, 1, buffer); + break; + end; + end; + ds := ds + 1; end; + dc := dc + 1; end; + LL_Free(dirChain); + kfree(buffer); kfree(void(namePart)); kfree(void(extPart)); if not found then begin io.syslog.logln('FAT32', 'renameFile: file not found'); if statusOut <> nil then statusOut^ := ord(eFileDoesNotExist); - kfree(buffer); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(fileName)); - exit; + end else begin + if statusOut <> nil then statusOut^ := ord(eNone); end; - { Write new 8.3 name into the raw entry } - splitFileNameParts(newName, newNamePart, newExtPart); - cleanName := cleanString(newNamePart, status); - - for j := 0 to 7 do - PDirectory(buffer)[rawIdx].fileName[j] := cleanName[j]; - - { Write extension — pad with spaces } - for j := 0 to 2 do begin - if (newExtPart <> nil) and (j < stringSize(newExtPart)) then begin - if (newExtPart[j] >= 'a') and (newExtPart[j] <= 'z') then - PDirectory(buffer)[rawIdx].fileExtension[j] := char(uint8(newExtPart[j]) - 32) - else - PDirectory(buffer)[rawIdx].fileExtension[j] := newExtPart[j]; - end else - PDirectory(buffer)[rawIdx].fileExtension[j] := ' '; - end; - - { Write back the affected sector } - sectorOffset := rawIdx div EntriesPerSector; - driver.storage.mgr.storage_write(volume^.device, - dataStart + sectorLocation + sectorOffset, 1, - puint32(uint32(buffer) + sectorOffset * bootRecord^.sectorSize)); - - kfree(buffer); - kfree(void(newNamePart)); - kfree(void(newExtPart)); - - if statusOut <> nil then statusOut^ := ord(eNone); - kfree(status); kfree(puint32(bootRecord)); kfree(void(parentDir)); @@ -1388,289 +1663,6 @@ begin io.syslog.logln('FAT32', 'renameFile: done'); end; -procedure writeFile(volume : PStorage_volume; directory : pchar; entry : PDirectory_Entry; byteCount : uint32; buffer : puint32; statusOut : puint32); -var - bootRecord : PBootRecord; - directories : PLinkedListBase; - clusters : PLinkedListBase; - startCluster: uint32; - dir : PDirectory; - exists : boolean = false; - sectorCount : uint32; - clusterCount : uint32; - clusterDifference : uint32; - dataStart : uint32; - iterations : uint32; - bufferPointer : puint32; - - i : uint32; - status : puint32; - namePart : pchar; - extPart : pchar; - - { Variables for updating directory entry byteSize } - entryIndex : uint32; - parentCluster : uint32; - dirSectorBuf : puint32; - dirSectorLoc : uint32; - entriesPerSec : uint32; - entryOffset : uint32; -begin - push_trace('driver.storage.fs.fat32.writeFile.enter'); - io.syslog.logln('FAT32', 'writeFile: enter'); - status:= kalloc(4); - status^:= ord(eNone); - - { Validate the filename before doing any I/O } - if not isValidFAT32Name(entry^.fileName) then begin - io.syslog.logln('FAT32', 'writeFile: invalid FAT32 filename'); - if statusOut <> nil then statusOut^:= ord(eInvalidFileName); - kfree(status); - exit; - end; - - bootRecord:= readBootRecord(volume); - push_trace('driver.storage.fs.fat32.writeFile.readDir'); - directories:= readDirectory(volume, directory, status); - - { If the parent directory does not exist, bail out } - if status^ <> ord(eNone) then begin - io.syslog.logln('FAT32', 'writeFile: parent directory error'); - if statusOut <> nil then statusOut^:= status^; - LL_Free(directories); - kfree(status); - kfree(puint32(bootRecord)); - exit; - end; - - sectorCount:= (byteCount div bootRecord^.sectorSize) + 1; - datastart:= volume^.sectorStart + 1 + bootRecord^.FATSize + bootRecord^.rsvSectors; - push_trace('driver.storage.fs.fat32.writeFile.checkExists'); - - { Split entry filename into name + extension for FAT32 8.3 matching } - splitFileNameParts(entry^.fileName, namePart, extPart); - - for i:=0 to LL_size(directories) - 1 do begin - dir:= PDirectory(LL_get(directories, i)); - if compareByteArray8(dir^.fileName, cleanString(namePart, status)) and matchExtension(dir^.fileExtension, extPart) then begin - exists:= true; - entryIndex:= i; - break; - end; - end; - - kfree(void(namePart)); - kfree(void(extPart)); - - - if exists then begin //saving to existing file - push_trace('driver.storage.fs.fat32.writeFile.existingFile'); - io.syslog.logln('FAT32', 'writeFile: updating existing file'); - startCluster:= uint32(dir^.clusterlow) or uint32(dir^.clusterhigh shl 16); - clusters:= getFatChain(volume, startCluster, bootRecord); //check no clusters and check if needs to be more or less, add/remove clusters - clusterCount := LL_size(clusters); - push_trace('driver.storage.fs.fat32.writeFile.gotFatChain'); - - if (clusterCount * bootRecord^.spc) > sectorCount then begin //shrink - clusterDifference:= clusterCount - (sectorCount div bootRecord^.spc); - for i:= (clusterCount - clusterDifference) + 1 to clusterCount do begin //free unused clusters - writeFat(volume, startCluster + i, 0, bootRecord); - end; - writeFat(volume, startCluster + (clusterCount - clusterDifference), $FFFFFFF8, bootRecord); // add new cluster terminator - LL_Free(clusters); - end else if (clusterCount * bootRecord^.spc) < sectorCount then begin //expand - clusterDifference:= (sectorCount div bootRecord^.spc) - clusterCount; - LL_Free(clusters); - clusters:= findFreeClusters(volume, clusterDifference, bootRecord); - if clusters = nil then begin - { Disk full — cannot expand file } - io.syslog.logln('FAT32', 'writeFile: disk full, cannot expand file'); - if statusOut <> nil then statusOut^:= ord(eDiskFull); - LL_Free(directories); - kfree(status); - kfree(puint32(bootRecord)); - exit; - end; - for i:= clusterCount to clusterCount + clusterDifference - 1 do begin - writeFat(volume, startCluster + i, startCluster + i + 1, bootRecord); - end; - writeFat(volume, startcluster + clusterCount + clusterDifference, $FFFFFFF8, bootRecord); - LL_Free(clusters); - end else begin //nothing - clusterDifference:= 0; - LL_Free(clusters); - end; - - end else begin - push_trace('driver.storage.fs.fat32.writeFile.newFile'); - io.syslog.logln('FAT32', 'writeFile: creating new file'); - - entryIndex:= LL_size(directories); { New entry appended at end } - startCluster:= writeDirectory(volume, directory, entry^.fileName, 0, status); - - { If writeDirectory failed (disk full, dir full, invalid name, etc.), propagate error } - if status^ <> ord(eNone) then begin - io.syslog.logln('FAT32', 'writeFile: writeDirectory failed'); - if statusOut <> nil then statusOut^:= status^; - LL_Free(directories); - kfree(status); - kfree(puint32(bootRecord)); - exit; - end; - - clusterDifference:= (byteCount div bootRecord^.sectorsize) div bootRecord^.spc; - push_trace('driver.storage.fs.fat32.writeFile.newFile.setupFat'); - - for i:= startcluster to startCluster + clusterDifference - 1 do begin - writeFat(volume, i, i + 1, bootRecord); - end; - push_trace('driver.storage.fs.fat32.writeFile.newFile.writeFatEnd'); - - writeFat(volume, startcluster + clusterDifference, $FFFFFFF8, bootRecord); - end; - - push_trace('driver.storage.fs.fat32.writeFile.writeSectors'); - io.syslog.logln('FAT32', 'writeFile: writing sectors'); - - { Calculate total sectors needed and write each sector individually. - Assumes contiguous clusters starting at startCluster. } - iterations:= (bytecount + bootRecord^.sectorSize - 1) div bootRecord^.sectorSize; - if iterations > 0 then begin - for i:=0 to iterations - 1 do begin - bufferPointer:= puint32(uint32(buffer) + uint32(i * bootRecord^.sectorSize)); - driver.storage.mgr.storage_write(volume^.device, dataStart + (startCluster * bootRecord^.spc) + i, 1, bufferPointer); - end; - end; - - { Write succeeded — now update the directory entry's byteSize on disk } - push_trace('driver.storage.fs.fat32.writeFile.updateByteSize'); - if LL_size(directories) > 0 then begin - parentCluster:= uint32(PDirectory(LL_get(directories, 0))^.clusterLow) - or uint32(PDirectory(LL_get(directories, 0))^.clusterHigh shl 16); - entriesPerSec:= bootRecord^.sectorSize div uint32(sizeof(TDirectory)); - dirSectorLoc:= (entryIndex * uint32(sizeof(TDirectory))) div bootRecord^.sectorSize; - dirSectorLoc:= dirSectorLoc + (parentCluster * bootRecord^.spc); - entryOffset:= entryIndex mod entriesPerSec; - - dirSectorBuf:= puint32(kalloc(bootRecord^.sectorSize)); - driver.storage.mgr.storage_read(volume^.device, dataStart + dirSectorLoc, 1, dirSectorBuf); - PDirectory(dirSectorBuf)[entryOffset].byteSize:= byteCount; - driver.storage.mgr.storage_write(volume^.device, dataStart + dirSectorLoc, 1, dirSectorBuf); - kfree(dirSectorBuf); - end; - - io.syslog.logln('FAT32', 'writeFile: done'); - if statusOut <> nil then statusOut^:= ord(eNone); - - LL_Free(directories); - kfree(status); - kfree(puint32(bootRecord)); -end; - -function readFile(volume : PStorage_Volume; directory : pchar; fileName : pchar; buffer : puint32; bytecount : puint32) : uint32; -var - bootRecord : PBootRecord; - dirs : PLinkedListBase; - dir : PDirectory; - readbuffer : puint32; - data : puint32; - statusOut : puint32; - i : uint32; - exists : boolean = false; - cluster : uint32; - clusters : PLinkedListBase; - noClusters : uint32; - dataStart : uint32; - cleanFileName : byteArray8; - otherCleanFileName : byteArray8; - tempdir : PDirectory; - namePart : pchar; - extPart : pchar; -begin - push_trace('driver.storage.fs.fat32.readFile.enter'); - io.syslog.logln('FAT32', 'readFile: enter'); - statusOut := puint32(kalloc(sizeof(uint32))); - statusOut^ := 0; - bootRecord := readBootRecord(volume); - push_trace('driver.storage.fs.fat32.readFile.readDir'); - dirs := readDirectory(volume, directory, statusOut); - datastart:= volume^.sectorStart + 1 + bootRecord^.FATSize + bootRecord^.rsvSectors; - push_trace('driver.storage.fs.fat32.readFile.searchFile'); - - { Split fileName into name + extension for FAT32 8.3 matching } - splitFileNameParts(fileName, namePart, extPart); - - if LL_size(dirs) > 0 then begin - cleanFileName := cleanString(namePart, statusOut); - - for i:=0 to LL_Size(dirs) -1 do begin - tempdir := PDirectory(LL_get(dirs, i)); - otherCleanFileName := cleanString(tempdir^.filename, statusout); - - if compareByteArray8(cleanFileName, otherCleanFileName) and matchExtension(tempdir^.fileExtension, extPart) then begin - dir:=tempdir; - exists := true; - statusOut^ := 0; - end; - end; - end; - - kfree(void(namePart)); - kfree(void(extPart)); - - - if exists = false then begin - push_trace('driver.storage.fs.fat32.readFile.notFound'); - io.syslog.logln('FAT32', 'readFile: file not found'); - LL_Free(dirs); - kfree(puint32(bootRecord)); - kfree(puint32(statusOut)); - exit(1); - end else begin - push_trace('driver.storage.fs.fat32.readFile.found'); - io.syslog.logln('FAT32', 'readFile: file found, reading clusters'); - cluster := uint32(dir^.clusterlow) or uint32(dir^.clusterhigh shl 16); - clusters := getFatChain(volume, cluster, bootRecord); - noClusters := LL_size(clusters); - - data := puint32(kalloc(noClusters * bootRecord^.spc * bootRecord^.sectorSize)); - if data = puint32(0) then begin - push_trace('UNABLE TO ALLOCATE MEMORY'); - io.syslog.logln('FAT32', 'readFile: OOM allocating data buffer'); - end; - memset(uint32(data), 0, noClusters * bootRecord^.spc * bootRecord^.sectorSize); - - readbuffer := puint32(kalloc(bootRecord^.sectorSize)); - if readbuffer = puint32(0) then begin - push_trace('UNABLE TO ALLOCATE MEMORY'); - io.syslog.logln('FAT32', 'readFile: OOM allocating read buffer'); - end; - - if noClusters * bootRecord^.spc > 0 then begin - for i:=0 to (noClusters * bootRecord^.spc) - 1 do begin - driver.storage.mgr.storage_read(volume^.device, dataStart + ((cluster * bootRecord^.spc) + i), 1, readbuffer); - memcpy(uint32(readbuffer), uint32(@data[i * bootRecord^.sectorSize div 4]), bootRecord^.sectorSize); - end; - end; - - kfree(readbuffer); - buffer^ := uint32(data); - { Report actual file size from directory entry, not cluster-based estimate } - if dir^.byteSize > 0 then - bytecount^ := dir^.byteSize - else - bytecount^ := noClusters * bootRecord^.spc * bootRecord^.sectorSize; - readFile:= statusOut^; - io.syslog.logln('FAT32', 'readFile: done'); - LL_Free(dirs); - LL_Free(clusters); - kfree(puint32(bootRecord)); - kfree(puint32(statusOut)); - push_trace('driver.storage.fs.fat32.readFile.exit'); - end; - -end; - // function checkExists(volume : PStorage_Volume; directory : pchar; fileName : pchar; fileExtension : pchar; entry : PDirectory_Entry) : uint32; // var // bootRecord : PBootRecord; @@ -2234,16 +2226,14 @@ begin kfree(buffer); end; -{ Read byteCount bytes from a file beginning at byte offset 'offset'. - Returns the number of bytes actually read (may be less than byteCount if EOF reached). - - NOTE: This implementation reads sectors sequentially from the first cluster of the - file, consistent with the existing readFile behaviour. It does NOT follow the FAT - chain between clusters \u2014 it assumes contiguous allocation. This is acceptable for - the initial streaming implementation. A future improvement should cache the last - visited (offset, cluster) pair in the open-file entry to make sequential chunked - reads O(1) per call instead of O(n) in the FAT chain length. } -function readFileAtOffset(volume : PStorage_Volume; directory : pchar; fileName : pchar; offset : uint32; buffer : puint32; byteCount : uint32) : uint32; +{ Write byteCount bytes to a file beginning at byte offset. + If offset + byteCount exceeds the current file size, the file is extended + by allocating new clusters. Returns the number of bytes actually written. } +function writeFileAtOffset(volume : PStorage_Volume; directory : pchar; + fileName : pchar; offset : uint32; + buffer : puint32; byteCount : uint32) : uint32; +const + MAX_RUN_SECTORS = 2048; { cap per multi-sector write = 1MB } var bootRecord : PBootRecord; dirs : PLinkedListBase; @@ -2252,15 +2242,13 @@ var i : uint32; exists : boolean; cluster : uint32; - clusters : PLinkedListBase; - noClusters : uint32; dataStart : uint32; cleanFileName : byteArray8; otherCFN : byteArray8; tempdir : PDirectory; namePart : pchar; extPart : pchar; - readbuffer : puint32; + ioBuf : puint32; bytesPerCluster : uint32; startClusterIdx : uint32; inClusterOffset : uint32; @@ -2270,13 +2258,547 @@ var curCluster : uint32; clusterLBA : uint32; sectorIdx : uint32; - destPos : uint32; + srcPos : uint32; remaining : uint32; bytesToCopy : uint32; fileSize : uint32; + newEnd : uint32; + oldClusters : uint32; + needClusters : uint32; + extraClusters : uint32; + newClusts : PLinkedListBase; + lastCluster : uint32; + nextCluster : uint32; + nextFat : uint32; + { dir entry update — raw sector scan (Lesson 18) } + dirCluster : uint32; + dirChain : PLinkedListBase; + dirBuf : puint32; + entriesPerSec : uint32; + entryIdx : uint32; + rawDir : PDirectory; + foundRaw : boolean; + dc : uint32; + ds : uint32; + newDirEntry : TDirectory; + { Multi-sector write variables } + spc : uint32; + secSize : uint32; + runStartClust : uint32; + runLen : uint32; + runSectors : uint32; + runBytes : uint32; + runLBA : uint32; + skipBytes : uint32; + copyBytes : uint32; + firstSectorOff : uint32; + partialBytes : uint32; + fullSectors : uint32; + batchBytes : uint32; + sectorLBA : uint32; + fatCache : PFATCache; +begin + push_trace('driver.storage.fs.fat32.writeFileAtOffset.enter'); + writeFileAtOffset := 0; + exists := false; + + statusOut := puint32(kalloc(sizeof(uint32))); + statusOut^ := 0; + bootRecord := readBootRecord(volume); + dirs := readDirectory(volume, directory, statusOut); + dataStart := volume^.sectorStart + 1 + bootRecord^.FATSize + bootRecord^.rsvSectors; + + splitFileNameParts(fileName, namePart, extPart); + + if LL_size(dirs) > 0 then begin + cleanFileName := cleanString(namePart, statusOut); + for i := 0 to LL_Size(dirs) - 1 do begin + tempdir := PDirectory(LL_get(dirs, i)); + otherCFN := cleanString(tempdir^.filename, statusOut); + if compareByteArray8(cleanFileName, otherCFN) and matchExtension(tempdir^.fileExtension, extPart) then begin + dir := tempdir; + exists := true; + end; + end; + end; + + kfree(void(namePart)); + kfree(void(extPart)); + + if not exists then begin + { --- Create a new zero-length file entry on disk --- } + + { Determine parent cluster (Lesson 18: root has no '.' entry) } + if (directory = nil) or (stringSize(directory) = 0) then + dirCluster := bootRecord^.rootCluster + else begin + if LL_size(dirs) > 0 then begin + tempdir := PDirectory(LL_get(dirs, 0)); + if tempdir^.fileName[0] = '.' then + dirCluster := uint32(tempdir^.clusterLow) + or uint32(tempdir^.clusterHigh shl 16) + else + dirCluster := bootRecord^.rootCluster; + end else + dirCluster := bootRecord^.rootCluster; + end; + + entriesPerSec := bootRecord^.sectorSize div uint32(sizeof(TDirectory)); + dirChain := getFatChain(volume, dirCluster, bootRecord); + dirBuf := puint32(kalloc(bootRecord^.sectorSize)); + foundRaw := false; + + splitFileNameParts(fileName, namePart, extPart); + cleanFileName := cleanString(namePart, statusOut); + + if LL_size(dirChain) > 0 then begin + for dc := 0 to LL_size(dirChain) - 1 do begin + curCluster := puint32(LL_Get(dirChain, dc))^; + clusterLBA := dataStart + (curCluster * uint32(bootRecord^.spc)); + for ds := 0 to uint32(bootRecord^.spc) - 1 do begin + driver.storage.mgr.storage_read(volume^.device, clusterLBA + ds, 1, dirBuf); + if entriesPerSec > 0 then begin + for entryIdx := 0 to entriesPerSec - 1 do begin + rawDir := @PDirectory(dirBuf)[entryIdx]; + if (rawDir^.fileName[0] = char(0)) or (rawDir^.fileName[0] = char($E5)) then begin + { Found a free slot — write the new file entry } + memset(uint32(rawDir), 0, sizeof(TDirectory)); + rawDir^.fileName := cleanFileName; + rawDir^.attributes := 0; { regular file } + rawDir^.fileExtension[0] := ' '; + rawDir^.fileExtension[1] := ' '; + rawDir^.fileExtension[2] := ' '; + if (extPart <> nil) and (extPart[0] <> char(0)) then begin + for i := 0 to 2 do begin + if extPart[i] = char(0) then break; + if (extPart[i] >= 'a') and (extPart[i] <= 'z') then + rawDir^.fileExtension[i] := char(uint8(extPart[i]) - 32) + else + rawDir^.fileExtension[i] := extPart[i]; + end; + end; + rawDir^.clusterLow := 0; + rawDir^.clusterHigh := 0; + rawDir^.byteSize := 0; + driver.storage.mgr.storage_write(volume^.device, clusterLBA + ds, 1, dirBuf); + foundRaw := true; + break; + end; + end; + end; + if foundRaw then break; + end; + if foundRaw then break; + end; + end; + + kfree(void(namePart)); + kfree(void(extPart)); + kfree(dirBuf); + LL_Free(dirChain); + + if not foundRaw then begin + io.syslog.logln('FAT32', 'writeFileAtOffset: exit-A no free dir slot'); + LL_Free(dirs); + kfree(puint32(bootRecord)); + kfree(puint32(statusOut)); + exit; + end; + + { Invalidate chain cache — new file } + sioc_valid := false; + + { Set up a local dir record so the rest of the function can use it } + memset(uint32(@newDirEntry), 0, sizeof(TDirectory)); + newDirEntry.byteSize := 0; + newDirEntry.clusterLow := 0; + newDirEntry.clusterHigh := 0; + dir := @newDirEntry; + end; + + fileSize := dir^.byteSize; + cluster := uint32(dir^.clusterlow) or uint32(dir^.clusterhigh shl 16); + newEnd := offset + byteCount; + bytesPerCluster := uint32(bootRecord^.spc) * uint32(bootRecord^.sectorSize); + + { ---- Extend file if writing past EOF ---- } + if newEnd > fileSize then begin + if fileSize = 0 then + oldClusters := 0 + else + oldClusters := (fileSize + bytesPerCluster - 1) div bytesPerCluster; + needClusters := (newEnd + bytesPerCluster - 1) div bytesPerCluster; + + if needClusters > oldClusters then begin + extraClusters := needClusters - oldClusters; + newClusts := findFreeClusters(volume, extraClusters, bootRecord); + if newClusts = nil then begin + io.syslog.logln('FAT32', 'writeFileAtOffset: exit-B disk full'); + LL_Free(dirs); + kfree(puint32(bootRecord)); + kfree(puint32(statusOut)); + exit; + end; + + if oldClusters > 0 then begin + { Walk to last cluster using cache if available } + if sioc_valid and (sioc_vol = volume) and (sioc_fileClust = cluster) then + lastCluster := sioc_chainClust + else + lastCluster := cluster; + nextFat := readFat(volume, lastCluster, bootRecord); + while ((nextFat and $0FFFFFFF) < $0FFFFFF8) and (nextFat <> 0) do begin + lastCluster := nextFat; + nextFat := readFat(volume, lastCluster, bootRecord); + end; + end else begin + { Zero-length file: first new cluster becomes start cluster } + lastCluster := 0; + end; + + { Link new clusters } + if extraClusters > 0 then begin + for i := 0 to extraClusters - 1 do begin + nextCluster := puint32(LL_Get(newClusts, i))^; + if lastCluster <> 0 then + writeFat(volume, lastCluster, nextCluster, bootRecord); + lastCluster := nextCluster; + end; + writeFat(volume, lastCluster, $FFFFFFF8, bootRecord); + end; + + { If file was zero-length, update dir entry's cluster pointer } + if oldClusters = 0 then begin + cluster := puint32(LL_Get(newClusts, 0))^; + dir^.clusterlow := uint16(cluster and $FFFF); + dir^.clusterhigh := uint16((cluster shr 16) and $FFFF); + end; + + { Update chain cache: last new cluster is the new tail } + sioc_vol := volume; + sioc_fileClust := cluster; + sioc_chainIdx := needClusters - 1; + sioc_chainClust := lastCluster; + sioc_valid := true; + + LL_Free(newClusts); + + end; + + fileSize := newEnd; + end; + + { ---- Walk to starting cluster using cache, then write sector by sector ---- } + startClusterIdx := offset div bytesPerCluster; + inClusterOffset := offset mod bytesPerCluster; + + { Determine curCluster at startClusterIdx using cache } + if sioc_valid and (sioc_vol = volume) and (sioc_fileClust = cluster) + and (sioc_chainIdx <= startClusterIdx) then begin + curCluster := sioc_chainClust; + chainPos := sioc_chainIdx; + end else begin + curCluster := cluster; + chainPos := 0; + end; + + { Walk forward from cached/start position to startClusterIdx } + while chainPos < startClusterIdx do begin + nextFat := readFat(volume, curCluster, bootRecord); + if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then begin + io.syslog.log('FAT32', 'writeFileAtOffset: exit-C chain broke at chainPos='); + io.syslog.writeint(chainPos); + io.syslog.writestring(' target='); + io.syslog.writeint(startClusterIdx); + io.syslog.writestring(' cluster='); + io.syslog.writeint(curCluster); + io.syslog.writestring(' fat='); + io.syslog.writehexln(nextFat); + LL_Free(dirs); + kfree(puint32(bootRecord)); + kfree(puint32(statusOut)); + exit; + end; + curCluster := nextFat; + chainPos := chainPos + 1; + end; + + spc := uint32(bootRecord^.spc); + secSize := uint32(bootRecord^.sectorSize); + ioBuf := puint32(kalloc(secSize)); + remaining := byteCount; + srcPos := 0; + + while (remaining > 0) do begin + { Build contiguous cluster run starting at curCluster } + runStartClust := curCluster; + runLen := 1; + + while (runLen * spc) < MAX_RUN_SECTORS do begin + nextFat := readFat(volume, curCluster, bootRecord); + if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then + break; + if nextFat <> curCluster + 1 then + break; + curCluster := nextFat; + runLen := runLen + 1; + end; + + runSectors := runLen * spc; + runBytes := runSectors * secSize; + runLBA := dataStart + (runStartClust * spc); + + if srcPos = 0 then + skipBytes := inClusterOffset + else + skipBytes := 0; + + copyBytes := runBytes - skipBytes; + if copyBytes > remaining then + copyBytes := remaining; + + firstSectorOff := skipBytes mod secSize; + + { Phase 1: Partial first sector — read-modify-write } + if firstSectorOff > 0 then begin + sectorLBA := runLBA + (skipBytes div secSize); + partialBytes := secSize - firstSectorOff; + if partialBytes > copyBytes then + partialBytes := copyBytes; + driver.storage.mgr.storage_read(volume^.device, sectorLBA, 1, ioBuf); + core.util.memcpy(uint32(buffer) + srcPos, uint32(ioBuf) + firstSectorOff, partialBytes); + driver.storage.mgr.storage_write(volume^.device, sectorLBA, 1, ioBuf); + srcPos := srcPos + partialBytes; + remaining := remaining - partialBytes; + copyBytes := copyBytes - partialBytes; + skipBytes := skipBytes + partialBytes; + end; + + { Phase 2: Full sectors — batch write directly from caller buffer } + fullSectors := copyBytes div secSize; + if fullSectors > 0 then begin + sectorLBA := runLBA + (skipBytes div secSize); + batchBytes := fullSectors * secSize; + driver.storage.mgr.storage_write(volume^.device, sectorLBA, fullSectors, + puint32(uint32(buffer) + srcPos)); + srcPos := srcPos + batchBytes; + remaining := remaining - batchBytes; + copyBytes := copyBytes - batchBytes; + skipBytes := skipBytes + batchBytes; + end; + + { Phase 3: Partial last sector — read-modify-write } + if copyBytes > 0 then begin + sectorLBA := runLBA + (skipBytes div secSize); + driver.storage.mgr.storage_read(volume^.device, sectorLBA, 1, ioBuf); + core.util.memcpy(uint32(buffer) + srcPos, uint32(ioBuf), copyBytes); + driver.storage.mgr.storage_write(volume^.device, sectorLBA, 1, ioBuf); + srcPos := srcPos + copyBytes; + remaining := remaining - copyBytes; + end; + + { Update chain cache to last cluster in this run } + sioc_vol := volume; + sioc_fileClust := cluster; + sioc_chainIdx := chainPos + runLen - 1; + sioc_chainClust := curCluster; + sioc_valid := true; + chainPos := chainPos + runLen; + + if remaining > 0 then begin + nextFat := readFat(volume, curCluster, bootRecord); + if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then begin + io.syslog.log('FAT32', 'writeFileAtOffset: exit-D chain broke in write loop, srcPos='); + io.syslog.writeint(srcPos); + io.syslog.writestring(' remaining='); + io.syslog.writeint(remaining); + io.syslog.writestring(' cluster='); + io.syslog.writeint(curCluster); + io.syslog.writestring(' fat='); + io.syslog.writehexln(nextFat); + break; + end; + curCluster := nextFat; + end; + end; + + kfree(ioBuf); + + { Flush dirty FAT cache entries to disk } + fatCache := fatCacheGet(volume); + if fatCache <> nil then + fatCacheFlush(fatCache, volume); + + { ---- Update directory entry byteSize if file was extended ---- } + { Only commit actual bytes written — never advance size past what was really written (Lesson 22) } + if (srcPos > 0) and ((offset + srcPos) > dir^.byteSize) then begin + { Lesson 18: search raw directory sectors, don't use LL indices. } + if (directory = nil) or (stringSize(directory) = 0) then + dirCluster := bootRecord^.rootCluster + else begin + dirCluster := bootRecord^.rootCluster; + if LL_size(dirs) > 0 then begin + tempdir := PDirectory(LL_get(dirs, 0)); + if tempdir^.fileName[0] = '.' then + dirCluster := uint32(tempdir^.clusterLow) + or uint32(tempdir^.clusterHigh shl 16); + end; + end; + + dirChain := getFatChain(volume, dirCluster, bootRecord); + entriesPerSec := bootRecord^.sectorSize div uint32(sizeof(TDirectory)); + dirBuf := puint32(kalloc(bootRecord^.sectorSize)); + foundRaw := false; + + splitFileNameParts(fileName, namePart, extPart); + cleanFileName := cleanString(namePart, statusOut); + + if LL_size(dirChain) > 0 then begin + for dc := 0 to LL_size(dirChain) - 1 do begin + curCluster := puint32(LL_Get(dirChain, dc))^; + clusterLBA := dataStart + (curCluster * uint32(bootRecord^.spc)); + for ds := 0 to uint32(bootRecord^.spc) - 1 do begin + driver.storage.mgr.storage_read(volume^.device, clusterLBA + ds, 1, dirBuf); + if entriesPerSec > 0 then begin + for entryIdx := 0 to entriesPerSec - 1 do begin + rawDir := @PDirectory(dirBuf)[entryIdx]; + if rawDir^.fileName[0] = char(0) then begin + foundRaw := true; + break; + end; + if rawDir^.fileName[0] = char($E5) then continue; + otherCFN := cleanString(rawDir^.filename, statusOut); + if compareByteArray8(cleanFileName, otherCFN) and matchExtension(rawDir^.fileExtension, extPart) then begin + rawDir^.byteSize := offset + srcPos; + { If file was zero-length, update cluster pointers too } + if dir^.byteSize = 0 then begin + rawDir^.clusterlow := dir^.clusterlow; + rawDir^.clusterhigh := dir^.clusterhigh; + end; + driver.storage.mgr.storage_write(volume^.device, clusterLBA + ds, 1, dirBuf); + foundRaw := true; + break; + end; + end; + end; + if foundRaw then break; + end; + if foundRaw then break; + end; + end; + + kfree(void(namePart)); + kfree(void(extPart)); + kfree(dirBuf); + LL_Free(dirChain); + end; + + LL_Free(dirs); + kfree(puint32(bootRecord)); + kfree(puint32(statusOut)); + writeFileAtOffset := srcPos; + push_trace('driver.storage.fs.fat32.writeFileAtOffset.exit'); +end; + +{ Read byteCount bytes from a file beginning at byte offset 'offset'. + Returns the number of bytes actually read (may be less than byteCount if EOF reached). + + NOTE: This implementation reads sectors sequentially from the first cluster of the + file, consistent with the existing readFile behaviour. It does NOT follow the FAT + chain between clusters \u2014 it assumes contiguous allocation. This is acceptable for + the initial streaming implementation. A future improvement should cache the last + visited (offset, cluster) pair in the open-file entry to make sequential chunked + reads O(1) per call instead of O(n) in the FAT chain length. } + +function getFileSize(volume : PStorage_Volume; directory : pchar; fileName : pchar) : uint32; +var + bootRecord : PBootRecord; + dirs : PLinkedListBase; + tempdir : PDirectory; + statusOut : puint32; + i : uint32; + cleanFileName : byteArray8; + otherCFN : byteArray8; + namePart : pchar; + extPart : pchar; + fsize : uint32; +begin + push_trace('driver.storage.fs.fat32.getFileSize.enter'); + getFileSize := 0; + + statusOut := puint32(kalloc(sizeof(uint32))); + statusOut^ := 0; + bootRecord := readBootRecord(volume); + dirs := readDirectory(volume, directory, statusOut); + + splitFileNameParts(fileName, namePart, extPart); + + fsize := 0; + if LL_size(dirs) > 0 then begin + cleanFileName := cleanString(namePart, statusOut); + for i := 0 to LL_Size(dirs) - 1 do begin + tempdir := PDirectory(LL_get(dirs, i)); + otherCFN := cleanString(tempdir^.filename, statusOut); + if compareByteArray8(cleanFileName, otherCFN) and matchExtension(tempdir^.fileExtension, extPart) then begin + fsize := tempdir^.byteSize; + break; + end; + end; + end; + + kfree(void(namePart)); + kfree(void(extPart)); + LL_Free(dirs); + kfree(puint32(bootRecord)); + kfree(puint32(statusOut)); + getFileSize := fsize; + push_trace('driver.storage.fs.fat32.getFileSize.exit'); +end; + +function readFileAtOffset(volume : PStorage_Volume; directory : pchar; fileName : pchar; offset : uint32; buffer : puint32; byteCount : uint32) : uint32; +const + MAX_RUN_SECTORS = 2048; { cap per multi-sector read = 1 MB } +var + bootRecord : PBootRecord; + dirs : PLinkedListBase; + dir : PDirectory; + statusOut : puint32; + i : uint32; + exists : boolean; + cluster : uint32; + dataStart : uint32; + cleanFileName : byteArray8; + otherCFN : byteArray8; + tempdir : PDirectory; + namePart : pchar; + extPart : pchar; + bytesPerCluster : uint32; + startClusterIdx : uint32; + inClusterOffset : uint32; + chainPos : uint32; + curCluster : uint32; + destPos : uint32; + remaining : uint32; + fileSize : uint32; + nextFat : uint32; + spc : uint32; + secSize : uint32; + { Multi-sector run variables } + runStartClust : uint32; + runLen : uint32; { clusters in current contiguous run } + runSectors : uint32; + runBytes : uint32; + runBuf : puint32; + runBufBytes : uint32; + skipBytes : uint32; { bytes to skip at start of run buffer } + copyBytes : uint32; + chunkSectors : uint32; + chunkBytes : uint32; + runLBA : uint32; + ioBuf : puint32; + partialBytes : uint32; begin push_trace('driver.storage.fs.fat32.readFileAtOffset.enter'); - io.syslog.logln('FAT32', 'readFileAtOffset: enter'); readFileAtOffset := 0; exists := false; @@ -2296,7 +2818,6 @@ begin if compareByteArray8(cleanFileName, otherCFN) and matchExtension(tempdir^.fileExtension, extPart) then begin dir := tempdir; exists := true; - statusOut^ := 0; end; end; end; @@ -2305,7 +2826,6 @@ begin kfree(void(extPart)); if not exists then begin - io.syslog.logln('FAT32', 'readFileAtOffset: file not found'); LL_Free(dirs); kfree(puint32(bootRecord)); kfree(puint32(statusOut)); @@ -2315,13 +2835,9 @@ begin fileSize := dir^.byteSize; cluster := uint32(dir^.clusterlow) or uint32(dir^.clusterhigh shl 16); - clusters := getFatChain(volume, cluster, bootRecord); - noClusters := LL_size(clusters); if offset >= fileSize then begin - io.syslog.logln('FAT32', 'readFileAtOffset: offset past EOF'); LL_Free(dirs); - LL_Free(clusters); kfree(puint32(bootRecord)); kfree(puint32(statusOut)); exit; @@ -2332,69 +2848,121 @@ begin if offset + remaining > fileSize then remaining := fileSize - offset; - bytesPerCluster := uint32(bootRecord^.spc) * uint32(bootRecord^.sectorSize); - - { Determine which cluster in the chain contains the starting offset } + spc := uint32(bootRecord^.spc); + secSize := uint32(bootRecord^.sectorSize); + bytesPerCluster := spc * secSize; startClusterIdx := offset div bytesPerCluster; inClusterOffset := offset mod bytesPerCluster; - if startClusterIdx >= noClusters then begin - io.syslog.logln('FAT32', 'readFileAtOffset: startCluster past chain'); - LL_Free(dirs); - LL_Free(clusters); - kfree(puint32(bootRecord)); - kfree(puint32(statusOut)); - exit; + { Determine curCluster at startClusterIdx using SIOC cache } + if sioc_valid and (sioc_vol = volume) and (sioc_fileClust = cluster) + and (sioc_chainIdx <= startClusterIdx) then begin + curCluster := sioc_chainClust; + chainPos := sioc_chainIdx; + end else begin + curCluster := cluster; + chainPos := 0; end; - readbuffer := puint32(kalloc(bootRecord^.sectorSize)); - destPos := 0; - chainPos := startClusterIdx; + { Walk forward to startClusterIdx } + while chainPos < startClusterIdx do begin + nextFat := readFat(volume, curCluster, bootRecord); + if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then begin + LL_Free(dirs); + kfree(puint32(bootRecord)); + kfree(puint32(statusOut)); + exit; + end; + curCluster := nextFat; + chainPos := chainPos + 1; + end; - while (remaining > 0) and (chainPos < noClusters) do begin - curCluster := puint32(LL_Get(clusters, chainPos))^; - clusterLBA := dataStart + (curCluster * uint32(bootRecord^.spc)); + destPos := 0; - { Determine starting sector and byte offset within this cluster } - if chainPos = startClusterIdx then begin - startSecInClust := inClusterOffset div uint32(bootRecord^.sectorSize); - secByteOff := inClusterOffset mod uint32(bootRecord^.sectorSize); + while (remaining > 0) do begin + { Build a contiguous cluster run starting at curCluster } + runStartClust := curCluster; + runLen := 1; + + { Scan ahead for contiguous clusters, capped by MAX_RUN_SECTORS worth } + while (runLen * spc) < MAX_RUN_SECTORS do begin + nextFat := readFat(volume, curCluster, bootRecord); + if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then + break; + if nextFat <> curCluster + 1 then + break; + curCluster := nextFat; + runLen := runLen + 1; + end; + + runSectors := runLen * spc; + runBytes := runSectors * secSize; + + { Calculate how many bytes from this run we actually need } + if destPos = 0 then + skipBytes := inClusterOffset + else + skipBytes := 0; + + copyBytes := runBytes - skipBytes; + if copyBytes > remaining then + copyBytes := remaining; + + { Determine if we can read directly into caller's buffer (aligned, full run) } + if (skipBytes = 0) and (copyBytes >= runBytes) then begin + { Direct read into output buffer — no temp allocation } + runLBA := dataStart + (runStartClust * spc); + driver.storage.mgr.storage_read(volume^.device, runLBA, runSectors, + puint32(uint32(buffer) + destPos)); + destPos := destPos + copyBytes; + remaining := remaining - copyBytes; + end else if (skipBytes = 0) and ((copyBytes mod secSize) = 0) then begin + { Partial run but sector-aligned — read only needed sectors } + chunkSectors := copyBytes div secSize; + runLBA := dataStart + (runStartClust * spc); + driver.storage.mgr.storage_read(volume^.device, runLBA, chunkSectors, + puint32(uint32(buffer) + destPos)); + destPos := destPos + copyBytes; + remaining := remaining - copyBytes; end else begin - startSecInClust := 0; - secByteOff := 0; + { Unaligned — need temp buffer for partial first/last sectors } + { Only allocate for sectors we actually touch } + chunkSectors := (skipBytes + copyBytes + secSize - 1) div secSize; + if chunkSectors > runSectors then chunkSectors := runSectors; + chunkBytes := chunkSectors * secSize; + runBuf := puint32(kalloc(chunkBytes)); + runLBA := dataStart + (runStartClust * spc) + (skipBytes div secSize); + + { Adjust skipBytes for sectors we're skipping entirely } + skipBytes := skipBytes mod secSize; + driver.storage.mgr.storage_read(volume^.device, runLBA, chunkSectors, runBuf); + core.util.memcpy(uint32(runBuf) + skipBytes, uint32(buffer) + destPos, copyBytes); + kfree(runBuf); + destPos := destPos + copyBytes; + remaining := remaining - copyBytes; end; - sectorIdx := startSecInClust; - while (sectorIdx < uint32(bootRecord^.spc)) and (remaining > 0) do begin - driver.storage.mgr.storage_read(volume^.device, clusterLBA + sectorIdx, 1, readbuffer); + { Update SIOC chain cache to last cluster in this run } + sioc_vol := volume; + sioc_fileClust := cluster; + sioc_chainIdx := chainPos + runLen - 1; + sioc_chainClust := curCluster; + sioc_valid := true; + chainPos := chainPos + runLen; - if secByteOff > 0 then begin - { Partial first sector — skip bytes before the offset } - bytesToCopy := uint32(bootRecord^.sectorSize) - secByteOff; - if bytesToCopy > remaining then bytesToCopy := remaining; - core.util.memcpy(uint32(readbuffer) + secByteOff, uint32(buffer) + destPos, bytesToCopy); - secByteOff := 0; { Only applies to the very first sector } - end else begin - bytesToCopy := uint32(bootRecord^.sectorSize); - if bytesToCopy > remaining then bytesToCopy := remaining; - core.util.memcpy(uint32(readbuffer), uint32(buffer) + destPos, bytesToCopy); - end; - - destPos := destPos + bytesToCopy; - remaining := remaining - bytesToCopy; - sectorIdx := sectorIdx + 1; + { Advance to next cluster beyond this run } + if remaining > 0 then begin + nextFat := readFat(volume, curCluster, bootRecord); + if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then + break; + curCluster := nextFat; end; - - chainPos := chainPos + 1; end; - kfree(readbuffer); LL_Free(dirs); - LL_Free(clusters); kfree(puint32(bootRecord)); kfree(puint32(statusOut)); readFileAtOffset := destPos; - io.syslog.logln('FAT32', 'readFileAtOffset: done'); push_trace('driver.storage.fs.fat32.readFileAtOffset.exit'); end; @@ -2409,9 +2977,9 @@ begin filesystem.createcallback:= @create_volume; filesystem.createAsyncCallback := @create_volume_async; filesystem.detectcallback:= @detect_volumes; - filesystem.writecallback:= @writeFile; - filesystem.readcallback := @readFile; filesystem.readOffsetCallback := @readFileAtOffset; + filesystem.writeOffsetCallback := @writeFileAtOffset; + filesystem.fileSizeCallback := @getFileSize; filesystem.identifyCallback := @identify_volume; filesystem.deleteFileCallback := @deleteFile; filesystem.deleteDirCallback := @deleteDir; @@ -2419,8 +2987,6 @@ begin { Async callbacks: nil - VFS falls through to the sync callbacks above. All disk I/O uses psAwaiting in submit_io_wait: the calling process is parked cleanly (CPU-free) while the driver.storage.ctl.ahci ISR completes the request. } - filesystem.writeAsyncCallback := nil; - filesystem.readAsyncCallback := nil; filesystem.createDirAsyncCallback := nil; filesystem.readDirAsyncCallback := nil; filesystem.deleteFileAsyncCallback := nil; diff --git a/src/driver/storage/fs/driver.storage.fs.flatfs.pas b/src/driver/storage/fs/driver.storage.fs.flatfs.pas index bbd63078..bf1f7fec 100644 --- a/src/driver/storage/fs/driver.storage.fs.flatfs.pas +++ b/src/driver/storage/fs/driver.storage.fs.flatfs.pas @@ -843,12 +843,106 @@ begin readDirectoryEntries := entries; end; +function getFileSize(volume : PStorage_Volume; directory : pchar; fileName : pchar) : uint32; +var + fileEntries : PFile_Entry; + fileCount : uint32; + i : uint32; +begin + getFileSize := 0; + fileCount := 1000; + fileEntries := PFile_Entry(kalloc(sizeof(TFile_Entry) * fileCount + 512)); + memset(uint32(fileEntries), 0, sizeof(TFile_Entry) * fileCount); + driver.storage.mgr.storage_read(volume^.device, volume^.sectorStart + 1, + sizeof(TFile_Entry) * fileCount div 512, puint32(fileEntries)); + + for i := 0 to fileCount - 1 do begin + if fileEntries[i].attribues = 0 then break; + if stringEquals(fileEntries[i].name, fileName) then begin + getFileSize := fileEntries[i].size; + break; + end; + end; + + kfree(puint32(fileEntries)); +end; + +function readFileAtOffset(volume : PStorage_Volume; directory : pchar; + fileName : pchar; offset : uint32; + buffer : puint32; byteCount : uint32) : uint32; +var + fileEntries : PFile_Entry; + fileCount : uint32; + i : uint32; + fileStart : uint32; + fileSize : uint32; + remaining : uint32; + secOff : uint32; + curSec : uint32; + destPos : uint32; + bytesToCopy : uint32; + secBuf : puint32; + found : boolean; +begin + readFileAtOffset := 0; + fileCount := 1000; + found := false; + + fileEntries := PFile_Entry(kalloc(sizeof(TFile_Entry) * fileCount + 512)); + memset(uint32(fileEntries), 0, sizeof(TFile_Entry) * fileCount); + driver.storage.mgr.storage_read(volume^.device, volume^.sectorStart + 1, + sizeof(TFile_Entry) * fileCount div 512, puint32(fileEntries)); + + for i := 0 to fileCount - 1 do begin + if fileEntries[i].attribues = 0 then break; + if stringEquals(fileEntries[i].name, fileName) then begin + fileStart := fileEntries[i].start; + fileSize := fileEntries[i].size; + found := true; + break; + end; + end; + + kfree(puint32(fileEntries)); + if not found then exit; + + { Clamp to EOF } + if offset >= fileSize then exit; + remaining := fileSize - offset; + if byteCount < remaining then remaining := byteCount; + + secBuf := puint32(kalloc(512)); + if secBuf = nil then exit; + + destPos := 0; + curSec := fileStart + (offset div 512); + secOff := offset mod 512; + + while remaining > 0 do begin + driver.storage.mgr.storage_read(volume^.device, curSec, 1, secBuf); + + bytesToCopy := 512 - secOff; + if bytesToCopy > remaining then bytesToCopy := remaining; + core.util.memcpy(uint32(secBuf) + secOff, uint32(buffer) + destPos, bytesToCopy); + + destPos := destPos + bytesToCopy; + remaining := remaining - bytesToCopy; + secOff := 0; + curSec := curSec + 1; + end; + + kfree(secBuf); + readFileAtOffset := destPos; +end; + procedure init(); begin push_trace('driver.storage.fs.flatfs.init()'); filesystem.sName:= 'driver.storage.fs.flatfs'; filesystem.system_id:= $02; filesystem.readDirCallback:= @readDirectoryEntries; + filesystem.fileSizeCallback:= @getFileSize; + filesystem.readOffsetCallback:= @readFileAtOffset; // filesystem.createDirCallback:= @writeDirectoryGen; filesystem.createcallback:= @create_volume; filesystem.detectcallback:= @detect_volumes; diff --git a/src/driver/storage/fs/driver.storage.fs.iso9660.pas b/src/driver/storage/fs/driver.storage.fs.iso9660.pas index b456bba9..d96e5e4c 100644 --- a/src/driver/storage/fs/driver.storage.fs.iso9660.pas +++ b/src/driver/storage/fs/driver.storage.fs.iso9660.pas @@ -88,9 +88,6 @@ var filesystem : TFilesystem; procedure init(); -procedure readFile_async(volume : PStorage_Volume; directory : pchar; fileName : pchar; - buffer : puint32; bytecount : puint32; - callback : TIOCallback; callbackData : pointer); procedure readDir_async(volume : PStorage_Volume; directory : pchar; resultList : PPLinkedListBase; status : puint32; callback : TIOCallback; callbackData : pointer); @@ -102,17 +99,6 @@ uses proc.types; type - TISORdCtx = record - Volume : PStorage_Volume; - Directory : pchar; - FileName : pchar; - RBuffer : puint32; - RByteCount : puint32; - Callback : TIOCallback; - CallbackData : pointer; - end; - PISORdCtx = ^TISORdCtx; - TISODirCtx = record Volume : PStorage_Volume; Directory : pchar; @@ -584,109 +570,8 @@ begin readDirectoryEntries := entries; end; -{ ReadFile: read file contents into buffer. - directory = parent directory path (e.g. '/' or '/BOOT') - fileName = file name (e.g. 'README.TXT') - buffer = destination buffer (caller allocates) - bytecount = pointer to uint32 receiving bytes read - Returns 0 on success, 1 on file not found. } -function readFile(volume : PStorage_Volume; directory : pchar; fileName : pchar; - buffer : puint32; bytecount : puint32) : uint32; -var - buf : puint32; - pvd : PPVD; - dirLBA, dirLen : uint32; - fileLBA, fileLen : uint32; - isDirFlag : uint32; - dirBuf : puint32; - fileBuf : puint32; - fullPath : pchar; - dirSize, nameSize : uint32; -begin - readFile := 1; - - if volume^.device = nil then exit; - - { Read PVD } - buf := readSectors(volume^.device, volume^.sectorStart + PVD_SECTOR, 1); - if (buf = nil) or (not isPVD(buf)) then begin - if buf <> nil then kfree(buf); - exit; - end; - pvd := PPVD(buf); - - { Resolve the parent directory } - if (directory = nil) or (stringSize(directory) = 0) or - ((stringSize(directory) = 1) and (puint8(directory)^ = ord('/'))) then begin - dirLBA := rootLBA(pvd); - dirLen := rootLen(pvd); - end else begin - if not resolvePath(volume^.device, pvd, directory, @dirLBA, @dirLen, @isDirFlag) then begin - kfree(buf); - exit; - end; - end; - - kfree(buf); - - { Read the directory and find the file } - dirBuf := readExtent(volume^.device, dirLBA, dirLen); - if dirBuf = nil then exit; - - if not findEntry(dirBuf, dirLen, fileName, @fileLBA, @fileLen, @isDirFlag) then begin - kfree(dirBuf); - exit; - end; - kfree(dirBuf); - - if isDirFlag <> 0 then exit; - - { Read the file data — store pointer in buffer^ for the VFS to own } - fileBuf := readExtent(volume^.device, fileLBA, fileLen); - if fileBuf = nil then exit; - buffer^ := uint32(fileBuf); - bytecount^ := fileLen; - - readFile := 0; -end; - { ==================== Async hooks ==================== } -procedure readFile_worker(pctx : PProcessContext); -var - ctx : PISORdCtx; - err : uint32; - error : TError; -begin - ctx := PISORdCtx(pctx^.Local); - err := readFile(ctx^.Volume, ctx^.Directory, ctx^.FileName, ctx^.RBuffer, ctx^.RByteCount); - if err = 0 then error := eNone else error := eFileDoesNotExist; - if ctx^.Callback <> nil then ctx^.Callback(error, ctx^.CallbackData); - if ctx^.Directory <> nil then kfree(void(ctx^.Directory)); - if ctx^.FileName <> nil then kfree(void(ctx^.FileName)); - kfree(void(ctx)); - proc.mgr.proc_exit(0); -end; - -procedure readFile_async(volume : PStorage_Volume; directory : pchar; fileName : pchar; - buffer : puint32; bytecount : puint32; - callback : TIOCallback; callbackData : pointer); -var - ctx : PISORdCtx; -begin - ctx := PISORdCtx(kalloc(sizeof(TISORdCtx))); - ctx^.Volume := volume; - ctx^.Directory := nil; - ctx^.FileName := nil; - if directory <> nil then ctx^.Directory := stringCopy(directory); - if fileName <> nil then ctx^.FileName := stringCopy(fileName); - ctx^.RBuffer := buffer; - ctx^.RByteCount := bytecount; - ctx^.Callback := callback; - ctx^.CallbackData := callbackData; - proc.mgr.create('iso.rd', @readFile_worker, void(ctx), 1); -end; - procedure readDir_worker(pctx : PProcessContext); var ctx : PISODirCtx; @@ -720,6 +605,135 @@ begin proc.mgr.create('iso.rdir', @readDir_worker, void(ctx), 1); end; +function getFileSize(volume : PStorage_Volume; directory : pchar; fileName : pchar) : uint32; +var + buf : puint32; + pvd : PPVD; + dirLBA : uint32; + dirLen : uint32; + fileLBA : uint32; + fileLen : uint32; + isDirFlag : uint32; + dirBuf : puint32; +begin + getFileSize := 0; + if volume^.device = nil then exit; + + buf := readSectors(volume^.device, volume^.sectorStart + PVD_SECTOR, 1); + if (buf = nil) or (not isPVD(buf)) then begin + if buf <> nil then kfree(buf); + exit; + end; + pvd := PPVD(buf); + + if (directory = nil) or (stringSize(directory) = 0) or + ((stringSize(directory) = 1) and (puint8(directory)^ = ord('/'))) then begin + dirLBA := rootLBA(pvd); + dirLen := rootLen(pvd); + end else begin + if not resolvePath(volume^.device, pvd, directory, @dirLBA, @dirLen, @isDirFlag) then begin + kfree(buf); + exit; + end; + end; + kfree(buf); + + dirBuf := readExtent(volume^.device, dirLBA, dirLen); + if dirBuf = nil then exit; + + if findEntry(dirBuf, dirLen, fileName, @fileLBA, @fileLen, @isDirFlag) then + getFileSize := fileLen; + + kfree(dirBuf); +end; + +function readFileAtOffset(volume : PStorage_Volume; directory : pchar; + fileName : pchar; offset : uint32; + buffer : puint32; byteCount : uint32) : uint32; +var + buf : puint32; + pvd : PPVD; + dirLBA : uint32; + dirLen : uint32; + fileLBA : uint32; + fileLen : uint32; + isDirFlag : uint32; + dirBuf : puint32; + secSize : uint32; + remaining : uint32; + secOff : uint32; + curLBA : uint32; + destPos : uint32; + bytesToCopy: uint32; + secBuf : puint32; +begin + readFileAtOffset := 0; + if volume^.device = nil then exit; + + { Read PVD } + buf := readSectors(volume^.device, volume^.sectorStart + PVD_SECTOR, 1); + if (buf = nil) or (not isPVD(buf)) then begin + if buf <> nil then kfree(buf); + exit; + end; + pvd := PPVD(buf); + + { Resolve parent directory } + if (directory = nil) or (stringSize(directory) = 0) or + ((stringSize(directory) = 1) and (puint8(directory)^ = ord('/'))) then begin + dirLBA := rootLBA(pvd); + dirLen := rootLen(pvd); + end else begin + if not resolvePath(volume^.device, pvd, directory, @dirLBA, @dirLen, @isDirFlag) then begin + kfree(buf); + exit; + end; + end; + kfree(buf); + + { Find the file entry } + dirBuf := readExtent(volume^.device, dirLBA, dirLen); + if dirBuf = nil then exit; + if not findEntry(dirBuf, dirLen, fileName, @fileLBA, @fileLen, @isDirFlag) then begin + kfree(dirBuf); + exit; + end; + kfree(dirBuf); + if isDirFlag <> 0 then exit; + + { Clamp to EOF } + if offset >= fileLen then exit; + remaining := fileLen - offset; + if byteCount < remaining then remaining := byteCount; + + { ISO9660 files are contiguous — straight LBA arithmetic } + secSize := volume^.device^.sectorSize; + if secSize = 0 then secSize := 2048; + + secBuf := puint32(kalloc(secSize)); + if secBuf = nil then exit; + + destPos := 0; + curLBA := fileLBA + (offset div secSize); + secOff := offset mod secSize; + + while remaining > 0 do begin + driver.storage.mgr.storage_read(volume^.device, curLBA, 1, secBuf); + + bytesToCopy := secSize - secOff; + if bytesToCopy > remaining then bytesToCopy := remaining; + core.util.memcpy(uint32(secBuf) + secOff, uint32(buffer) + destPos, bytesToCopy); + + destPos := destPos + bytesToCopy; + remaining := remaining - bytesToCopy; + secOff := 0; + curLBA := curLBA + 1; + end; + + kfree(secBuf); + readFileAtOffset := destPos; +end; + { ==================== Init ==================== } procedure init(); @@ -728,16 +742,15 @@ begin filesystem.sName := 'ISO9660'; filesystem.system_id := $05; filesystem.readDirCallback := @readDirectoryEntries; - filesystem.readCallback := @readFile; + filesystem.fileSizeCallback := @getFileSize; + filesystem.readOffsetCallback := @readFileAtOffset; filesystem.detectCallback := @detect_volumes; filesystem.identifyCallback := @identify_volume; - filesystem.writeCallback := nil; filesystem.createCallback := nil; filesystem.createDirCallback := nil; filesystem.deleteFileCallback := nil; filesystem.deleteDirCallback := nil; filesystem.renameFileCallback := nil; - filesystem.readAsyncCallback := nil; filesystem.readDirAsyncCallback := nil; driver.storage.fs.mgr.register_filesystem(@filesystem); diff --git a/src/driver/storage/vol/driver.storage.vol.mgr.pas b/src/driver/storage/vol/driver.storage.vol.mgr.pas index 1d3cc800..26471497 100644 --- a/src/driver/storage/vol/driver.storage.vol.mgr.pas +++ b/src/driver/storage/vol/driver.storage.vol.mgr.pas @@ -80,6 +80,9 @@ procedure delete_volume(volume : PStorage_Volume); implementation +uses + driver.storage.vfs; + { ===================== helpers ===================== } { Create a TStorage_Volume from a partition entry and register it } @@ -135,8 +138,10 @@ begin i := i + 1; end; - if found <> nil then + if found <> nil then begin + driver.storage.vfs.InvalidateVolume(found); kfree(puint32(found)); + end; end; { Remove all volumes belonging to a device } @@ -150,6 +155,7 @@ begin v := PStorage_Volume(Void(DL_Get(volumes, i))^); if v^.device = device then begin DL_Delete(volumes, i); + driver.storage.vfs.InvalidateVolume(v); kfree(puint32(v)); end else i := i + 1; -- 2.54.0 From 666a011a9f922635482bf28fcc18cdee86e3c201 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 15 Mar 2026 15:11:00 +0000 Subject: [PATCH 02/21] refactor fat32 --- src/app/app.iotest.pas | 30 +- .../storage/fs/driver.storage.fs.fat32.pas | 1438 ++++++++--------- 2 files changed, 737 insertions(+), 731 deletions(-) diff --git a/src/app/app.iotest.pas b/src/app/app.iotest.pas index b610baa0..ac9c858d 100644 --- a/src/app/app.iotest.pas +++ b/src/app/app.iotest.pas @@ -42,6 +42,20 @@ begin buf[i] := uint8((chunkIdx + i) and $FF); end; +{ Write throughput with auto-scaled units: KB/s or MB/s } +procedure writeThroughput(buf : POutBuf; kbps : uint32); +begin + if kbps >= 1024 then begin + io.stdio.bufWriteInt(buf, kbps div 1024); + io.stdio.bufWriteStr(buf, '.'); + io.stdio.bufWriteInt(buf, ((kbps mod 1024) * 10) div 1024); + io.stdio.bufWriteStr(buf, ' MB/s'); + end else begin + io.stdio.bufWriteInt(buf, kbps); + io.stdio.bufWriteStr(buf, ' KB/s'); + end; +end; + procedure run(Params : PParamList; stdin_buf, stdout_buf, stderr_buf : POutBuf); var path : pchar; @@ -109,8 +123,8 @@ begin io.stdio.bufWriteStr(stdout_buf, ' W '); io.stdio.bufWriteInt(stdout_buf, offset div 1024); io.stdio.bufWriteStr(stdout_buf, ' KB '); - io.stdio.bufWriteInt(stdout_buf, kbps); - io.stdio.bufWriteStrLn(stdout_buf, ' KB/s'); + writeThroughput(stdout_buf, kbps); + io.stdio.bufWriteNewLine(stdout_buf); tLast := t1; bytesInSec := 0; end; @@ -124,8 +138,8 @@ begin io.stdio.bufWriteStr(stdout_buf, 'Write complete: '); if elapsed > 0 then begin kbps := ((FILE_SIZE div 1024) * TICK_HZ) div elapsed; - io.stdio.bufWriteInt(stdout_buf, kbps); - io.stdio.bufWriteStr(stdout_buf, ' KB/s ('); + writeThroughput(stdout_buf, kbps); + io.stdio.bufWriteStr(stdout_buf, ' ('); io.stdio.bufWriteInt(stdout_buf, (elapsed * 1000) div TICK_HZ); io.stdio.bufWriteStrLn(stdout_buf, ' ms)'); end else @@ -193,8 +207,8 @@ begin io.stdio.bufWriteStr(stdout_buf, ' R '); io.stdio.bufWriteInt(stdout_buf, offset div 1024); io.stdio.bufWriteStr(stdout_buf, ' KB '); - io.stdio.bufWriteInt(stdout_buf, kbps); - io.stdio.bufWriteStrLn(stdout_buf, ' KB/s'); + writeThroughput(stdout_buf, kbps); + io.stdio.bufWriteNewLine(stdout_buf); tLast := t1; bytesInSec := 0; end; @@ -208,8 +222,8 @@ begin io.stdio.bufWriteStr(stdout_buf, 'Read+verify complete: '); if elapsed > 0 then begin kbps := ((FILE_SIZE div 1024) * TICK_HZ) div elapsed; - io.stdio.bufWriteInt(stdout_buf, kbps); - io.stdio.bufWriteStr(stdout_buf, ' KB/s ('); + writeThroughput(stdout_buf, kbps); + io.stdio.bufWriteStr(stdout_buf, ' ('); io.stdio.bufWriteInt(stdout_buf, (elapsed * 1000) div TICK_HZ); io.stdio.bufWriteStrLn(stdout_buf, ' ms)'); end else diff --git a/src/driver/storage/fs/driver.storage.fs.fat32.pas b/src/driver/storage/fs/driver.storage.fs.fat32.pas index beb91b98..a56ac295 100644 --- a/src/driver/storage/fs/driver.storage.fs.fat32.pas +++ b/src/driver/storage/fs/driver.storage.fs.fat32.pas @@ -107,17 +107,22 @@ type end; PFatVolumeInfo = ^TFatVolumeInfo; - { FAT sector cache — direct-mapped, 64 lines, 32 KB data } + { FAT sector cache — 2-way set-associative, 128 sets, LRU replacement } + FAT_CACHE_SETS = 128; + FAT_CACHE_WAYS = 2; + FAT_CACHE_LINES = 256; { SETS * WAYS } + TFATCacheLine = record SectorIdx : uint32; { which FAT sector is stored here } Dirty : boolean; { true if written but not flushed to disk } Valid : boolean; { true if line contains valid data } + LRU : uint8; { 0 = MRU, 1 = LRU (for 2-way) } end; PFATCache = ^TFATCache; TFATCache = record - Lines : array[0..63] of TFATCacheLine; { 64 cache line tags } - Data : puint32; { kalloc'd 64*512 = 32768 bytes of FAT sector data } + Lines : array[0..FAT_CACHE_LINES - 1] of TFATCacheLine; + Data : puint32; { kalloc'd 256*512 = 128 KB of FAT sector data } EvictBuf : puint32; { kalloc'd 512-byte scratch for eviction writes } Busy : boolean; { spinlock: serializes miss-path eviction+load } FatStart : uint32; { LBA of first FAT sector on disk } @@ -685,6 +690,337 @@ begin findFreeClusters:= clusters; end; +function compareByteArray8(str1 : byteArray8; str2 : byteArray8) : boolean; +var + i : uint32; +begin + push_trace('driver.storage.fs.fat32.compareArray'); + compareByteArray8:= true; + for i:=0 to 7 do begin + if str1[i] <> str2[i] then begin + compareByteArray8:= false; + break; + end; + end; +end; + +{ Extract the starting cluster number from a directory entry. } +function dirFirstCluster(dir : PDirectory) : uint32; +begin + dirFirstCluster := uint32(dir^.clusterLow) or uint32(dir^.clusterHigh shl 16); +end; + +{ Set the starting cluster number in a directory entry. } +procedure setDirFirstCluster(dir : PDirectory; cluster : uint32); +begin + dir^.clusterLow := uint16(cluster and $FFFF); + dir^.clusterHigh := uint16((cluster shr 16) and $FFFF); +end; + +{ Calculate the LBA of the first data sector on a FAT32 volume. } +function fatDataStartLBA(volume : PStorage_Volume; bootRecord : PBootRecord) : uint32; +begin + fatDataStartLBA := volume^.sectorStart + 1 + bootRecord^.rsvSectors + bootRecord^.FATSize; +end; + +{ Convert a cluster number to its starting LBA. } +function clusterToLBA(dataStart : uint32; spc : uint32; cluster : uint32) : uint32; +begin + clusterToLBA := dataStart + (cluster * spc); +end; + +{ Write an 8.3 extension into a TFATExtArray: uppercase, pad with spaces. } +procedure fillFatExt(var dest : TFATExtArray; extPart : pchar); +var + j : uint32; +begin + dest[0] := ' '; + dest[1] := ' '; + dest[2] := ' '; + if (extPart <> nil) and (extPart[0] <> char(0)) then begin + for j := 0 to 2 do begin + if extPart[j] = char(0) then break; + if (extPart[j] >= 'a') and (extPart[j] <= 'z') then + dest[j] := char(uint8(extPart[j]) - 32) + else + dest[j] := extPart[j]; + end; + end; +end; + +{ Search a single sector buffer for a directory entry matching name+ext. + Returns the index (0..entriesPerSec-1), or -1 if not found. + Sets hitEnd to true if a char(0) sentinel was reached (end of directory). } +function findEntryInSector(buffer : puint32; entriesPerSec : uint32; + name : byteArray8; ext : pchar; var hitEnd : boolean) : integer; +var + idx : uint32; + dir : PDirectory; +begin + findEntryInSector := -1; + hitEnd := false; + if entriesPerSec > 0 then + for idx := 0 to entriesPerSec - 1 do begin + dir := @PDirectory(buffer)[idx]; + if dir^.fileName[0] = char(0) then begin + hitEnd := true; + exit; + end; + if dir^.fileName[0] = char($E5) then continue; + if compareByteArray8(dir^.fileName, name) + and matchExtension(dir^.fileExtension, ext) then begin + findEntryInSector := integer(idx); + exit; + end; + end; +end; + +{ Search a single sector buffer for a free directory entry (char(0) or $E5). + Returns the index (0..entriesPerSec-1), or -1 if sector is full. } +function findFreeEntryInSector(buffer : puint32; entriesPerSec : uint32) : integer; +var + idx : uint32; + dir : PDirectory; +begin + findFreeEntryInSector := -1; + if entriesPerSec > 0 then + for idx := 0 to entriesPerSec - 1 do begin + dir := @PDirectory(buffer)[idx]; + if (dir^.fileName[0] = char(0)) or (dir^.fileName[0] = char($E5)) then begin + findFreeEntryInSector := integer(idx); + exit; + end; + end; +end; + +{ --- Higher-level helpers --- } + +type + TDirEntryLocation = record + Found : boolean; + SectorLBA : uint32; + EntryIdx : uint32; + end; + + TFileLookup = record + BootRecord : PBootRecord; + Dirs : PLinkedListBase; + Dir : PDirectory; + Exists : boolean; + Status : puint32; + end; + + TRunInfo = record + StartCluster : uint32; + LastCluster : uint32; + RunLen : uint32; + RunSectors : uint32; + RunBytes : uint32; + LBA : uint32; + end; + +{ Locate a named entry in the parent directory's raw sectors. + Walks the full FAT chain of parentCluster, scanning each sector + with findEntryInSector. On match, fills loc and leaves buffer + containing the matching sector (caller must kfree buffer). + Returns true if found. } +function locateDirEntry( + volume : PStorage_Volume; + parentCluster : uint32; + bootRecord : PBootRecord; + cleanName : byteArray8; + ext : pchar; + var loc : TDirEntryLocation; + buffer : puint32 +) : boolean; +var + dataStart : uint32; + spc : uint32; + entriesPerSector : uint32; + dirChain : PLinkedListBase; + dc : uint32; + ds : uint32; + curCluster : uint32; + clusterLBA : uint32; + hitEnd : boolean; + entryIdx : integer; + done : boolean; +begin + locateDirEntry := false; + loc.Found := false; + + dataStart := fatDataStartLBA(volume, bootRecord); + spc := uint32(bootRecord^.spc); + entriesPerSector := uint32(bootRecord^.sectorSize) div uint32(sizeof(TDirectory)); + dirChain := getFatChain(volume, parentCluster, bootRecord); + done := false; + + dc := 0; + while (dc < LL_size(dirChain)) and (not done) do begin + curCluster := puint32(LL_Get(dirChain, dc))^; + clusterLBA := clusterToLBA(dataStart, spc, curCluster); + ds := 0; + while (ds < spc) and (not done) do begin + driver.storage.mgr.storage_read(volume^.device, clusterLBA + ds, 1, buffer); + entryIdx := findEntryInSector(buffer, entriesPerSector, cleanName, ext, hitEnd); + if hitEnd then + done := true + else if entryIdx >= 0 then begin + loc.Found := true; + loc.SectorLBA := clusterLBA + ds; + loc.EntryIdx := uint32(entryIdx); + done := true; + end; + ds := ds + 1; + end; + dc := dc + 1; + end; + + LL_Free(dirChain); + locateDirEntry := loc.Found; +end; + +{ Locate an unused (free) directory entry slot in the parent directory. + Returns true if a free slot was found, filling loc and leaving buffer + containing the sector. } +function locateFreeDirEntry( + volume : PStorage_Volume; + parentCluster : uint32; + bootRecord : PBootRecord; + var loc : TDirEntryLocation; + buffer : puint32 +) : boolean; +var + dataStart : uint32; + spc : uint32; + entriesPerSector : uint32; + dirChain : PLinkedListBase; + dc : uint32; + ds : uint32; + curCluster : uint32; + clusterLBA : uint32; + entryIdx : integer; + found : boolean; +begin + locateFreeDirEntry := false; + loc.Found := false; + + dataStart := fatDataStartLBA(volume, bootRecord); + spc := uint32(bootRecord^.spc); + entriesPerSector := uint32(bootRecord^.sectorSize) div uint32(sizeof(TDirectory)); + dirChain := getFatChain(volume, parentCluster, bootRecord); + found := false; + + if LL_size(dirChain) > 0 then begin + for dc := 0 to LL_size(dirChain) - 1 do begin + curCluster := puint32(LL_Get(dirChain, dc))^; + clusterLBA := clusterToLBA(dataStart, spc, curCluster); + for ds := 0 to spc - 1 do begin + driver.storage.mgr.storage_read(volume^.device, clusterLBA + ds, 1, buffer); + entryIdx := findFreeEntryInSector(buffer, entriesPerSector); + if entryIdx >= 0 then begin + loc.Found := true; + loc.SectorLBA := clusterLBA + ds; + loc.EntryIdx := uint32(entryIdx); + found := true; + end; + if found then break; + end; + if found then break; + end; + end; + + LL_Free(dirChain); + locateFreeDirEntry := loc.Found; +end; + +{ Build a contiguous cluster run starting at startCluster. + Scans ahead via readFat, accumulating consecutive clusters up to + maxRunSectors worth of sectors. + Returns the number of clusters in the run (runLen). + Sets lastCluster to the last cluster in the run (= startCluster + runLen - 1). } +function buildContiguousRun( + volume : PStorage_Volume; + startCluster : uint32; + spc : uint32; + maxRunSectors : uint32; + bootRecord : PBootRecord; + var runLen : uint32; + var lastCluster : uint32 +) : uint32; +var + nextFat : uint32; +begin + runLen := 1; + lastCluster := startCluster; + + while (runLen * spc) < maxRunSectors do begin + nextFat := readFat(volume, lastCluster, bootRecord); + if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then + break; + if nextFat <> lastCluster + 1 then + break; + lastCluster := nextFat; + runLen := runLen + 1; + end; + + buildContiguousRun := runLen; +end; + +{ Update the SIOC chain-position cache. } +procedure updateSIOC( + volume : PStorage_Volume; + fileStartCluster : uint32; + chainIdx : uint32; + chainCluster : uint32 +); +begin + sioc_vol := volume; + sioc_fileClust := fileStartCluster; + sioc_chainIdx := chainIdx; + sioc_chainClust := chainCluster; + sioc_valid := true; +end; + +{ Seek to a specific cluster index in a file's FAT chain. + Uses the SIOC cache to resume from a previously cached position + when possible, otherwise walks from the file's start cluster. + Returns true on success, false if the chain ended prematurely. } +function seekToClusterIndex( + volume : PStorage_Volume; + fileStartCluster : uint32; + targetIdx : uint32; + bootRecord : PBootRecord; + var curCluster : uint32; + var chainPos : uint32 +) : boolean; +var + nextFat : uint32; +begin + seekToClusterIndex := false; + + { Resume from SIOC cache if it covers this file at a useful position } + if sioc_valid and (sioc_vol = volume) and (sioc_fileClust = fileStartCluster) + and (sioc_chainIdx <= targetIdx) then begin + curCluster := sioc_chainClust; + chainPos := sioc_chainIdx; + end else begin + curCluster := fileStartCluster; + chainPos := 0; + end; + + { Walk forward to targetIdx } + while chainPos < targetIdx do begin + nextFat := readFat(volume, curCluster, bootRecord); + if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then + exit; + curCluster := nextFat; + chainPos := chainPos + 1; + end; + + seekToClusterIndex := true; +end; + //TODO add optional attributes flag to refine what i return function getDirEntries(volume : PStorage_volume; cluster : uint32; bootRecord : PBootRecord) : PLinkedListBase; var @@ -706,7 +1042,7 @@ begin buffer:= puint32(kalloc( (bootRecord^.sectorSize * bootRecord^.spc) * LL_size(clusters) )); memset(uint32(buffer), 0, (bootRecord^.sectorSize * bootRecord^.spc) * LL_size(clusters) ); - dataStart:= volume^.sectorStart + 1 + bootRecord^.rsvSectors + bootRecord^.FATSize; + dataStart:= fatDataStartLBA(volume, bootRecord); push_trace('driver.storage.fs.fat32.getDirEntries.readSectors'); for i:=0 to LL_size(clusters) - 1 do begin @@ -737,20 +1073,6 @@ begin push_trace('driver.storage.fs.fat32.getDirEntries.exit'); end; -function compareByteArray8(str1 : byteArray8; str2 : byteArray8) : boolean; -var - i : uint32; -begin - push_trace('driver.storage.fs.fat32.compareArray'); - compareByteArray8:= true; - for i:=0 to 7 do begin - if str1[i] <> str2[i] then begin - compareByteArray8:= false; - break; - end; - end; -end; - function fat2GenericEntries(list : PLinkedListBase) : PLinkedListBase; var i : uint32; @@ -851,8 +1173,7 @@ begin dirEntry:= PDirectory(LL_Get(directories, ii)); if compareByteArray8( dirEntry^.fileName, cleanString( pchar(puint32(LL_Get(directoryStrings, i))^), status)) then begin - cluster:= uint32(dirEntry^.clusterLow); - cluster:= uint32(cluster) or uint32(dirEntry^.clusterHigh shl 16); + cluster:= dirFirstCluster(dirEntry); break; end; ii+=1; @@ -956,7 +1277,7 @@ begin kfree(void(extPart)); bootRecord:= readBootRecord(volume); - datastart:= volume^.sectorStart + 1 + bootRecord^.FATSize + bootRecord^.rsvSectors; + datastart:= fatDataStartLBA(volume, bootRecord); if status^ = ord(eNone) then begin push_trace('driver.storage.fs.fat32.writeDirectory.createEntry'); @@ -974,7 +1295,7 @@ begin end; parentDirectory:= PDirectory(LL_Get(directories, 0)); - parentCluster:= uint32(parentDirectory^.clusterlow) or uint32(parentDirectory^.clusterhigh shl 16); + parentCluster:= dirFirstCluster(parentDirectory); clusters:= findFreeClusters(volume, 1, bootRecord); if clusters = nil then begin @@ -998,17 +1319,15 @@ begin bufferPointer:= @PDirectory(buffer)[0]; bufferPointer^.fileName:= thisArray; //TODO implement time bufferPointer^.attributes:= attributes; - bufferPointer^.clusterLow:= cluster; - bufferPointer^.clusterHigh:= uint16((cluster shr 16) and $0000FFFF); + setDirFirstCluster(bufferPointer, cluster); bufferPointer:= @PDirectory(buffer)[1]; bufferPointer^.fileName:= parentArray; //TODO implement time bufferPointer^.attributes:= attributes; - bufferPointer^.clusterLow:= parentCluster; - bufferPointer^.clusterHigh:= uint16((parentCluster shr 16) and $0000FFFF); + setDirFirstCluster(bufferPointer, parentCluster); //write to disk - driver.storage.mgr.storage_write(volume^.device, dataStart + (cluster * bootRecord^.spc), 1, buffer); + driver.storage.mgr.storage_write(volume^.device, clusterToLBA(dataStart, bootRecord^.spc, cluster), 1, buffer); //write fat writeFat(volume, cluster, $FFFFFFF8, bootRecord); @@ -1029,27 +1348,15 @@ begin splitFileNameParts(dirName, namePart, extPart); bufferPointer^.fileName:= cleanString(namePart, status); bufferPointer^.attributes:= attributes; - { Always initialize extension to spaces } - bufferPointer^.fileExtension[0] := ' '; - bufferPointer^.fileExtension[1] := ' '; - bufferPointer^.fileExtension[2] := ' '; if attributes = 0 then begin { Copy extension characters for files } - if (extPart <> nil) and (extPart[0] <> char(0)) then begin - for i := 0 to 2 do begin - if extPart[i] = char(0) then break; - { Uppercase for FAT32 } - if (extPart[i] >= 'a') and (extPart[i] <= 'z') then - bufferPointer^.fileExtension[i] := char(uint8(extPart[i]) - 32) - else - bufferPointer^.fileExtension[i] := extPart[i]; - end; - end; + fillFatExt(bufferPointer^.fileExtension, extPart); + end else begin + fillFatExt(bufferPointer^.fileExtension, nil); end; kfree(void(namePart)); kfree(void(extPart)); - bufferPointer^.clusterLow:= cluster; - bufferPointer^.clusterHigh:= uint16((cluster shr 16) and $0000FFFF); + setDirFirstCluster(bufferPointer, cluster); writeDirectory:= cluster; @@ -1151,6 +1458,61 @@ begin push_trace('driver.storage.fs.fat32.freeFatChain.exit'); end; +{ Resolve a full path into parentCluster + leafName. + Splits the path, reads the parent directory, and returns + the parent cluster. On failure sets statusOut and returns false. + Caller must kfree parentDir and leafName when done. + bootRecord must already be read by the caller. } +function resolveParentCluster( + volume : PStorage_Volume; + path : pchar; + bootRecord : PBootRecord; + var parentDir : pchar; + var leafName : pchar; + var parentClust : uint32; + var statusOut : uint32 +) : boolean; +var + directories : PLinkedListBase; + parentEntry : PDirectory; + status : puint32; +begin + resolveParentCluster := false; + splitPathParts(path, parentDir, leafName); + + if (leafName = nil) or (stringSize(leafName) = 0) then begin + statusOut := ord(eInvalidFileName); + exit; + end; + + if (parentDir = nil) or (stringSize(parentDir) = 0) then begin + parentClust := bootRecord^.rootCluster; + resolveParentCluster := true; + exit; + end; + + status := puint32(kalloc(4)); + status^ := ord(eNone); + directories := readDirectory(volume, parentDir, status); + if status^ <> ord(eNone) then begin + statusOut := status^; + LL_Free(directories); + kfree(status); + exit; + end; + if LL_size(directories) < 1 then begin + statusOut := ord(eDirectoryDoesNotExist); + LL_Free(directories); + kfree(status); + exit; + end; + parentEntry := PDirectory(LL_Get(directories, 0)); + parentClust := dirFirstCluster(parentEntry); + LL_Free(directories); + kfree(status); + resolveParentCluster := true; +end; + { Delete a file from the volume. filePath is relative to volume root, e.g. 'SYSTEM/FILE.TXT' or 'FILE.TXT' } procedure deleteFile(volume : PStorage_Volume; filePath : pchar; statusOut : puint32); var @@ -1159,152 +1521,85 @@ var namePart : pchar; extPart : pchar; bootRecord : PBootRecord; - directories : PLinkedListBase; - parentEntry : PDirectory; parentCluster : uint32; status : puint32; - dataStart : uint32; - spc : uint32; buffer : puint32; - EntriesPerSector : uint32; dir : PDirectory; - found : boolean; - done : boolean; - entryIdx : uint32; cluster : uint32; - sectorOffset : uint32; cleanFileName : byteArray8; - dirChain : PLinkedListBase; - dc : uint32; - ds : uint32; - curCluster : uint32; - clusterLBA : uint32; + loc : TDirEntryLocation; + resolveStatus : uint32; begin push_trace('driver.storage.fs.fat32.deleteFile.enter'); io.syslog.logln('FAT32', 'deleteFile: enter'); status := puint32(kalloc(4)); status^ := ord(eNone); - { Split path into parent directory and filename } - splitPathParts(filePath, parentDir, fileName); + bootRecord := readBootRecord(volume); - if (fileName = nil) or (stringSize(fileName) = 0) then begin - io.syslog.logln('FAT32', 'deleteFile: invalid filename'); - if statusOut <> nil then statusOut^ := ord(eInvalidFileName); + { Resolve parent directory + leaf name from full path } + resolveStatus := ord(eNone); + if not resolveParentCluster(volume, filePath, bootRecord, parentDir, fileName, parentCluster, resolveStatus) then begin + io.syslog.logln('FAT32', 'deleteFile: resolve failed'); + if statusOut <> nil then statusOut^ := resolveStatus; kfree(status); + kfree(puint32(bootRecord)); if parentDir <> nil then kfree(void(parentDir)); if fileName <> nil then kfree(void(fileName)); exit; end; - bootRecord := readBootRecord(volume); - spc := bootRecord^.spc; - - { Determine parent cluster — root has no '.' entry } - if (parentDir = nil) or (stringSize(parentDir) = 0) then begin - parentCluster := bootRecord^.rootCluster; - end else begin - directories := readDirectory(volume, parentDir, status); - if status^ <> ord(eNone) then begin - io.syslog.logln('FAT32', 'deleteFile: parent directory error'); - if statusOut <> nil then statusOut^ := status^; - LL_Free(directories); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(fileName)); - exit; - end; - if LL_size(directories) < 1 then begin - io.syslog.logln('FAT32', 'deleteFile: parent directory empty/missing'); - if statusOut <> nil then statusOut^ := ord(eDirectoryDoesNotExist); - LL_Free(directories); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(fileName)); - exit; - end; - parentEntry := PDirectory(LL_Get(directories, 0)); - parentCluster := uint32(parentEntry^.clusterLow) or uint32(parentEntry^.clusterHigh shl 16); - LL_Free(directories); - end; - - { Split filename for 8.3 matching — compute cleanFileName once before loop } + { Split filename for 8.3 matching } splitFileNameParts(fileName, namePart, extPart); cleanFileName := cleanString(namePart, status); - { Walk the full parent directory FAT chain to find the target entry } - dataStart := volume^.sectorStart + 1 + bootRecord^.rsvSectors + bootRecord^.FATSize; - EntriesPerSector := uint32(bootRecord^.sectorSize) div uint32(sizeof(TDirectory)); + { Locate target entry in parent directory } buffer := puint32(kalloc(bootRecord^.sectorSize)); - dirChain := getFatChain(volume, parentCluster, bootRecord); - found := false; - done := false; - - dc := 0; - while (dc < LL_size(dirChain)) and (not done) do begin - curCluster := puint32(LL_Get(dirChain, dc))^; - clusterLBA := dataStart + (curCluster * spc); - ds := 0; - while (ds < spc) and (not done) do begin - driver.storage.mgr.storage_read(volume^.device, clusterLBA + ds, 1, buffer); - if EntriesPerSector > 0 then - for entryIdx := 0 to EntriesPerSector - 1 do begin - dir := @PDirectory(buffer)[entryIdx]; - if dir^.fileName[0] = char(0) then begin - done := true; - break; - end; - if dir^.fileName[0] = char($E5) then continue; - if compareByteArray8(dir^.fileName, cleanFileName) - and matchExtension(dir^.fileExtension, extPart) then begin - { Don't allow deleting directories via deleteFile } - if (dir^.attributes and $10) = $10 then begin - io.syslog.logln('FAT32', 'deleteFile: target is a directory, not a file'); - if statusOut <> nil then statusOut^ := ord(eNotADirectory); - kfree(buffer); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(fileName)); - kfree(void(namePart)); - kfree(void(extPart)); - LL_Free(dirChain); - exit; - end; - found := true; - done := true; - cluster := uint32(dir^.clusterLow) or uint32(dir^.clusterHigh shl 16); - - { Free the FAT chain for this file } - io.syslog.logln('FAT32', 'deleteFile: freeing FAT chain'); - if cluster >= 2 then - freeFatChain(volume, cluster, bootRecord); - - { Mark entry deleted and write back this one sector } - dir^.fileName[0] := char($E5); - driver.storage.mgr.storage_write(volume^.device, clusterLBA + ds, 1, buffer); - break; - end; - end; - ds := ds + 1; - end; - dc := dc + 1; + if not locateDirEntry(volume, parentCluster, bootRecord, cleanFileName, extPart, loc, buffer) then begin + io.syslog.logln('FAT32', 'deleteFile: file not found'); + if statusOut <> nil then statusOut^ := ord(eFileDoesNotExist); + kfree(buffer); + kfree(void(namePart)); + kfree(void(extPart)); + kfree(status); + kfree(puint32(bootRecord)); + kfree(void(parentDir)); + kfree(void(fileName)); + exit; end; - LL_Free(dirChain); + dir := @PDirectory(buffer)[loc.EntryIdx]; + + { Don't allow deleting directories via deleteFile } + if (dir^.attributes and $10) = $10 then begin + io.syslog.logln('FAT32', 'deleteFile: target is a directory, not a file'); + if statusOut <> nil then statusOut^ := ord(eNotADirectory); + kfree(buffer); + kfree(void(namePart)); + kfree(void(extPart)); + kfree(status); + kfree(puint32(bootRecord)); + kfree(void(parentDir)); + kfree(void(fileName)); + exit; + end; + + cluster := dirFirstCluster(dir); + + { Free the FAT chain for this file } + io.syslog.logln('FAT32', 'deleteFile: freeing FAT chain'); + if cluster >= 2 then + freeFatChain(volume, cluster, bootRecord); + + { Mark entry deleted and write back } + dir^.fileName[0] := char($E5); + driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, buffer); + + if statusOut <> nil then statusOut^ := ord(eNone); + kfree(buffer); kfree(void(namePart)); kfree(void(extPart)); - - if not found then begin - io.syslog.logln('FAT32', 'deleteFile: file not found'); - if statusOut <> nil then statusOut^ := ord(eFileDoesNotExist); - end else begin - if statusOut <> nil then statusOut^ := ord(eNone); - end; - kfree(status); kfree(puint32(bootRecord)); kfree(void(parentDir)); @@ -1321,169 +1616,103 @@ var namePart : pchar; extPart : pchar; bootRecord : PBootRecord; - directories : PLinkedListBase; childDirs : PLinkedListBase; - parentEntry : PDirectory; parentCluster : uint32; status : puint32; - dataStart : uint32; - spc : uint32; buffer : puint32; - EntriesPerSector : uint32; dir : PDirectory; - found : boolean; - done : boolean; - entryIdx : uint32; cluster : uint32; childCount : uint32; cleanFileName : byteArray8; - dirChain : PLinkedListBase; - dc : uint32; - ds : uint32; - curCluster : uint32; - clusterLBA : uint32; + loc : TDirEntryLocation; + resolveStatus : uint32; begin push_trace('driver.storage.fs.fat32.deleteDir.enter'); io.syslog.logln('FAT32', 'deleteDir: enter'); status := puint32(kalloc(4)); status^ := ord(eNone); - { Split path into parent directory and target dir name } - splitPathParts(path, parentDir, dirName); + bootRecord := readBootRecord(volume); - if (dirName = nil) or (stringSize(dirName) = 0) then begin - io.syslog.logln('FAT32', 'deleteDir: invalid directory name'); - if statusOut <> nil then statusOut^ := ord(eInvalidFileName); + { Resolve parent directory + leaf name from full path } + resolveStatus := ord(eNone); + if not resolveParentCluster(volume, path, bootRecord, parentDir, dirName, parentCluster, resolveStatus) then begin + io.syslog.logln('FAT32', 'deleteDir: resolve failed'); + if statusOut <> nil then statusOut^ := resolveStatus; kfree(status); + kfree(puint32(bootRecord)); if parentDir <> nil then kfree(void(parentDir)); if dirName <> nil then kfree(void(dirName)); exit; end; - bootRecord := readBootRecord(volume); - spc := bootRecord^.spc; - - { Determine parent cluster — root has no '.' entry } - if (parentDir = nil) or (stringSize(parentDir) = 0) then begin - parentCluster := bootRecord^.rootCluster; - end else begin - directories := readDirectory(volume, parentDir, status); - if status^ <> ord(eNone) then begin - if statusOut <> nil then statusOut^ := status^; - LL_Free(directories); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(dirName)); - exit; - end; - if LL_size(directories) < 1 then begin - if statusOut <> nil then statusOut^ := ord(eDirectoryDoesNotExist); - LL_Free(directories); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(dirName)); - exit; - end; - parentEntry := PDirectory(LL_Get(directories, 0)); - parentCluster := uint32(parentEntry^.clusterLow) or uint32(parentEntry^.clusterHigh shl 16); - LL_Free(directories); - end; - - { Split dirName for 8.3 matching — compute cleanFileName once before loop } + { Split dirName for 8.3 matching } splitFileNameParts(dirName, namePart, extPart); cleanFileName := cleanString(namePart, status); - { Walk the full parent directory FAT chain to find the target entry } - dataStart := volume^.sectorStart + 1 + bootRecord^.rsvSectors + bootRecord^.FATSize; - EntriesPerSector := uint32(bootRecord^.sectorSize) div uint32(sizeof(TDirectory)); + { Locate target entry in parent directory } buffer := puint32(kalloc(bootRecord^.sectorSize)); - dirChain := getFatChain(volume, parentCluster, bootRecord); - found := false; - done := false; - cluster := 0; - - dc := 0; - while (dc < LL_size(dirChain)) and (not done) do begin - curCluster := puint32(LL_Get(dirChain, dc))^; - clusterLBA := dataStart + (curCluster * spc); - ds := 0; - while (ds < spc) and (not done) do begin - driver.storage.mgr.storage_read(volume^.device, clusterLBA + ds, 1, buffer); - if EntriesPerSector > 0 then - for entryIdx := 0 to EntriesPerSector - 1 do begin - dir := @PDirectory(buffer)[entryIdx]; - if dir^.fileName[0] = char(0) then begin - done := true; - break; - end; - if dir^.fileName[0] = char($E5) then continue; - if compareByteArray8(dir^.fileName, cleanFileName) - and matchExtension(dir^.fileExtension, extPart) then begin - { Must be a directory } - if (dir^.attributes and $10) <> $10 then begin - if statusOut <> nil then statusOut^ := ord(eNotADirectory); - kfree(buffer); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(dirName)); - kfree(void(namePart)); - kfree(void(extPart)); - LL_Free(dirChain); - exit; - end; - found := true; - done := true; - cluster := uint32(dir^.clusterLow) or uint32(dir^.clusterHigh shl 16); - - { Check if directory is empty — should contain only '.' and '..' } - childDirs := getDirEntries(volume, cluster, bootRecord); - childCount := LL_size(childDirs); - LL_Free(childDirs); - - if childCount > 2 then begin - io.syslog.logln('FAT32', 'deleteDir: directory not empty, refusing to delete'); - if statusOut <> nil then statusOut^ := ord(eDirectoryNotEmpty); - kfree(buffer); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(dirName)); - kfree(void(namePart)); - kfree(void(extPart)); - LL_Free(dirChain); - exit; - end; - - { Free the FAT chain for this directory's data } - if cluster >= 2 then - freeFatChain(volume, cluster, bootRecord); - - { Mark entry deleted and write back this one sector } - dir^.fileName[0] := char($E5); - driver.storage.mgr.storage_write(volume^.device, clusterLBA + ds, 1, buffer); - break; - end; - end; - ds := ds + 1; - end; - dc := dc + 1; + if not locateDirEntry(volume, parentCluster, bootRecord, cleanFileName, extPart, loc, buffer) then begin + io.syslog.logln('FAT32', 'deleteDir: directory not found'); + if statusOut <> nil then statusOut^ := ord(eDirectoryDoesNotExist); + kfree(buffer); + kfree(void(namePart)); + kfree(void(extPart)); + kfree(status); + kfree(puint32(bootRecord)); + kfree(void(parentDir)); + kfree(void(dirName)); + exit; end; - LL_Free(dirChain); + dir := @PDirectory(buffer)[loc.EntryIdx]; + + { Must be a directory } + if (dir^.attributes and $10) <> $10 then begin + if statusOut <> nil then statusOut^ := ord(eNotADirectory); + kfree(buffer); + kfree(void(namePart)); + kfree(void(extPart)); + kfree(status); + kfree(puint32(bootRecord)); + kfree(void(parentDir)); + kfree(void(dirName)); + exit; + end; + + cluster := dirFirstCluster(dir); + + { Check if directory is empty — should contain only '.' and '..' } + childDirs := getDirEntries(volume, cluster, bootRecord); + childCount := LL_size(childDirs); + LL_Free(childDirs); + + if childCount > 2 then begin + io.syslog.logln('FAT32', 'deleteDir: directory not empty, refusing to delete'); + if statusOut <> nil then statusOut^ := ord(eDirectoryNotEmpty); + kfree(buffer); + kfree(void(namePart)); + kfree(void(extPart)); + kfree(status); + kfree(puint32(bootRecord)); + kfree(void(parentDir)); + kfree(void(dirName)); + exit; + end; + + { Free the FAT chain for this directory's data } + if cluster >= 2 then + freeFatChain(volume, cluster, bootRecord); + + { Mark entry deleted and write back } + dir^.fileName[0] := char($E5); + driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, buffer); + + if statusOut <> nil then statusOut^ := ord(eNone); + kfree(buffer); kfree(void(namePart)); kfree(void(extPart)); - - if not found then begin - io.syslog.logln('FAT32', 'deleteDir: directory not found'); - if statusOut <> nil then statusOut^ := ord(eDirectoryDoesNotExist); - end else begin - if statusOut <> nil then statusOut^ := ord(eNone); - end; - kfree(status); kfree(puint32(bootRecord)); kfree(void(parentDir)); @@ -1504,157 +1733,82 @@ var newNamePart : pchar; newExtPart : pchar; bootRecord : PBootRecord; - directories : PLinkedListBase; - parentEntry : PDirectory; parentCluster : uint32; - found : boolean; j : uint32; status : puint32; - dataStart : uint32; - spc : uint32; buffer : puint32; - EntriesPerSector: uint32; dir : PDirectory; cleanName : byteArray8; newCleanName : byteArray8; - done : boolean; - dirChain : PLinkedListBase; - dc : uint32; - ds : uint32; - entryIdx : uint32; - curCluster : uint32; - clusterLBA : uint32; + loc : TDirEntryLocation; + resolveStatus : uint32; begin push_trace('driver.storage.fs.fat32.renameFile.enter'); io.syslog.logln('FAT32', 'renameFile: enter'); status := puint32(kalloc(4)); status^ := ord(eNone); - { Split path into parent directory and current filename } - splitPathParts(filePath, parentDir, fileName); + bootRecord := readBootRecord(volume); - if (fileName = nil) or (stringSize(fileName) = 0) then begin - io.syslog.logln('FAT32', 'renameFile: invalid filename'); + { Validate new name before doing any work } + if (newName = nil) or (stringSize(newName) = 0) then begin + io.syslog.logln('FAT32', 'renameFile: invalid new name'); if statusOut <> nil then statusOut^ := ord(eInvalidFileName); kfree(status); + kfree(puint32(bootRecord)); + exit; + end; + + { Resolve parent directory + leaf name from full path } + resolveStatus := ord(eNone); + if not resolveParentCluster(volume, filePath, bootRecord, parentDir, fileName, parentCluster, resolveStatus) then begin + io.syslog.logln('FAT32', 'renameFile: resolve failed'); + if statusOut <> nil then statusOut^ := resolveStatus; + kfree(status); + kfree(puint32(bootRecord)); if parentDir <> nil then kfree(void(parentDir)); if fileName <> nil then kfree(void(fileName)); exit; end; - if (newName = nil) or (stringSize(newName) = 0) then begin - io.syslog.logln('FAT32', 'renameFile: invalid new name'); - if statusOut <> nil then statusOut^ := ord(eInvalidFileName); + { Split current filename for 8.3 matching } + splitFileNameParts(fileName, namePart, extPart); + cleanName := cleanString(namePart, status); + + { Locate target entry in parent directory } + buffer := puint32(kalloc(bootRecord^.sectorSize)); + if not locateDirEntry(volume, parentCluster, bootRecord, cleanName, extPart, loc, buffer) then begin + io.syslog.logln('FAT32', 'renameFile: file not found'); + if statusOut <> nil then statusOut^ := ord(eFileDoesNotExist); + kfree(buffer); + kfree(void(namePart)); + kfree(void(extPart)); kfree(status); + kfree(puint32(bootRecord)); kfree(void(parentDir)); kfree(void(fileName)); exit; end; - bootRecord := readBootRecord(volume); - spc := bootRecord^.spc; + dir := @PDirectory(buffer)[loc.EntryIdx]; - { Determine parent cluster — root has no '.' entry } - if (parentDir = nil) or (stringSize(parentDir) = 0) then begin - parentCluster := bootRecord^.rootCluster; - end else begin - directories := readDirectory(volume, parentDir, status); - if status^ <> ord(eNone) then begin - io.syslog.logln('FAT32', 'renameFile: parent directory error'); - if statusOut <> nil then statusOut^ := status^; - LL_Free(directories); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(fileName)); - exit; - end; - if LL_size(directories) < 1 then begin - io.syslog.logln('FAT32', 'renameFile: parent directory empty'); - if statusOut <> nil then statusOut^ := ord(eDirectoryDoesNotExist); - LL_Free(directories); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(fileName)); - exit; - end; - parentEntry := PDirectory(LL_Get(directories, 0)); - parentCluster := uint32(parentEntry^.clusterLow) or uint32(parentEntry^.clusterHigh shl 16); - LL_Free(directories); - end; + { Apply new 8.3 name in-place } + splitFileNameParts(newName, newNamePart, newExtPart); + newCleanName := cleanString(newNamePart, status); + for j := 0 to 7 do + dir^.fileName[j] := newCleanName[j]; + fillFatExt(dir^.fileExtension, newExtPart); + kfree(void(newNamePart)); + kfree(void(newExtPart)); - { Split current filename for 8.3 matching — compute once before loop } - splitFileNameParts(fileName, namePart, extPart); - cleanName := cleanString(namePart, status); + { Write back this one sector } + driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, buffer); - { Walk the full parent directory FAT chain to find the target entry } - dataStart := volume^.sectorStart + 1 + bootRecord^.rsvSectors + bootRecord^.FATSize; - EntriesPerSector := uint32(bootRecord^.sectorSize) div uint32(sizeof(TDirectory)); - buffer := puint32(kalloc(bootRecord^.sectorSize)); - dirChain := getFatChain(volume, parentCluster, bootRecord); - found := false; - done := false; + if statusOut <> nil then statusOut^ := ord(eNone); - dc := 0; - while (dc < LL_size(dirChain)) and (not done) do begin - curCluster := puint32(LL_Get(dirChain, dc))^; - clusterLBA := dataStart + (curCluster * spc); - ds := 0; - while (ds < spc) and (not done) do begin - driver.storage.mgr.storage_read(volume^.device, clusterLBA + ds, 1, buffer); - if EntriesPerSector > 0 then - for entryIdx := 0 to EntriesPerSector - 1 do begin - dir := @PDirectory(buffer)[entryIdx]; - if dir^.fileName[0] = char(0) then begin - done := true; - break; - end; - if dir^.fileName[0] = char($E5) then continue; - if compareByteArray8(dir^.fileName, cleanName) - and matchExtension(dir^.fileExtension, extPart) then begin - found := true; - done := true; - - { Apply new 8.3 name in-place } - splitFileNameParts(newName, newNamePart, newExtPart); - newCleanName := cleanString(newNamePart, status); - for j := 0 to 7 do - dir^.fileName[j] := newCleanName[j]; - for j := 0 to 2 do begin - if (newExtPart <> nil) and (j < stringSize(newExtPart)) then begin - if (newExtPart[j] >= 'a') and (newExtPart[j] <= 'z') then - dir^.fileExtension[j] := char(uint8(newExtPart[j]) - 32) - else - dir^.fileExtension[j] := newExtPart[j]; - end else - dir^.fileExtension[j] := ' '; - end; - kfree(void(newNamePart)); - kfree(void(newExtPart)); - - { Write back this one sector } - driver.storage.mgr.storage_write(volume^.device, clusterLBA + ds, 1, buffer); - break; - end; - end; - ds := ds + 1; - end; - dc := dc + 1; - end; - - LL_Free(dirChain); kfree(buffer); kfree(void(namePart)); kfree(void(extPart)); - - if not found then begin - io.syslog.logln('FAT32', 'renameFile: file not found'); - if statusOut <> nil then statusOut^ := ord(eFileDoesNotExist); - end else begin - if statusOut <> nil then statusOut^ := ord(eNone); - end; - kfree(status); kfree(puint32(bootRecord)); kfree(void(parentDir)); @@ -1783,7 +1937,7 @@ begin PDirectory(ctx^.Buffer)[2].attributes := $10; PDirectory(ctx^.Buffer)[2].clusterLow := 3; driver.storage.mgr.storage_write_async(ctx^.Disk, - ctx^.DataStart + (ctx^.SPC * ctx^.RootCluster), 1, ctx^.Buffer, + clusterToLBA(ctx^.DataStart, ctx^.SPC, ctx^.RootCluster), 1, ctx^.Buffer, @fmt_step_complete, pointer(ctx)); end; fmtSystemDir: begin @@ -1811,7 +1965,7 @@ begin PDirectory(ctx^.Buffer)[1].attributes := $10; PDirectory(ctx^.Buffer)[1].clusterLow := ctx^.RootCluster; driver.storage.mgr.storage_write_async(ctx^.Disk, - ctx^.DataStart + (ctx^.SPC * 3), 1, ctx^.Buffer, + clusterToLBA(ctx^.DataStart, ctx^.SPC, 3), 1, ctx^.Buffer, @fmt_step_complete, pointer(ctx)); end; fmtDone: begin @@ -2096,7 +2250,7 @@ begin PDirectory(buffer)[1].attributes := $10; PDirectory(buffer)[1].clusterLow := rootCluster; - driver.storage.mgr.storage_write(disk, dataStart + (spc * rootCluster), 1, buffer); + driver.storage.mgr.storage_write(disk, clusterToLBA(dataStart, spc, rootCluster), 1, buffer); io.syslog.logln('FAT32', 'create_volume (sync): root dir written'); memset(uint32(buffer), 0, disk^.sectorsize); @@ -2235,12 +2389,9 @@ function writeFileAtOffset(volume : PStorage_Volume; directory : pchar; const MAX_RUN_SECTORS = 2048; { cap per multi-sector write = 1MB } var - bootRecord : PBootRecord; - dirs : PLinkedListBase; - dir : PDirectory; - statusOut : puint32; + lookup : TFileLookup; + run : TRunInfo; i : uint32; - exists : boolean; cluster : uint32; dataStart : uint32; cleanFileName : byteArray8; @@ -2252,15 +2403,10 @@ var bytesPerCluster : uint32; startClusterIdx : uint32; inClusterOffset : uint32; - startSecInClust : uint32; - secByteOff : uint32; chainPos : uint32; curCluster : uint32; - clusterLBA : uint32; - sectorIdx : uint32; srcPos : uint32; remaining : uint32; - bytesToCopy : uint32; fileSize : uint32; newEnd : uint32; oldClusters : uint32; @@ -2272,23 +2418,13 @@ var nextFat : uint32; { dir entry update — raw sector scan (Lesson 18) } dirCluster : uint32; - dirChain : PLinkedListBase; dirBuf : puint32; - entriesPerSec : uint32; - entryIdx : uint32; rawDir : PDirectory; - foundRaw : boolean; - dc : uint32; - ds : uint32; + loc : TDirEntryLocation; newDirEntry : TDirectory; { Multi-sector write variables } spc : uint32; secSize : uint32; - runStartClust : uint32; - runLen : uint32; - runSectors : uint32; - runBytes : uint32; - runLBA : uint32; skipBytes : uint32; copyBytes : uint32; firstSectorOff : uint32; @@ -2300,24 +2436,24 @@ var begin push_trace('driver.storage.fs.fat32.writeFileAtOffset.enter'); writeFileAtOffset := 0; - exists := false; - statusOut := puint32(kalloc(sizeof(uint32))); - statusOut^ := 0; - bootRecord := readBootRecord(volume); - dirs := readDirectory(volume, directory, statusOut); - dataStart := volume^.sectorStart + 1 + bootRecord^.FATSize + bootRecord^.rsvSectors; + lookup.Status := puint32(kalloc(sizeof(uint32))); + lookup.Status^ := 0; + lookup.BootRecord := readBootRecord(volume); + lookup.Dirs := readDirectory(volume, directory, lookup.Status); + lookup.Exists := false; + dataStart := fatDataStartLBA(volume, lookup.BootRecord); splitFileNameParts(fileName, namePart, extPart); - if LL_size(dirs) > 0 then begin - cleanFileName := cleanString(namePart, statusOut); - for i := 0 to LL_Size(dirs) - 1 do begin - tempdir := PDirectory(LL_get(dirs, i)); - otherCFN := cleanString(tempdir^.filename, statusOut); + if LL_size(lookup.Dirs) > 0 then begin + cleanFileName := cleanString(namePart, lookup.Status); + for i := 0 to LL_Size(lookup.Dirs) - 1 do begin + tempdir := PDirectory(LL_get(lookup.Dirs, i)); + otherCFN := cleanString(tempdir^.filename, lookup.Status); if compareByteArray8(cleanFileName, otherCFN) and matchExtension(tempdir^.fileExtension, extPart) then begin - dir := tempdir; - exists := true; + lookup.Dir := tempdir; + lookup.Exists := true; end; end; end; @@ -2325,85 +2461,52 @@ begin kfree(void(namePart)); kfree(void(extPart)); - if not exists then begin + if not lookup.Exists then begin { --- Create a new zero-length file entry on disk --- } { Determine parent cluster (Lesson 18: root has no '.' entry) } if (directory = nil) or (stringSize(directory) = 0) then - dirCluster := bootRecord^.rootCluster + dirCluster := lookup.BootRecord^.rootCluster else begin - if LL_size(dirs) > 0 then begin - tempdir := PDirectory(LL_get(dirs, 0)); + if LL_size(lookup.Dirs) > 0 then begin + tempdir := PDirectory(LL_get(lookup.Dirs, 0)); if tempdir^.fileName[0] = '.' then - dirCluster := uint32(tempdir^.clusterLow) - or uint32(tempdir^.clusterHigh shl 16) + dirCluster := dirFirstCluster(tempdir) else - dirCluster := bootRecord^.rootCluster; + dirCluster := lookup.BootRecord^.rootCluster; end else - dirCluster := bootRecord^.rootCluster; + dirCluster := lookup.BootRecord^.rootCluster; end; - entriesPerSec := bootRecord^.sectorSize div uint32(sizeof(TDirectory)); - dirChain := getFatChain(volume, dirCluster, bootRecord); - dirBuf := puint32(kalloc(bootRecord^.sectorSize)); - foundRaw := false; + dirBuf := puint32(kalloc(lookup.BootRecord^.sectorSize)); splitFileNameParts(fileName, namePart, extPart); - cleanFileName := cleanString(namePart, statusOut); + cleanFileName := cleanString(namePart, lookup.Status); - if LL_size(dirChain) > 0 then begin - for dc := 0 to LL_size(dirChain) - 1 do begin - curCluster := puint32(LL_Get(dirChain, dc))^; - clusterLBA := dataStart + (curCluster * uint32(bootRecord^.spc)); - for ds := 0 to uint32(bootRecord^.spc) - 1 do begin - driver.storage.mgr.storage_read(volume^.device, clusterLBA + ds, 1, dirBuf); - if entriesPerSec > 0 then begin - for entryIdx := 0 to entriesPerSec - 1 do begin - rawDir := @PDirectory(dirBuf)[entryIdx]; - if (rawDir^.fileName[0] = char(0)) or (rawDir^.fileName[0] = char($E5)) then begin - { Found a free slot — write the new file entry } - memset(uint32(rawDir), 0, sizeof(TDirectory)); - rawDir^.fileName := cleanFileName; - rawDir^.attributes := 0; { regular file } - rawDir^.fileExtension[0] := ' '; - rawDir^.fileExtension[1] := ' '; - rawDir^.fileExtension[2] := ' '; - if (extPart <> nil) and (extPart[0] <> char(0)) then begin - for i := 0 to 2 do begin - if extPart[i] = char(0) then break; - if (extPart[i] >= 'a') and (extPart[i] <= 'z') then - rawDir^.fileExtension[i] := char(uint8(extPart[i]) - 32) - else - rawDir^.fileExtension[i] := extPart[i]; - end; - end; - rawDir^.clusterLow := 0; - rawDir^.clusterHigh := 0; - rawDir^.byteSize := 0; - driver.storage.mgr.storage_write(volume^.device, clusterLBA + ds, 1, dirBuf); - foundRaw := true; - break; - end; - end; - end; - if foundRaw then break; - end; - if foundRaw then break; - end; + if not locateFreeDirEntry(volume, dirCluster, lookup.BootRecord, loc, dirBuf) then begin + kfree(void(namePart)); + kfree(void(extPart)); + kfree(dirBuf); + io.syslog.logln('FAT32', 'writeFileAtOffset: exit-A no free dir slot'); + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); + exit; end; + rawDir := @PDirectory(dirBuf)[loc.EntryIdx]; + memset(uint32(rawDir), 0, sizeof(TDirectory)); + rawDir^.fileName := cleanFileName; + rawDir^.attributes := 0; { regular file } + fillFatExt(rawDir^.fileExtension, extPart); + rawDir^.clusterLow := 0; + rawDir^.clusterHigh := 0; + rawDir^.byteSize := 0; + driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, dirBuf); + kfree(void(namePart)); kfree(void(extPart)); kfree(dirBuf); - LL_Free(dirChain); - - if not foundRaw then begin - io.syslog.logln('FAT32', 'writeFileAtOffset: exit-A no free dir slot'); - LL_Free(dirs); - kfree(puint32(bootRecord)); - kfree(puint32(statusOut)); - exit; - end; { Invalidate chain cache — new file } sioc_valid := false; @@ -2413,13 +2516,13 @@ begin newDirEntry.byteSize := 0; newDirEntry.clusterLow := 0; newDirEntry.clusterHigh := 0; - dir := @newDirEntry; + lookup.Dir := @newDirEntry; end; - fileSize := dir^.byteSize; - cluster := uint32(dir^.clusterlow) or uint32(dir^.clusterhigh shl 16); + fileSize := lookup.Dir^.byteSize; + cluster := dirFirstCluster(lookup.Dir); newEnd := offset + byteCount; - bytesPerCluster := uint32(bootRecord^.spc) * uint32(bootRecord^.sectorSize); + bytesPerCluster := uint32(lookup.BootRecord^.spc) * uint32(lookup.BootRecord^.sectorSize); { ---- Extend file if writing past EOF ---- } if newEnd > fileSize then begin @@ -2431,12 +2534,12 @@ begin if needClusters > oldClusters then begin extraClusters := needClusters - oldClusters; - newClusts := findFreeClusters(volume, extraClusters, bootRecord); + newClusts := findFreeClusters(volume, extraClusters, lookup.BootRecord); if newClusts = nil then begin io.syslog.logln('FAT32', 'writeFileAtOffset: exit-B disk full'); - LL_Free(dirs); - kfree(puint32(bootRecord)); - kfree(puint32(statusOut)); + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); exit; end; @@ -2446,10 +2549,10 @@ begin lastCluster := sioc_chainClust else lastCluster := cluster; - nextFat := readFat(volume, lastCluster, bootRecord); + nextFat := readFat(volume, lastCluster, lookup.BootRecord); while ((nextFat and $0FFFFFFF) < $0FFFFFF8) and (nextFat <> 0) do begin lastCluster := nextFat; - nextFat := readFat(volume, lastCluster, bootRecord); + nextFat := readFat(volume, lastCluster, lookup.BootRecord); end; end else begin { Zero-length file: first new cluster becomes start cluster } @@ -2461,25 +2564,20 @@ begin for i := 0 to extraClusters - 1 do begin nextCluster := puint32(LL_Get(newClusts, i))^; if lastCluster <> 0 then - writeFat(volume, lastCluster, nextCluster, bootRecord); + writeFat(volume, lastCluster, nextCluster, lookup.BootRecord); lastCluster := nextCluster; end; - writeFat(volume, lastCluster, $FFFFFFF8, bootRecord); + writeFat(volume, lastCluster, $FFFFFFF8, lookup.BootRecord); end; { If file was zero-length, update dir entry's cluster pointer } if oldClusters = 0 then begin cluster := puint32(LL_Get(newClusts, 0))^; - dir^.clusterlow := uint16(cluster and $FFFF); - dir^.clusterhigh := uint16((cluster shr 16) and $FFFF); + setDirFirstCluster(lookup.Dir, cluster); end; { Update chain cache: last new cluster is the new tail } - sioc_vol := volume; - sioc_fileClust := cluster; - sioc_chainIdx := needClusters - 1; - sioc_chainClust := lastCluster; - sioc_valid := true; + updateSIOC(volume, cluster, needClusters - 1, lastCluster); LL_Free(newClusts); @@ -2492,68 +2590,36 @@ begin startClusterIdx := offset div bytesPerCluster; inClusterOffset := offset mod bytesPerCluster; - { Determine curCluster at startClusterIdx using cache } - if sioc_valid and (sioc_vol = volume) and (sioc_fileClust = cluster) - and (sioc_chainIdx <= startClusterIdx) then begin - curCluster := sioc_chainClust; - chainPos := sioc_chainIdx; - end else begin - curCluster := cluster; - chainPos := 0; + if not seekToClusterIndex(volume, cluster, startClusterIdx, lookup.BootRecord, curCluster, chainPos) then begin + io.syslog.logln('FAT32', 'writeFileAtOffset: exit-C chain broke seeking to start'); + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); + exit; end; - { Walk forward from cached/start position to startClusterIdx } - while chainPos < startClusterIdx do begin - nextFat := readFat(volume, curCluster, bootRecord); - if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then begin - io.syslog.log('FAT32', 'writeFileAtOffset: exit-C chain broke at chainPos='); - io.syslog.writeint(chainPos); - io.syslog.writestring(' target='); - io.syslog.writeint(startClusterIdx); - io.syslog.writestring(' cluster='); - io.syslog.writeint(curCluster); - io.syslog.writestring(' fat='); - io.syslog.writehexln(nextFat); - LL_Free(dirs); - kfree(puint32(bootRecord)); - kfree(puint32(statusOut)); - exit; - end; - curCluster := nextFat; - chainPos := chainPos + 1; - end; - - spc := uint32(bootRecord^.spc); - secSize := uint32(bootRecord^.sectorSize); + spc := uint32(lookup.BootRecord^.spc); + secSize := uint32(lookup.BootRecord^.sectorSize); ioBuf := puint32(kalloc(secSize)); remaining := byteCount; srcPos := 0; while (remaining > 0) do begin { Build contiguous cluster run starting at curCluster } - runStartClust := curCluster; - runLen := 1; + run.StartCluster := curCluster; + buildContiguousRun(volume, curCluster, spc, MAX_RUN_SECTORS, lookup.BootRecord, run.RunLen, run.LastCluster); + curCluster := run.LastCluster; - while (runLen * spc) < MAX_RUN_SECTORS do begin - nextFat := readFat(volume, curCluster, bootRecord); - if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then - break; - if nextFat <> curCluster + 1 then - break; - curCluster := nextFat; - runLen := runLen + 1; - end; - - runSectors := runLen * spc; - runBytes := runSectors * secSize; - runLBA := dataStart + (runStartClust * spc); + run.RunSectors := run.RunLen * spc; + run.RunBytes := run.RunSectors * secSize; + run.LBA := clusterToLBA(dataStart, spc, run.StartCluster); if srcPos = 0 then skipBytes := inClusterOffset else skipBytes := 0; - copyBytes := runBytes - skipBytes; + copyBytes := run.RunBytes - skipBytes; if copyBytes > remaining then copyBytes := remaining; @@ -2561,7 +2627,7 @@ begin { Phase 1: Partial first sector — read-modify-write } if firstSectorOff > 0 then begin - sectorLBA := runLBA + (skipBytes div secSize); + sectorLBA := run.LBA + (skipBytes div secSize); partialBytes := secSize - firstSectorOff; if partialBytes > copyBytes then partialBytes := copyBytes; @@ -2577,7 +2643,7 @@ begin { Phase 2: Full sectors — batch write directly from caller buffer } fullSectors := copyBytes div secSize; if fullSectors > 0 then begin - sectorLBA := runLBA + (skipBytes div secSize); + sectorLBA := run.LBA + (skipBytes div secSize); batchBytes := fullSectors * secSize; driver.storage.mgr.storage_write(volume^.device, sectorLBA, fullSectors, puint32(uint32(buffer) + srcPos)); @@ -2589,7 +2655,7 @@ begin { Phase 3: Partial last sector — read-modify-write } if copyBytes > 0 then begin - sectorLBA := runLBA + (skipBytes div secSize); + sectorLBA := run.LBA + (skipBytes div secSize); driver.storage.mgr.storage_read(volume^.device, sectorLBA, 1, ioBuf); core.util.memcpy(uint32(buffer) + srcPos, uint32(ioBuf), copyBytes); driver.storage.mgr.storage_write(volume^.device, sectorLBA, 1, ioBuf); @@ -2598,15 +2664,11 @@ begin end; { Update chain cache to last cluster in this run } - sioc_vol := volume; - sioc_fileClust := cluster; - sioc_chainIdx := chainPos + runLen - 1; - sioc_chainClust := curCluster; - sioc_valid := true; - chainPos := chainPos + runLen; + updateSIOC(volume, cluster, chainPos + run.RunLen - 1, curCluster); + chainPos := chainPos + run.RunLen; if remaining > 0 then begin - nextFat := readFat(volume, curCluster, bootRecord); + nextFat := readFat(volume, curCluster, lookup.BootRecord); if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then begin io.syslog.log('FAT32', 'writeFileAtOffset: exit-D chain broke in write loop, srcPos='); io.syslog.writeint(srcPos); @@ -2631,71 +2693,43 @@ begin { ---- Update directory entry byteSize if file was extended ---- } { Only commit actual bytes written — never advance size past what was really written (Lesson 22) } - if (srcPos > 0) and ((offset + srcPos) > dir^.byteSize) then begin + if (srcPos > 0) and ((offset + srcPos) > lookup.Dir^.byteSize) then begin { Lesson 18: search raw directory sectors, don't use LL indices. } if (directory = nil) or (stringSize(directory) = 0) then - dirCluster := bootRecord^.rootCluster + dirCluster := lookup.BootRecord^.rootCluster else begin - dirCluster := bootRecord^.rootCluster; - if LL_size(dirs) > 0 then begin - tempdir := PDirectory(LL_get(dirs, 0)); + dirCluster := lookup.BootRecord^.rootCluster; + if LL_size(lookup.Dirs) > 0 then begin + tempdir := PDirectory(LL_get(lookup.Dirs, 0)); if tempdir^.fileName[0] = '.' then - dirCluster := uint32(tempdir^.clusterLow) - or uint32(tempdir^.clusterHigh shl 16); + dirCluster := dirFirstCluster(tempdir); end; end; - dirChain := getFatChain(volume, dirCluster, bootRecord); - entriesPerSec := bootRecord^.sectorSize div uint32(sizeof(TDirectory)); - dirBuf := puint32(kalloc(bootRecord^.sectorSize)); - foundRaw := false; + dirBuf := puint32(kalloc(lookup.BootRecord^.sectorSize)); splitFileNameParts(fileName, namePart, extPart); - cleanFileName := cleanString(namePart, statusOut); + cleanFileName := cleanString(namePart, lookup.Status); - if LL_size(dirChain) > 0 then begin - for dc := 0 to LL_size(dirChain) - 1 do begin - curCluster := puint32(LL_Get(dirChain, dc))^; - clusterLBA := dataStart + (curCluster * uint32(bootRecord^.spc)); - for ds := 0 to uint32(bootRecord^.spc) - 1 do begin - driver.storage.mgr.storage_read(volume^.device, clusterLBA + ds, 1, dirBuf); - if entriesPerSec > 0 then begin - for entryIdx := 0 to entriesPerSec - 1 do begin - rawDir := @PDirectory(dirBuf)[entryIdx]; - if rawDir^.fileName[0] = char(0) then begin - foundRaw := true; - break; - end; - if rawDir^.fileName[0] = char($E5) then continue; - otherCFN := cleanString(rawDir^.filename, statusOut); - if compareByteArray8(cleanFileName, otherCFN) and matchExtension(rawDir^.fileExtension, extPart) then begin - rawDir^.byteSize := offset + srcPos; - { If file was zero-length, update cluster pointers too } - if dir^.byteSize = 0 then begin - rawDir^.clusterlow := dir^.clusterlow; - rawDir^.clusterhigh := dir^.clusterhigh; - end; - driver.storage.mgr.storage_write(volume^.device, clusterLBA + ds, 1, dirBuf); - foundRaw := true; - break; - end; - end; - end; - if foundRaw then break; - end; - if foundRaw then break; + if locateDirEntry(volume, dirCluster, lookup.BootRecord, cleanFileName, extPart, loc, dirBuf) then begin + rawDir := @PDirectory(dirBuf)[loc.EntryIdx]; + rawDir^.byteSize := offset + srcPos; + { If file was zero-length, update cluster pointers too } + if lookup.Dir^.byteSize = 0 then begin + rawDir^.clusterlow := lookup.Dir^.clusterlow; + rawDir^.clusterhigh := lookup.Dir^.clusterhigh; end; + driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, dirBuf); end; kfree(void(namePart)); kfree(void(extPart)); kfree(dirBuf); - LL_Free(dirChain); end; - LL_Free(dirs); - kfree(puint32(bootRecord)); - kfree(puint32(statusOut)); + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); writeFileAtOffset := srcPos; push_trace('driver.storage.fs.fat32.writeFileAtOffset.exit'); end; @@ -2759,12 +2793,9 @@ function readFileAtOffset(volume : PStorage_Volume; directory : pchar; fileName const MAX_RUN_SECTORS = 2048; { cap per multi-sector read = 1 MB } var - bootRecord : PBootRecord; - dirs : PLinkedListBase; - dir : PDirectory; - statusOut : puint32; + lookup : TFileLookup; + run : TRunInfo; i : uint32; - exists : boolean; cluster : uint32; dataStart : uint32; cleanFileName : byteArray8; @@ -2784,40 +2815,32 @@ var spc : uint32; secSize : uint32; { Multi-sector run variables } - runStartClust : uint32; - runLen : uint32; { clusters in current contiguous run } - runSectors : uint32; - runBytes : uint32; runBuf : puint32; - runBufBytes : uint32; skipBytes : uint32; { bytes to skip at start of run buffer } copyBytes : uint32; chunkSectors : uint32; chunkBytes : uint32; - runLBA : uint32; - ioBuf : puint32; - partialBytes : uint32; begin push_trace('driver.storage.fs.fat32.readFileAtOffset.enter'); readFileAtOffset := 0; - exists := false; - statusOut := puint32(kalloc(sizeof(uint32))); - statusOut^ := 0; - bootRecord := readBootRecord(volume); - dirs := readDirectory(volume, directory, statusOut); - dataStart := volume^.sectorStart + 1 + bootRecord^.FATSize + bootRecord^.rsvSectors; + lookup.Status := puint32(kalloc(sizeof(uint32))); + lookup.Status^ := 0; + lookup.BootRecord := readBootRecord(volume); + lookup.Dirs := readDirectory(volume, directory, lookup.Status); + lookup.Exists := false; + dataStart := fatDataStartLBA(volume, lookup.BootRecord); splitFileNameParts(fileName, namePart, extPart); - if LL_size(dirs) > 0 then begin - cleanFileName := cleanString(namePart, statusOut); - for i := 0 to LL_Size(dirs) - 1 do begin - tempdir := PDirectory(LL_get(dirs, i)); - otherCFN := cleanString(tempdir^.filename, statusOut); + if LL_size(lookup.Dirs) > 0 then begin + cleanFileName := cleanString(namePart, lookup.Status); + for i := 0 to LL_Size(lookup.Dirs) - 1 do begin + tempdir := PDirectory(LL_get(lookup.Dirs, i)); + otherCFN := cleanString(tempdir^.filename, lookup.Status); if compareByteArray8(cleanFileName, otherCFN) and matchExtension(tempdir^.fileExtension, extPart) then begin - dir := tempdir; - exists := true; + lookup.Dir := tempdir; + lookup.Exists := true; end; end; end; @@ -2825,21 +2848,21 @@ begin kfree(void(namePart)); kfree(void(extPart)); - if not exists then begin - LL_Free(dirs); - kfree(puint32(bootRecord)); - kfree(puint32(statusOut)); + if not lookup.Exists then begin + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); push_trace('driver.storage.fs.fat32.readFileAtOffset.notFound'); exit; end; - fileSize := dir^.byteSize; - cluster := uint32(dir^.clusterlow) or uint32(dir^.clusterhigh shl 16); + fileSize := lookup.Dir^.byteSize; + cluster := dirFirstCluster(lookup.Dir); if offset >= fileSize then begin - LL_Free(dirs); - kfree(puint32(bootRecord)); - kfree(puint32(statusOut)); + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); exit; end; @@ -2848,55 +2871,30 @@ begin if offset + remaining > fileSize then remaining := fileSize - offset; - spc := uint32(bootRecord^.spc); - secSize := uint32(bootRecord^.sectorSize); + spc := uint32(lookup.BootRecord^.spc); + secSize := uint32(lookup.BootRecord^.sectorSize); bytesPerCluster := spc * secSize; startClusterIdx := offset div bytesPerCluster; inClusterOffset := offset mod bytesPerCluster; { Determine curCluster at startClusterIdx using SIOC cache } - if sioc_valid and (sioc_vol = volume) and (sioc_fileClust = cluster) - and (sioc_chainIdx <= startClusterIdx) then begin - curCluster := sioc_chainClust; - chainPos := sioc_chainIdx; - end else begin - curCluster := cluster; - chainPos := 0; - end; - - { Walk forward to startClusterIdx } - while chainPos < startClusterIdx do begin - nextFat := readFat(volume, curCluster, bootRecord); - if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then begin - LL_Free(dirs); - kfree(puint32(bootRecord)); - kfree(puint32(statusOut)); - exit; - end; - curCluster := nextFat; - chainPos := chainPos + 1; + if not seekToClusterIndex(volume, cluster, startClusterIdx, lookup.BootRecord, curCluster, chainPos) then begin + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); + exit; end; destPos := 0; while (remaining > 0) do begin { Build a contiguous cluster run starting at curCluster } - runStartClust := curCluster; - runLen := 1; + run.StartCluster := curCluster; + buildContiguousRun(volume, curCluster, spc, MAX_RUN_SECTORS, lookup.BootRecord, run.RunLen, curCluster); - { Scan ahead for contiguous clusters, capped by MAX_RUN_SECTORS worth } - while (runLen * spc) < MAX_RUN_SECTORS do begin - nextFat := readFat(volume, curCluster, bootRecord); - if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then - break; - if nextFat <> curCluster + 1 then - break; - curCluster := nextFat; - runLen := runLen + 1; - end; - - runSectors := runLen * spc; - runBytes := runSectors * secSize; + run.RunSectors := run.RunLen * spc; + run.RunBytes := run.RunSectors * secSize; + run.LBA := clusterToLBA(dataStart, spc, run.StartCluster); { Calculate how many bytes from this run we actually need } if destPos = 0 then @@ -2904,23 +2902,21 @@ begin else skipBytes := 0; - copyBytes := runBytes - skipBytes; + copyBytes := run.RunBytes - skipBytes; if copyBytes > remaining then copyBytes := remaining; { Determine if we can read directly into caller's buffer (aligned, full run) } - if (skipBytes = 0) and (copyBytes >= runBytes) then begin + if (skipBytes = 0) and (copyBytes >= run.RunBytes) then begin { Direct read into output buffer — no temp allocation } - runLBA := dataStart + (runStartClust * spc); - driver.storage.mgr.storage_read(volume^.device, runLBA, runSectors, + driver.storage.mgr.storage_read(volume^.device, run.LBA, run.RunSectors, puint32(uint32(buffer) + destPos)); destPos := destPos + copyBytes; remaining := remaining - copyBytes; end else if (skipBytes = 0) and ((copyBytes mod secSize) = 0) then begin { Partial run but sector-aligned — read only needed sectors } chunkSectors := copyBytes div secSize; - runLBA := dataStart + (runStartClust * spc); - driver.storage.mgr.storage_read(volume^.device, runLBA, chunkSectors, + driver.storage.mgr.storage_read(volume^.device, run.LBA, chunkSectors, puint32(uint32(buffer) + destPos)); destPos := destPos + copyBytes; remaining := remaining - copyBytes; @@ -2928,14 +2924,14 @@ begin { Unaligned — need temp buffer for partial first/last sectors } { Only allocate for sectors we actually touch } chunkSectors := (skipBytes + copyBytes + secSize - 1) div secSize; - if chunkSectors > runSectors then chunkSectors := runSectors; + if chunkSectors > run.RunSectors then chunkSectors := run.RunSectors; chunkBytes := chunkSectors * secSize; runBuf := puint32(kalloc(chunkBytes)); - runLBA := dataStart + (runStartClust * spc) + (skipBytes div secSize); + run.LBA := clusterToLBA(dataStart, spc, run.StartCluster) + (skipBytes div secSize); { Adjust skipBytes for sectors we're skipping entirely } skipBytes := skipBytes mod secSize; - driver.storage.mgr.storage_read(volume^.device, runLBA, chunkSectors, runBuf); + driver.storage.mgr.storage_read(volume^.device, run.LBA, chunkSectors, runBuf); core.util.memcpy(uint32(runBuf) + skipBytes, uint32(buffer) + destPos, copyBytes); kfree(runBuf); destPos := destPos + copyBytes; @@ -2943,25 +2939,21 @@ begin end; { Update SIOC chain cache to last cluster in this run } - sioc_vol := volume; - sioc_fileClust := cluster; - sioc_chainIdx := chainPos + runLen - 1; - sioc_chainClust := curCluster; - sioc_valid := true; - chainPos := chainPos + runLen; + updateSIOC(volume, cluster, chainPos + run.RunLen - 1, curCluster); + chainPos := chainPos + run.RunLen; { Advance to next cluster beyond this run } if remaining > 0 then begin - nextFat := readFat(volume, curCluster, bootRecord); + nextFat := readFat(volume, curCluster, lookup.BootRecord); if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then break; curCluster := nextFat; end; end; - LL_Free(dirs); - kfree(puint32(bootRecord)); - kfree(puint32(statusOut)); + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); readFileAtOffset := destPos; push_trace('driver.storage.fs.fat32.readFileAtOffset.exit'); end; -- 2.54.0 From 48106179221daf6c196dd9807cdff54aa222e806 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 15 Mar 2026 19:46:31 +0000 Subject: [PATCH 03/21] FAT improvements and switch to 2-way associative cache --- .../storage/fs/driver.storage.fs.fat32.pas | 588 +++++++++++++----- 1 file changed, 419 insertions(+), 169 deletions(-) diff --git a/src/driver/storage/fs/driver.storage.fs.fat32.pas b/src/driver/storage/fs/driver.storage.fs.fat32.pas index a56ac295..50d75947 100644 --- a/src/driver/storage/fs/driver.storage.fs.fat32.pas +++ b/src/driver/storage/fs/driver.storage.fs.fat32.pas @@ -107,16 +107,18 @@ type end; PFatVolumeInfo = ^TFatVolumeInfo; +const { FAT sector cache — 2-way set-associative, 128 sets, LRU replacement } FAT_CACHE_SETS = 128; FAT_CACHE_WAYS = 2; FAT_CACHE_LINES = 256; { SETS * WAYS } +type TFATCacheLine = record SectorIdx : uint32; { which FAT sector is stored here } Dirty : boolean; { true if written but not flushed to disk } Valid : boolean; { true if line contains valid data } - LRU : uint8; { 0 = MRU, 1 = LRU (for 2-way) } + LRU : uint8; { 0 = MRU, 1 = LRU, 2 = LRU (for 3-way), 3 = LRU (for 4-way) } end; PFATCache = ^TFATCache; @@ -159,6 +161,21 @@ type CallbackData : pointer; end; + { Per-file cached metadata created by fat32OpenFile and stored in the + file descriptor's FSPrivate pointer. Avoids re-reading boot record, + directory listing, and searching for the file on every read/write. } + PFATOpenFile = ^TFATOpenFile; + TFATOpenFile = record + BootRecord : PBootRecord; { borrows from TFATCache — do NOT kfree } + DataStart : uint32; { fatDataStartLBA result } + FirstCluster : uint32; { file's start cluster (0 if new/empty) } + ByteSize : uint32; { current file size in bytes } + DirCluster : uint32; { parent directory's start cluster } + CleanName : byteArray8; { cleaned 8.3 filename } + ExtPart : pchar; { heap-allocated extension string — owned } + Exists : boolean; { true if file existed at open time } + end; + var filesystem : TFilesystem; @@ -373,14 +390,15 @@ begin end; cache := PFATCache(kalloc(sizeof(TFATCache))); memset(uint32(cache), 0, sizeof(TFATCache)); - cache^.Data := puint32(kalloc(64 * 512)); - memset(uint32(cache^.Data), 0, 64 * 512); + cache^.Data := puint32(kalloc(FAT_CACHE_LINES * 512)); + memset(uint32(cache^.Data), 0, FAT_CACHE_LINES * 512); cache^.EvictBuf := puint32(kalloc(512)); memset(uint32(cache^.EvictBuf), 0, 512); cache^.Busy := false; - for i := 0 to 63 do begin + for i := 0 to FAT_CACHE_LINES - 1 do begin cache^.Lines[i].Valid := false; cache^.Lines[i].Dirty := false; + cache^.Lines[i].LRU := uint8(i mod FAT_CACHE_WAYS); end; br := readBootRecord(volume); cache^.BootRecord := br; @@ -410,7 +428,7 @@ begin asm hlt end; end; - for i := 0 to 63 do begin + for i := 0 to FAT_CACHE_LINES - 1 do begin asm pushf; cli end; needFlush := cache^.Lines[i].Valid and cache^.Lines[i].Dirty; if needFlush then begin @@ -437,38 +455,55 @@ procedure fatCacheInvalidate(cache : PFATCache); var i : uint32; begin - for i := 0 to 63 do begin + for i := 0 to FAT_CACHE_LINES - 1 do begin cache^.Lines[i].Valid := false; cache^.Lines[i].Dirty := false; + cache^.Lines[i].LRU := uint8(i mod FAT_CACHE_WAYS); end; end; +{ Return the per-volume cached boot record without any allocation. + The FAT cache lazily reads and keeps a single copy; callers MUST NOT kfree it. } +function getCachedBootRecord(volume : PStorage_Volume) : PBootRecord; +begin + getCachedBootRecord := fatCacheGet(volume)^.BootRecord; +end; + { Read a single FAT entry using the sector cache. - 128 FAT entries per 512-byte sector. Direct-mapped: slot = fatSector mod 64. + 128 FAT entries per 512-byte sector. 2-way set-associative: set = fatSector mod 128. CLI/STI protects cache metadata; disk I/O runs with interrupts enabled. } function readFat(volume : PStorage_volume; cluster : uint32; bootRecord : PBootRecord) : uint32; var cache : PFATCache; fatSecIdx : uint32; { which FAT sector this cluster lives in } - slot : uint32; { cache line index } + setIdx : uint32; { cache set index } + baseSlot : uint32; { first line index of this set } entryOff : uint32; { offset within that sector (0..127) } - dataPtr : puint32; { pointer into cache data for this slot } + dataPtr : puint32; { pointer into cache data for the hit/victim slot } lba : uint32; doEvict : boolean; evictLBA : uint32; + w : uint32; { way iterator } + victim : uint32; { chosen victim slot index } begin cache := fatCacheGet(volume); fatSecIdx := cluster div 128; - slot := fatSecIdx and 63; { mod 64 } + setIdx := fatSecIdx mod FAT_CACHE_SETS; + baseSlot := setIdx * FAT_CACHE_WAYS; entryOff := cluster mod 128; - dataPtr := puint32(uint32(cache^.Data) + (slot * 512)); - { Fast path: cache hit under CLI — no lock needed } + { Fast path: cache hit under CLI — check both ways } asm pushf; cli end; - if cache^.Lines[slot].Valid and (cache^.Lines[slot].SectorIdx = fatSecIdx) then begin - readFat := dataPtr[entryOff] and $0FFFFFFF; - asm popf end; - exit; + for w := 0 to FAT_CACHE_WAYS - 1 do begin + if cache^.Lines[baseSlot + w].Valid and (cache^.Lines[baseSlot + w].SectorIdx = fatSecIdx) then begin + dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); + readFat := dataPtr[entryOff] and $0FFFFFFF; + { Update LRU: this way is MRU } + cache^.Lines[baseSlot + w].LRU := 0; + cache^.Lines[baseSlot + (1 - w)].LRU := 1; + asm popf end; + exit; + end; end; asm popf end; @@ -476,19 +511,35 @@ begin while true do begin asm pushf; cli end; if not cache^.Busy then begin - { Re-check for hit — another process may have loaded this slot } - if cache^.Lines[slot].Valid and (cache^.Lines[slot].SectorIdx = fatSecIdx) then begin - readFat := dataPtr[entryOff] and $0FFFFFFF; - asm popf end; - exit; + { Re-check for hit — another process may have loaded this sector } + for w := 0 to FAT_CACHE_WAYS - 1 do begin + if cache^.Lines[baseSlot + w].Valid and (cache^.Lines[baseSlot + w].SectorIdx = fatSecIdx) then begin + dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); + readFat := dataPtr[entryOff] and $0FFFFFFF; + cache^.Lines[baseSlot + w].LRU := 0; + cache^.Lines[baseSlot + (1 - w)].LRU := 1; + asm popf end; + exit; + end; end; cache^.Busy := true; - doEvict := cache^.Lines[slot].Valid and cache^.Lines[slot].Dirty; + { Pick victim: prefer invalid way, else LRU way } + victim := baseSlot; + if (not cache^.Lines[baseSlot].Valid) then + victim := baseSlot + else if (not cache^.Lines[baseSlot + 1].Valid) then + victim := baseSlot + 1 + else if cache^.Lines[baseSlot + 1].LRU >= cache^.Lines[baseSlot].LRU then + victim := baseSlot + 1 + else + victim := baseSlot; + dataPtr := puint32(uint32(cache^.Data) + (victim * 512)); + doEvict := cache^.Lines[victim].Valid and cache^.Lines[victim].Dirty; if doEvict then begin - evictLBA := cache^.FatStart + cache^.Lines[slot].SectorIdx; + evictLBA := cache^.FatStart + cache^.Lines[victim].SectorIdx; core.util.memcpy(uint32(dataPtr), uint32(cache^.EvictBuf), 512); end; - cache^.Lines[slot].Valid := false; + cache^.Lines[victim].Valid := false; asm popf end; break; end; @@ -502,45 +553,61 @@ begin lba := cache^.FatStart + fatSecIdx; driver.storage.mgr.storage_read(cache^.Device, lba, 1, dataPtr); - { Update cache metadata and release lock under CLI } + { Update cache metadata, set LRU, release lock under CLI } asm pushf; cli end; - cache^.Lines[slot].SectorIdx := fatSecIdx; - cache^.Lines[slot].Valid := true; - cache^.Lines[slot].Dirty := false; + cache^.Lines[victim].SectorIdx := fatSecIdx; + cache^.Lines[victim].Valid := true; + cache^.Lines[victim].Dirty := false; + { New entry is MRU, the other way is LRU } + cache^.Lines[victim].LRU := 0; + if victim = baseSlot then + cache^.Lines[baseSlot + 1].LRU := 1 + else + cache^.Lines[baseSlot].LRU := 1; readFat := dataPtr[entryOff] and $0FFFFFFF; cache^.Busy := false; asm popf end; end; { Write a single FAT entry into cache. No immediate disk I/O — call fatCacheFlush later. + 2-way set-associative: set = fatSector mod 128. CLI/STI protects cache metadata; disk I/O runs with interrupts enabled. } procedure writeFat(volume : PStorage_volume; cluster : uint32; value : uint32; bootRecord : PBootRecord); var cache : PFATCache; fatSecIdx : uint32; - slot : uint32; + setIdx : uint32; + baseSlot : uint32; entryOff : uint32; dataPtr : puint32; lba : uint32; doEvict : boolean; evictLBA : uint32; + w : uint32; + victim : uint32; begin cache := fatCacheGet(volume); fatSecIdx := cluster div 128; - slot := fatSecIdx and 63; + setIdx := fatSecIdx mod FAT_CACHE_SETS; + baseSlot := setIdx * FAT_CACHE_WAYS; entryOff := cluster mod 128; - dataPtr := puint32(uint32(cache^.Data) + (slot * 512)); - { Fast path: cache hit — no lock needed } + { Fast path: cache hit — check both ways } asm pushf; cli end; - if cache^.Lines[slot].Valid and (cache^.Lines[slot].SectorIdx = fatSecIdx) then begin - if value = 0 then - dataPtr[entryOff] := 0 - else - dataPtr[entryOff] := (dataPtr[entryOff] and $F0000000) or (value and $0FFFFFFF); - cache^.Lines[slot].Dirty := true; - asm popf end; - exit; + for w := 0 to FAT_CACHE_WAYS - 1 do begin + if cache^.Lines[baseSlot + w].Valid and (cache^.Lines[baseSlot + w].SectorIdx = fatSecIdx) then begin + dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); + if value = 0 then + dataPtr[entryOff] := 0 + else + dataPtr[entryOff] := (dataPtr[entryOff] and $F0000000) or (value and $0FFFFFFF); + cache^.Lines[baseSlot + w].Dirty := true; + { Update LRU: this way is MRU } + cache^.Lines[baseSlot + w].LRU := 0; + cache^.Lines[baseSlot + (1 - w)].LRU := 1; + asm popf end; + exit; + end; end; asm popf end; @@ -549,22 +616,38 @@ begin asm pushf; cli end; if not cache^.Busy then begin { Re-check for hit after acquiring lock } - if cache^.Lines[slot].Valid and (cache^.Lines[slot].SectorIdx = fatSecIdx) then begin - if value = 0 then - dataPtr[entryOff] := 0 - else - dataPtr[entryOff] := (dataPtr[entryOff] and $F0000000) or (value and $0FFFFFFF); - cache^.Lines[slot].Dirty := true; - asm popf end; - exit; + for w := 0 to FAT_CACHE_WAYS - 1 do begin + if cache^.Lines[baseSlot + w].Valid and (cache^.Lines[baseSlot + w].SectorIdx = fatSecIdx) then begin + dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); + if value = 0 then + dataPtr[entryOff] := 0 + else + dataPtr[entryOff] := (dataPtr[entryOff] and $F0000000) or (value and $0FFFFFFF); + cache^.Lines[baseSlot + w].Dirty := true; + cache^.Lines[baseSlot + w].LRU := 0; + cache^.Lines[baseSlot + (1 - w)].LRU := 1; + asm popf end; + exit; + end; end; cache^.Busy := true; - doEvict := cache^.Lines[slot].Valid and cache^.Lines[slot].Dirty; + { Pick victim: prefer invalid way, else LRU way } + victim := baseSlot; + if (not cache^.Lines[baseSlot].Valid) then + victim := baseSlot + else if (not cache^.Lines[baseSlot + 1].Valid) then + victim := baseSlot + 1 + else if cache^.Lines[baseSlot + 1].LRU >= cache^.Lines[baseSlot].LRU then + victim := baseSlot + 1 + else + victim := baseSlot; + dataPtr := puint32(uint32(cache^.Data) + (victim * 512)); + doEvict := cache^.Lines[victim].Valid and cache^.Lines[victim].Dirty; if doEvict then begin - evictLBA := cache^.FatStart + cache^.Lines[slot].SectorIdx; + evictLBA := cache^.FatStart + cache^.Lines[victim].SectorIdx; core.util.memcpy(uint32(dataPtr), uint32(cache^.EvictBuf), 512); end; - cache^.Lines[slot].Valid := false; + cache^.Lines[victim].Valid := false; asm popf end; break; end; @@ -578,16 +661,21 @@ begin lba := cache^.FatStart + fatSecIdx; driver.storage.mgr.storage_read(cache^.Device, lba, 1, dataPtr); - { Update cache, write value, release lock under CLI } + { Update cache, write value, set LRU, release lock under CLI } asm pushf; cli end; - cache^.Lines[slot].SectorIdx := fatSecIdx; - cache^.Lines[slot].Valid := true; - cache^.Lines[slot].Dirty := false; + cache^.Lines[victim].SectorIdx := fatSecIdx; + cache^.Lines[victim].Valid := true; + cache^.Lines[victim].Dirty := false; if value = 0 then dataPtr[entryOff] := 0 else dataPtr[entryOff] := (dataPtr[entryOff] and $F0000000) or (value and $0FFFFFFF); - cache^.Lines[slot].Dirty := true; + cache^.Lines[victim].Dirty := true; + cache^.Lines[victim].LRU := 0; + if victim = baseSlot then + cache^.Lines[baseSlot + 1].LRU := 1 + else + cache^.Lines[baseSlot].LRU := 1; cache^.Busy := false; asm popf end; end; @@ -1038,6 +1126,12 @@ begin directories:= LL_New(sizeof(TDirectory)); clusters:= PLinkedListBase(getFatChain(volume, cluster, bootRecord)); + if (clusters = nil) or (LL_size(clusters) = 0) then begin + getDirEntries := directories; + if clusters <> nil then LL_Free(clusters); + push_trace('driver.storage.fs.fat32.getDirEntries.exit'); + exit; + end; push_trace('driver.storage.fs.fat32.getDirEntries.allocBuffer'); buffer:= puint32(kalloc( (bootRecord^.sectorSize * bootRecord^.spc) * LL_size(clusters) )); memset(uint32(buffer), 0, (bootRecord^.sectorSize * bootRecord^.spc) * LL_size(clusters) ); @@ -2380,17 +2474,110 @@ begin kfree(buffer); end; +{ ---- Per-file open / close for VFS metadata caching ---- } + +{ Called by VFS on OpenFile. Reads the boot record (cached per-volume), + scans the directory for the file, and returns a TFATOpenFile context + that writeFileAtOffset / readFileAtOffset can reuse on every call. + Also populates fileSize so the VFS can skip calling fileSizeCallback. } +function fat32OpenFile(volume : PStorage_Volume; directory : pchar; + fileName : pchar; var fileSize : uint32) : pointer; +var + ofi : PFATOpenFile; + bootRecord : PBootRecord; + dirs : PLinkedListBase; + status : puint32; + tempdir : PDirectory; + i : uint32; + cleanFileName : byteArray8; + otherCFN : byteArray8; + namePart : pchar; + extPart : pchar; +begin + push_trace('driver.storage.fs.fat32.openFile.enter'); + fileSize := 0; + + bootRecord := getCachedBootRecord(volume); + + ofi := PFATOpenFile(kalloc(SizeOf(TFATOpenFile))); + memset(uint32(ofi), 0, SizeOf(TFATOpenFile)); + ofi^.BootRecord := bootRecord; + ofi^.DataStart := fatDataStartLBA(volume, bootRecord); + ofi^.Exists := false; + + { Read directory entries } + status := puint32(kalloc(sizeof(uint32))); + status^ := 0; + dirs := readDirectory(volume, directory, status); + + { Determine parent cluster (Lesson 18: root has no '.' entry) } + if (directory = nil) or (stringSize(directory) = 0) then + ofi^.DirCluster := bootRecord^.rootCluster + else begin + if (dirs <> nil) and (LL_size(dirs) > 0) then begin + tempdir := PDirectory(LL_get(dirs, 0)); + if tempdir^.fileName[0] = '.' then + ofi^.DirCluster := dirFirstCluster(tempdir) + else + ofi^.DirCluster := bootRecord^.rootCluster; + end else + ofi^.DirCluster := bootRecord^.rootCluster; + end; + + { Parse file name and compute cleaned 8.3 name } + splitFileNameParts(fileName, namePart, extPart); + cleanFileName := cleanString(namePart, status); + ofi^.CleanName := cleanFileName; + ofi^.ExtPart := stringCopy(extPart); + + { Search for file in directory entries } + if (dirs <> nil) and (LL_size(dirs) > 0) then begin + for i := 0 to LL_Size(dirs) - 1 do begin + tempdir := PDirectory(LL_get(dirs, i)); + otherCFN := cleanString(tempdir^.filename, status); + if compareByteArray8(cleanFileName, otherCFN) and matchExtension(tempdir^.fileExtension, extPart) then begin + ofi^.FirstCluster := dirFirstCluster(tempdir); + ofi^.ByteSize := tempdir^.byteSize; + ofi^.Exists := true; + end; + end; + end; + + kfree(void(namePart)); + kfree(void(extPart)); + if dirs <> nil then LL_Free(dirs); + kfree(puint32(status)); + + fileSize := ofi^.ByteSize; + fat32OpenFile := pointer(ofi); + push_trace('driver.storage.fs.fat32.openFile.exit'); +end; + +{ Called by VFS on CloseFile to free the per-file context. } +procedure fat32CloseFile(ctx : pointer); +var + ofi : PFATOpenFile; +begin + if ctx = nil then exit; + ofi := PFATOpenFile(ctx); + if ofi^.ExtPart <> nil then + kfree(void(ofi^.ExtPart)); + kfree(puint32(ofi)); +end; + { Write byteCount bytes to a file beginning at byte offset. If offset + byteCount exceeds the current file size, the file is extended by allocating new clusters. Returns the number of bytes actually written. } function writeFileAtOffset(volume : PStorage_Volume; directory : pchar; fileName : pchar; offset : uint32; - buffer : puint32; byteCount : uint32) : uint32; + buffer : puint32; byteCount : uint32; + ctx : pointer) : uint32; const MAX_RUN_SECTORS = 2048; { cap per multi-sector write = 1MB } var lookup : TFileLookup; run : TRunInfo; + ofi : PFATOpenFile; i : uint32; cluster : uint32; dataStart : uint32; @@ -2408,6 +2595,7 @@ var srcPos : uint32; remaining : uint32; fileSize : uint32; + origByteSize : uint32; newEnd : uint32; oldClusters : uint32; needClusters : uint32; @@ -2421,7 +2609,6 @@ var dirBuf : puint32; rawDir : PDirectory; loc : TDirEntryLocation; - newDirEntry : TDirectory; { Multi-sector write variables } spc : uint32; secSize : uint32; @@ -2436,33 +2623,49 @@ var begin push_trace('driver.storage.fs.fat32.writeFileAtOffset.enter'); writeFileAtOffset := 0; + ofi := PFATOpenFile(ctx); - lookup.Status := puint32(kalloc(sizeof(uint32))); - lookup.Status^ := 0; - lookup.BootRecord := readBootRecord(volume); - lookup.Dirs := readDirectory(volume, directory, lookup.Status); - lookup.Exists := false; - dataStart := fatDataStartLBA(volume, lookup.BootRecord); + if ofi <> nil then begin + { Fast path: use cached open-file metadata } + lookup.BootRecord := ofi^.BootRecord; + lookup.Dirs := nil; + lookup.Dir := nil; + lookup.Status := nil; + lookup.Exists := ofi^.Exists; + dataStart := ofi^.DataStart; + dirCluster := ofi^.DirCluster; + cleanFileName := ofi^.CleanName; + extPart := ofi^.ExtPart; + namePart := nil; + if lookup.Exists then begin + origByteSize := ofi^.ByteSize; + cluster := ofi^.FirstCluster; + end else begin + origByteSize := 0; + cluster := 0; + end; + end else begin + { Slow path: rediscover everything from disk } + lookup.Status := puint32(kalloc(sizeof(uint32))); + lookup.Status^ := 0; + lookup.BootRecord := readBootRecord(volume); + lookup.Dirs := readDirectory(volume, directory, lookup.Status); + lookup.Exists := false; + dataStart := fatDataStartLBA(volume, lookup.BootRecord); - splitFileNameParts(fileName, namePart, extPart); - - if LL_size(lookup.Dirs) > 0 then begin + splitFileNameParts(fileName, namePart, extPart); cleanFileName := cleanString(namePart, lookup.Status); - for i := 0 to LL_Size(lookup.Dirs) - 1 do begin - tempdir := PDirectory(LL_get(lookup.Dirs, i)); - otherCFN := cleanString(tempdir^.filename, lookup.Status); - if compareByteArray8(cleanFileName, otherCFN) and matchExtension(tempdir^.fileExtension, extPart) then begin - lookup.Dir := tempdir; - lookup.Exists := true; + + if LL_size(lookup.Dirs) > 0 then begin + for i := 0 to LL_Size(lookup.Dirs) - 1 do begin + tempdir := PDirectory(LL_get(lookup.Dirs, i)); + otherCFN := cleanString(tempdir^.filename, lookup.Status); + if compareByteArray8(cleanFileName, otherCFN) and matchExtension(tempdir^.fileExtension, extPart) then begin + lookup.Dir := tempdir; + lookup.Exists := true; + end; end; end; - end; - - kfree(void(namePart)); - kfree(void(extPart)); - - if not lookup.Exists then begin - { --- Create a new zero-length file entry on disk --- } { Determine parent cluster (Lesson 18: root has no '.' entry) } if (directory = nil) or (stringSize(directory) = 0) then @@ -2478,19 +2681,32 @@ begin dirCluster := lookup.BootRecord^.rootCluster; end; + if lookup.Exists then begin + origByteSize := lookup.Dir^.byteSize; + cluster := dirFirstCluster(lookup.Dir); + end else begin + origByteSize := 0; + cluster := 0; + end; + end; + fileSize := origByteSize; + + if not lookup.Exists then begin + { --- Create a new zero-length file entry on disk --- } + { dirCluster, cleanFileName, extPart already set above } + dirBuf := puint32(kalloc(lookup.BootRecord^.sectorSize)); - splitFileNameParts(fileName, namePart, extPart); - cleanFileName := cleanString(namePart, lookup.Status); - if not locateFreeDirEntry(volume, dirCluster, lookup.BootRecord, loc, dirBuf) then begin - kfree(void(namePart)); - kfree(void(extPart)); kfree(dirBuf); io.syslog.logln('FAT32', 'writeFileAtOffset: exit-A no free dir slot'); - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); + if ofi = nil then begin + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); + end; exit; end; @@ -2504,23 +2720,11 @@ begin rawDir^.byteSize := 0; driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, dirBuf); - kfree(void(namePart)); - kfree(void(extPart)); kfree(dirBuf); { Invalidate chain cache — new file } sioc_valid := false; - - { Set up a local dir record so the rest of the function can use it } - memset(uint32(@newDirEntry), 0, sizeof(TDirectory)); - newDirEntry.byteSize := 0; - newDirEntry.clusterLow := 0; - newDirEntry.clusterHigh := 0; - lookup.Dir := @newDirEntry; end; - - fileSize := lookup.Dir^.byteSize; - cluster := dirFirstCluster(lookup.Dir); newEnd := offset + byteCount; bytesPerCluster := uint32(lookup.BootRecord^.spc) * uint32(lookup.BootRecord^.sectorSize); @@ -2537,9 +2741,21 @@ begin newClusts := findFreeClusters(volume, extraClusters, lookup.BootRecord); if newClusts = nil then begin io.syslog.logln('FAT32', 'writeFileAtOffset: exit-B disk full'); - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); + { Undo just-created dir entry if file was new } + if not lookup.Exists then begin + dirBuf := puint32(kalloc(lookup.BootRecord^.sectorSize)); + driver.storage.mgr.storage_read(volume^.device, loc.SectorLBA, 1, dirBuf); + PDirectory(dirBuf)[loc.EntryIdx].fileName[0] := char($E5); + driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, dirBuf); + kfree(dirBuf); + end; + if ofi = nil then begin + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); + end; exit; end; @@ -2570,11 +2786,9 @@ begin writeFat(volume, lastCluster, $FFFFFFF8, lookup.BootRecord); end; - { If file was zero-length, update dir entry's cluster pointer } - if oldClusters = 0 then begin + { If file was zero-length, first new cluster becomes start cluster } + if oldClusters = 0 then cluster := puint32(LL_Get(newClusts, 0))^; - setDirFirstCluster(lookup.Dir, cluster); - end; { Update chain cache: last new cluster is the new tail } updateSIOC(volume, cluster, needClusters - 1, lastCluster); @@ -2592,9 +2806,13 @@ begin if not seekToClusterIndex(volume, cluster, startClusterIdx, lookup.BootRecord, curCluster, chainPos) then begin io.syslog.logln('FAT32', 'writeFileAtOffset: exit-C chain broke seeking to start'); - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); + if ofi = nil then begin + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); + end; exit; end; @@ -2693,43 +2911,40 @@ begin { ---- Update directory entry byteSize if file was extended ---- } { Only commit actual bytes written — never advance size past what was really written (Lesson 22) } - if (srcPos > 0) and ((offset + srcPos) > lookup.Dir^.byteSize) then begin - { Lesson 18: search raw directory sectors, don't use LL indices. } - if (directory = nil) or (stringSize(directory) = 0) then - dirCluster := lookup.BootRecord^.rootCluster - else begin - dirCluster := lookup.BootRecord^.rootCluster; - if LL_size(lookup.Dirs) > 0 then begin - tempdir := PDirectory(LL_get(lookup.Dirs, 0)); - if tempdir^.fileName[0] = '.' then - dirCluster := dirFirstCluster(tempdir); - end; - end; - + if (srcPos > 0) and ((offset + srcPos) > origByteSize) then begin + { Lesson 18: search raw directory sectors, don't use LL indices. + dirCluster, cleanFileName, extPart were set once in the discovery phase. } dirBuf := puint32(kalloc(lookup.BootRecord^.sectorSize)); - splitFileNameParts(fileName, namePart, extPart); - cleanFileName := cleanString(namePart, lookup.Status); - if locateDirEntry(volume, dirCluster, lookup.BootRecord, cleanFileName, extPart, loc, dirBuf) then begin rawDir := @PDirectory(dirBuf)[loc.EntryIdx]; rawDir^.byteSize := offset + srcPos; { If file was zero-length, update cluster pointers too } - if lookup.Dir^.byteSize = 0 then begin - rawDir^.clusterlow := lookup.Dir^.clusterlow; - rawDir^.clusterhigh := lookup.Dir^.clusterhigh; - end; + if origByteSize = 0 then + setDirFirstCluster(rawDir, cluster); driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, dirBuf); end; - kfree(void(namePart)); - kfree(void(extPart)); kfree(dirBuf); end; - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); + { Update cached open-file metadata so the next read/write sees the new state } + if ofi <> nil then begin + if (srcPos > 0) and ((offset + srcPos) > ofi^.ByteSize) then + ofi^.ByteSize := offset + srcPos; + if (ofi^.FirstCluster = 0) and (cluster <> 0) then begin + ofi^.FirstCluster := cluster; + ofi^.Exists := true; + end; + end; + + if ofi = nil then begin + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); + end; writeFileAtOffset := srcPos; push_trace('driver.storage.fs.fat32.writeFileAtOffset.exit'); end; @@ -2789,12 +3004,13 @@ begin push_trace('driver.storage.fs.fat32.getFileSize.exit'); end; -function readFileAtOffset(volume : PStorage_Volume; directory : pchar; fileName : pchar; offset : uint32; buffer : puint32; byteCount : uint32) : uint32; +function readFileAtOffset(volume : PStorage_Volume; directory : pchar; fileName : pchar; offset : uint32; buffer : puint32; byteCount : uint32; ctx : pointer) : uint32; const MAX_RUN_SECTORS = 2048; { cap per multi-sector read = 1 MB } var lookup : TFileLookup; run : TRunInfo; + ofi : PFATOpenFile; i : uint32; cluster : uint32; dataStart : uint32; @@ -2823,46 +3039,74 @@ var begin push_trace('driver.storage.fs.fat32.readFileAtOffset.enter'); readFileAtOffset := 0; + ofi := PFATOpenFile(ctx); - lookup.Status := puint32(kalloc(sizeof(uint32))); - lookup.Status^ := 0; - lookup.BootRecord := readBootRecord(volume); - lookup.Dirs := readDirectory(volume, directory, lookup.Status); - lookup.Exists := false; - dataStart := fatDataStartLBA(volume, lookup.BootRecord); + if ofi <> nil then begin + { Fast path: use cached open-file metadata } + lookup.BootRecord := ofi^.BootRecord; + lookup.Dirs := nil; + lookup.Dir := nil; + lookup.Status := nil; + lookup.Exists := ofi^.Exists; + dataStart := ofi^.DataStart; + if lookup.Exists then begin + fileSize := ofi^.ByteSize; + cluster := ofi^.FirstCluster; + end else begin + fileSize := 0; + cluster := 0; + end; + end else begin + { Slow path: rediscover everything from disk } + lookup.Status := puint32(kalloc(sizeof(uint32))); + lookup.Status^ := 0; + lookup.BootRecord := readBootRecord(volume); + lookup.Dirs := readDirectory(volume, directory, lookup.Status); + lookup.Exists := false; + dataStart := fatDataStartLBA(volume, lookup.BootRecord); - splitFileNameParts(fileName, namePart, extPart); + splitFileNameParts(fileName, namePart, extPart); - if LL_size(lookup.Dirs) > 0 then begin - cleanFileName := cleanString(namePart, lookup.Status); - for i := 0 to LL_Size(lookup.Dirs) - 1 do begin - tempdir := PDirectory(LL_get(lookup.Dirs, i)); - otherCFN := cleanString(tempdir^.filename, lookup.Status); - if compareByteArray8(cleanFileName, otherCFN) and matchExtension(tempdir^.fileExtension, extPart) then begin - lookup.Dir := tempdir; - lookup.Exists := true; + if LL_size(lookup.Dirs) > 0 then begin + cleanFileName := cleanString(namePart, lookup.Status); + for i := 0 to LL_Size(lookup.Dirs) - 1 do begin + tempdir := PDirectory(LL_get(lookup.Dirs, i)); + otherCFN := cleanString(tempdir^.filename, lookup.Status); + if compareByteArray8(cleanFileName, otherCFN) and matchExtension(tempdir^.fileExtension, extPart) then begin + lookup.Dir := tempdir; + lookup.Exists := true; + end; end; end; + + kfree(void(namePart)); + kfree(void(extPart)); + + if lookup.Exists then begin + fileSize := lookup.Dir^.byteSize; + cluster := dirFirstCluster(lookup.Dir); + end else begin + fileSize := 0; + cluster := 0; + end; end; - kfree(void(namePart)); - kfree(void(extPart)); - if not lookup.Exists then begin - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); + if ofi = nil then begin + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); + end; push_trace('driver.storage.fs.fat32.readFileAtOffset.notFound'); exit; end; - fileSize := lookup.Dir^.byteSize; - cluster := dirFirstCluster(lookup.Dir); - if offset >= fileSize then begin - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); + if ofi = nil then begin + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); + end; exit; end; @@ -2879,9 +3123,11 @@ begin { Determine curCluster at startClusterIdx using SIOC cache } if not seekToClusterIndex(volume, cluster, startClusterIdx, lookup.BootRecord, curCluster, chainPos) then begin - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); + if ofi = nil then begin + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); + end; exit; end; @@ -2951,9 +3197,11 @@ begin end; end; - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); + if ofi = nil then begin + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); + end; readFileAtOffset := destPos; push_trace('driver.storage.fs.fat32.readFileAtOffset.exit'); end; @@ -2976,6 +3224,8 @@ begin filesystem.deleteFileCallback := @deleteFile; filesystem.deleteDirCallback := @deleteDir; filesystem.renameFileCallback := @renameFile; + filesystem.openFileCallback := @fat32OpenFile; + filesystem.closeFileCallback := @fat32CloseFile; { Async callbacks: nil - VFS falls through to the sync callbacks above. All disk I/O uses psAwaiting in submit_io_wait: the calling process is parked cleanly (CPU-free) while the driver.storage.ctl.ahci ISR completes the request. } -- 2.54.0 From 5d7aacc28fc81bdf2aeba10255e0c740c198ba0f Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 15 Mar 2026 23:58:52 +0000 Subject: [PATCH 04/21] more improvements --- src/app/app.filebrowser.pas | 2 +- src/app/app.iotest.pas | 10 +- .../ctl/ahci/driver.storage.ctl.ahci.pas | 183 +- .../ahci/driver.storage.ctl.ahci.types.pas | 4 +- .../storage/ctl/driver.storage.ctl.ram.pas | 3 +- .../ctl/ide/driver.storage.ctl.ide.types.pas | 4 +- src/driver/storage/driver.storage.fdtable.pas | 44 + .../storage/driver.storage.iorequest.pas | 89 +- src/driver/storage/driver.storage.mgr.pas | 142 +- src/driver/storage/driver.storage.test.pas | 177 +- src/driver/storage/driver.storage.types.pas | 35 +- src/driver/storage/driver.storage.vfs.pas | 450 +++-- .../storage/fs/driver.storage.fs.fat32.pas | 1711 +++++++++++++++-- .../storage/fs/driver.storage.fs.flatfs.pas | 3 +- .../storage/fs/driver.storage.fs.iso9660.pas | 3 +- .../storage/vol/driver.storage.vol.mgr.pas | 12 +- src/proc/proc.types.pas | 2 +- 17 files changed, 2417 insertions(+), 457 deletions(-) diff --git a/src/app/app.filebrowser.pas b/src/app/app.filebrowser.pas index bca299c0..0305e63c 100644 --- a/src/app/app.filebrowser.pas +++ b/src/app/app.filebrowser.pas @@ -1860,7 +1860,7 @@ begin if (error = eNone) and (nfctx^.handle <> 0) then begin { WriteFileAsync needs a non-nil buffer; Length=0 so it is never read } driver.storage.vfs.WriteFileAsync(nfctx^.handle, 0, - puint8(@nfctx^.err), 0, @fb_newfile_written, nfctx); + puint8(@nfctx^.err), 0, nil, @fb_newfile_written, nfctx); end else begin if nfctx^.handle <> 0 then driver.storage.vfs.CloseFile(nfctx^.handle); diff --git a/src/app/app.iotest.pas b/src/app/app.iotest.pas index ac9c858d..ad9b1bd9 100644 --- a/src/app/app.iotest.pas +++ b/src/app/app.iotest.pas @@ -27,8 +27,8 @@ uses core.strings, arch.x86.bda; const - FILE_SIZE = 10 * 1024 * 1024; { 10 MB } - CHUNK_SIZE = 4096*64; { MB per is 4k*64 = 256 KB, so 40 chunks total } + FILE_SIZE = 25 * 1024 * 1024; { 25 MB } + CHUNK_SIZE = 4096*128; { MB per is 4k*128 = 512 KB, so 50 chunks total } TICK_HZ = 1024; { timer ISR frequency } DEFAULT_PATH : pchar = '/disk/vol0/IOTEST.BIN'; @@ -89,7 +89,7 @@ begin verBuf := puint8(kalloc(CHUNK_SIZE)); { ==================== WRITE PHASE ==================== } - io.stdio.bufWriteStr(stdout_buf, 'Writing 10 MB to '); + io.stdio.bufWriteStr(stdout_buf, 'Writing 25 MB to '); io.stdio.bufWriteStrLn(stdout_buf, path); fh := OpenFile(path, omCreate, @fErr); @@ -131,7 +131,9 @@ begin end; end; t1 := arch.x86.bda.Counters.c32; + io.stdio.bufWriteStrLn(stdout_buf, 'Closing write handle...'); CloseFile(fh); + io.stdio.bufWriteStrLn(stdout_buf, 'Write handle closed.'); if ok then begin elapsed := t1 - t0; @@ -215,7 +217,9 @@ begin end; end; t1 := arch.x86.bda.Counters.c32; + io.stdio.bufWriteStrLn(stdout_buf, 'Closing read handle...'); CloseFile(fh); + io.stdio.bufWriteStrLn(stdout_buf, 'Read handle closed.'); if ok then begin elapsed := t1 - t0; diff --git a/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas b/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas index 22e0d77a..d449211c 100644 --- a/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas +++ b/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas @@ -53,9 +53,9 @@ function find_cmd_slot(device : PAHCI_Device) : uint32; { Async primitives — return immediately after issuing the command. completion is called from IRQ context when the slot clears. completion may be nil if no callback is needed (e.g. init-time polling). } -function send_read_dma_async(device : PAHCI_Device; lba : uint64; count : uint32; buffer : puint32; completion : TIOCompletion; userdata : puint32) : boolean; -function send_write_dma_async(device : PAHCI_Device; lba : uint64; count : uint32; buffer : puint32; completion : TIOCompletion; userdata : puint32) : boolean; -function read_atapi_async(device : PAHCI_Device; lba : uint64; count : uint32; buffer : puint32; completion : TIOCompletion; userdata : puint32) : boolean; +function send_read_dma_async(device : PAHCI_Device; lba : uint64; count : uint32; buffer : puint32; completion : TIOCompletion; userdata : puint32) : TError; +function send_write_dma_async(device : PAHCI_Device; lba : uint64; count : uint32; buffer : puint32; completion : TIOCompletion; userdata : puint32) : TError; +function read_atapi_async(device : PAHCI_Device; lba : uint64; count : uint32; buffer : puint32; completion : TIOCompletion; userdata : puint32) : TError; { Storage-manager async callback wrappers } { Phase 3: TDriverDispatch procedures — bridge PIORequest to driver.storage.ctl.ahci DMA } @@ -68,6 +68,17 @@ function send_read_capacity(device : PAHCI_Device; sectorCount : puint32; blockS implementation +const + AHCI_ENABLE_NCQ = false; + +function min_u32(a : uint32; b : uint32) : uint32; +begin + if a < b then + min_u32 := a + else + min_u32 := b; +end; + procedure init(); var devID : TDeviceIdentifier; @@ -312,8 +323,15 @@ var b8 : puint16; storageDev : PStorage_Device; storageDev_sectorSize : uint32; + ncqDepth : uint32; + slotCount : uint32; + ncqSupported : boolean; begin device := PAHCI_Device(@controller^.devices[portIndex]); + slotCount := ((controller^.mio^.capabilites shr 8) and $1F) + 1; + device^.slotCount := slotCount; + device^.supportsNCQ := false; + device^.queueDepth := 1; //clear any pending interrupts device^.port^.int_status := $FFFFFFFF; @@ -414,7 +432,17 @@ begin device^.port^.int_enable := $40000003; b8 := puint16(buffer); + memcpy(uint32(buffer), uint32(@device^.ata_info[0]), 512); sec_count := (b8[61] shl 16) or b8[60]; + ncqDepth := (b8[75] and $1F) + 1; + ncqSupported := ((b8[76] and (1 shl 8)) <> 0) and (not isATAPI) and AHCI_ENABLE_NCQ; + if ncqSupported then begin + device^.supportsNCQ := true; + device^.queueDepth := min_u32(slotCount, ncqDepth); + end else begin + device^.supportsNCQ := false; + device^.queueDepth := 1; + end; { Free the 512-byte IDENTIFY buffer before allocating the write-test buffer } kfree(buffer); @@ -456,7 +484,7 @@ begin storageDev^.writable := true; storageDev^.dispatchRead := TDriverDispatch(@ahci_dispatch_read); storageDev^.dispatchWrite := TDriverDispatch(@ahci_dispatch_write); - storageDev^.maxActive := 32; { AHCI supports up to 32 command slots } + storageDev^.maxActive := device^.queueDepth; end; driver.storage.mgr.register_device(storageDev); @@ -611,6 +639,10 @@ var k : uint32; ctrl_is : uint32; port_is : uint32; + active : uint32; + slotCount : uint32; + completionCb : TIOCompletion; + completionData : puint32; port : PHBA_Port; controller : PAHCI_Controller; device : PAHCI_Device; @@ -633,15 +665,24 @@ begin success := ((port^.tfd and $1) = 0) and ((port_is and $40000000) = 0); { Scan all command slots for completed operations } - for k := 0 to 31 do begin - if device^.pending[k].inUse then begin - if (port^.cmd_issue shr k) and 1 = 0 then begin - device^.pending[k].inUse := false; - if device^.pending[k].completion <> nil then - device^.pending[k].completion(success, device^.pending[k].userdata); + active := port^.cmd_issue or port^.sata_active; + slotCount := device^.slotCount; + if slotCount = 0 then slotCount := 32; + if slotCount > 32 then slotCount := 32; + if slotCount > 0 then + for k := 0 to slotCount - 1 do begin + if device^.pending[k].inUse then begin + if ((active shr k) and 1) = 0 then begin + completionCb := device^.pending[k].completion; + completionData := device^.pending[k].userdata; + device^.pending[k].inUse := false; + device^.pending[k].completion := nil; + device^.pending[k].userdata := nil; + if completionCb <> nil then + completionCb(success, completionData); + end; end; end; - end; { Acknowledge port interrupt by writing back the read value } port^.int_status := port_is; @@ -655,16 +696,21 @@ end; function find_cmd_slot(device : PAHCI_Device) : uint32; var i : uint32; - cmd_issue : uint32; + active : uint32; + slotCount : uint32; begin - cmd_issue := device^.port^.cmd_issue; - for i := 0 to 31 do begin - if ((cmd_issue shr i) and 1 = 0) and (not device^.pending[i].inUse) then begin - find_cmd_slot := i; - exit; + active := device^.port^.cmd_issue or device^.port^.sata_active; + slotCount := device^.slotCount; + if slotCount = 0 then slotCount := 32; + if slotCount > 32 then slotCount := 32; + if slotCount > 0 then + for i := 0 to slotCount - 1 do begin + if ((active shr i) and 1 = 0) and (not device^.pending[i].inUse) then begin + find_cmd_slot := i; + exit; + end; end; - end; - { All 32 slots busy } + { All usable slots busy } find_cmd_slot := $FFFFFFFF; end; @@ -678,7 +724,7 @@ end; { ---- Async send functions ---- } -function send_read_dma_async(device : PAHCI_Device; lba : uint64; count : uint32; buffer : puint32; completion : TIOCompletion; userdata : puint32) : boolean; +function send_read_dma_async(device : PAHCI_Device; lba : uint64; count : uint32; buffer : puint32; completion : TIOCompletion; userdata : puint32) : TError; var fis : PHBA_FIS_REG_H2D; cmd_header : PHBA_CMD_HEADER; @@ -689,13 +735,16 @@ var prdt_count : uint32; slot : uint32; begin - send_read_dma_async := false; + send_read_dma_async := eIOError; prdt_count := (count div 4194304) + 1; if prdt_count > 32 then exit; slot := find_cmd_slot(device); - if slot = $FFFFFFFF then exit; { All 32 slots busy } + if slot = $FFFFFFFF then begin + send_read_dma_async := eNoFreeSlot; + exit; + end; cmd_header := PHBA_CMD_HEADER(uint32(device^.command_list) + (slot * sizeof(THBA_CMD_HEADER))); cmd_header^.cmd_fis_length := sizeof(THBA_FIS_REG_H2D) div sizeof(uint32); @@ -718,7 +767,6 @@ begin memset(uint32(fis), 0, sizeof(THBA_FIS_REG_H2D)); fis^.fis_type := uint8(FIS_TYPE_REG_H2D); fis^.c := $1; - fis^.command := ATA_CMD_READ_DMA_EXT; fis^.device := $40; fis^.lba0 := lba and $FF; @@ -730,8 +778,17 @@ begin sec_count := count div 512; if sec_count = 0 then sec_count := 1; - fis^.countl := sec_count and $FF; - fis^.counth := (sec_count shr 8) and $FF; + if device^.supportsNCQ then begin + fis^.command := ATA_CMD_READ_FPDMA_QUEUED; + fis^.featurel := sec_count and $FF; + fis^.featureh := (sec_count shr 8) and $FF; + fis^.countl := (slot and $1F) shl 3; + fis^.counth := 0; + end else begin + fis^.command := ATA_CMD_READ_DMA_EXT; + fis^.countl := sec_count and $FF; + fis^.counth := (sec_count shr 8) and $FF; + end; { Wait for TFD BSY+DRQ to clear } timeout := 0; @@ -739,6 +796,7 @@ begin timeout := timeout + 1; if timeout > 100000 then begin io.syslog.writestringln('driver.storage.ctl.ahci: Timeout waiting for port ready (async read)'); + send_read_dma_async := eIOTimeout; exit; end; end; @@ -748,14 +806,17 @@ begin device^.pending[slot].completion := completion; device^.pending[slot].userdata := userdata; + if device^.supportsNCQ then + device^.port^.sata_active := device^.port^.sata_active or (1 shl slot); + { Issue command \u2014 returns immediately } device^.port^.cmd_issue := device^.port^.cmd_issue or (1 shl slot); - send_read_dma_async := true; + send_read_dma_async := eNone; end; -function send_write_dma_async(device : PAHCI_Device; lba : uint64; count : uint32; buffer : puint32; completion : TIOCompletion; userdata : puint32) : boolean; +function send_write_dma_async(device : PAHCI_Device; lba : uint64; count : uint32; buffer : puint32; completion : TIOCompletion; userdata : puint32) : TError; var fis : PHBA_FIS_REG_H2D; cmd_header : PHBA_CMD_HEADER; @@ -766,13 +827,16 @@ var prdt_count : uint32; slot : uint32; begin - send_write_dma_async := false; + send_write_dma_async := eIOError; prdt_count := (count div 4194304) + 1; if prdt_count > 32 then exit; slot := find_cmd_slot(device); - if slot = $FFFFFFFF then exit; { All 32 slots busy } + if slot = $FFFFFFFF then begin + send_write_dma_async := eNoFreeSlot; + exit; + end; cmd_header := PHBA_CMD_HEADER(uint32(device^.command_list) + (slot * sizeof(THBA_CMD_HEADER))); cmd_header^.cmd_fis_length := sizeof(THBA_FIS_REG_H2D) div sizeof(uint32); @@ -795,7 +859,6 @@ begin memset(uint32(fis), 0, sizeof(THBA_FIS_REG_H2D)); fis^.fis_type := uint8(FIS_TYPE_REG_H2D); fis^.c := $1; - fis^.command := ATA_CMD_WRITE_DMA_EXT; fis^.device := $40; fis^.lba0 := lba and $FF; @@ -807,8 +870,17 @@ begin sec_count := count div 512; if sec_count = 0 then sec_count := 1; - fis^.countl := sec_count and $FF; - fis^.counth := (sec_count shr 8) and $FF; + if device^.supportsNCQ then begin + fis^.command := ATA_CMD_WRITE_FPDMA_QUEUED; + fis^.featurel := sec_count and $FF; + fis^.featureh := (sec_count shr 8) and $FF; + fis^.countl := (slot and $1F) shl 3; + fis^.counth := 0; + end else begin + fis^.command := ATA_CMD_WRITE_DMA_EXT; + fis^.countl := sec_count and $FF; + fis^.counth := (sec_count shr 8) and $FF; + end; { Wait for TFD BSY+DRQ to clear } timeout := 0; @@ -816,6 +888,7 @@ begin timeout := timeout + 1; if timeout > 100000 then begin io.syslog.writestringln('driver.storage.ctl.ahci: Timeout waiting for port ready (async write)'); + send_write_dma_async := eIOTimeout; exit; end; end; @@ -825,14 +898,17 @@ begin device^.pending[slot].completion := completion; device^.pending[slot].userdata := userdata; + if device^.supportsNCQ then + device^.port^.sata_active := device^.port^.sata_active or (1 shl slot); + { Issue command \u2014 returns immediately } device^.port^.cmd_issue := device^.port^.cmd_issue or (1 shl slot); - send_write_dma_async := true; + send_write_dma_async := eNone; end; //read atapi -function read_atapi_async(device : PAHCI_Device; lba : uint64; count : uint32; buffer : puint32; completion : TIOCompletion; userdata : puint32) : boolean; +function read_atapi_async(device : PAHCI_Device; lba : uint64; count : uint32; buffer : puint32; completion : TIOCompletion; userdata : puint32) : TError; var fis : PHBA_FIS_REG_H2D; cmd_header : PHBA_CMD_HEADER; @@ -842,13 +918,16 @@ var prdt_count : uint32; slot : uint32; begin - read_atapi_async := false; + read_atapi_async := eIOError; prdt_count := (count div 4194304) + 1; if prdt_count > 32 then exit; slot := find_cmd_slot(device); - if slot = $FFFFFFFF then exit; + if slot = $FFFFFFFF then begin + read_atapi_async := eNoFreeSlot; + exit; + end; cmd_header := PHBA_CMD_HEADER(uint32(device^.command_list) + (slot * sizeof(THBA_CMD_HEADER))); cmd_header^.cmd_fis_length := sizeof(THBA_FIS_REG_H2D) div sizeof(uint32); @@ -893,6 +972,7 @@ begin timeout := timeout + 1; if timeout > 100000 then begin io.syslog.writestringln('driver.storage.ctl.ahci: Timeout waiting for port ready (async ATAPI)'); + read_atapi_async := eIOTimeout; exit; end; end; @@ -905,7 +985,7 @@ begin { Issue command \u2014 returns immediately } device^.port^.cmd_issue := device^.port^.cmd_issue or (1 shl slot); - read_atapi_async := true; + read_atapi_async := eNone; end; { Send a SCSI READ CAPACITY (10) command to an ATAPI device via AHCI. @@ -932,6 +1012,10 @@ begin memset(uint32(buffer), 0, 512); slot := find_cmd_slot(device); + if slot = $FFFFFFFF then begin + kfree(buffer); + exit; + end; cmd_header := PHBA_CMD_HEADER(uint32(device^.command_list) + (slot * sizeof(THBA_CMD_HEADER))); cmd_header^.cmd_fis_length := sizeof(THBA_FIS_REG_H2D) div sizeof(uint32); cmd_header^.wrt := 0; @@ -1039,43 +1123,48 @@ end; procedure ahci_dispatch_read(device : PStorage_Device; request : PIORequest); var ahciDev : PAHCI_Device; + err : TError; begin ahciDev := PAHCI_Device(device^.controllerId0); - if not send_read_dma_async(ahciDev, request^.LBA, + err := send_read_dma_async(ahciDev, request^.LBA, request^.SectorCount * device^.sectorSize, puint32(request^.Buffer), - @ahci_io_completion, puint32(request)) then - driver.storage.mgr.complete_io(request, false, eIOError); + @ahci_io_completion, puint32(request)); + if err <> eNone then + driver.storage.mgr.complete_io(request, false, err); end; { TDriverDispatch — SATA write via DMA } procedure ahci_dispatch_write(device : PStorage_Device; request : PIORequest); var ahciDev : PAHCI_Device; + err : TError; begin ahciDev := PAHCI_Device(device^.controllerId0); - if not send_write_dma_async(ahciDev, request^.LBA, + err := send_write_dma_async(ahciDev, request^.LBA, request^.SectorCount * device^.sectorSize, puint32(request^.Buffer), - @ahci_io_completion, puint32(request)) then - driver.storage.mgr.complete_io(request, false, eIOError); + @ahci_io_completion, puint32(request)); + if err <> eNone then + driver.storage.mgr.complete_io(request, false, err); end; { TDriverDispatch — ATAPI read via SCSI READ(12) } procedure ahci_dispatch_atapi_read(device : PStorage_Device; request : PIORequest); var ahciDev : PAHCI_Device; + err : TError; begin ahciDev := PAHCI_Device(device^.controllerId0); - if not read_atapi_async(ahciDev, request^.LBA, + err := read_atapi_async(ahciDev, request^.LBA, request^.SectorCount * device^.sectorSize, puint32(request^.Buffer), - @ahci_io_completion, puint32(request)) then - driver.storage.mgr.complete_io(request, false, eIOError); + @ahci_io_completion, puint32(request)); + if err <> eNone then + driver.storage.mgr.complete_io(request, false, err); end; Initialization boot.mgr.registerBoot('driver.storage.ctl.ahci', @init, 'AHCI Controller', 'driver.storage.fs.*'); end. - diff --git a/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.types.pas b/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.types.pas index e856945b..cc5d4645 100644 --- a/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.types.pas +++ b/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.types.pas @@ -269,6 +269,9 @@ type port : PHBA_Port; device_type : TDeviceType; ata_info : TIdentResponse; + supportsNCQ : boolean; + queueDepth : uint32; { safe active depth for this device } + slotCount : uint32; { controller slot count from CAP.NCS + 1 } command_list : PHBA_CMD_HEADER; fis : PHBA_FIS; command_table: PHBA_CMD_TABLE; @@ -335,4 +338,3 @@ end; // end; end. - diff --git a/src/driver/storage/ctl/driver.storage.ctl.ram.pas b/src/driver/storage/ctl/driver.storage.ctl.ram.pas index cdbc0c42..fc50021b 100644 --- a/src/driver/storage/ctl/driver.storage.ctl.ram.pas +++ b/src/driver/storage/ctl/driver.storage.ctl.ram.pas @@ -86,7 +86,8 @@ end; Returns actual bytes copied. } function rd_ReadOffset(volume : PStorage_Volume; directory : pchar; fileName : pchar; offset : uint32; - buffer : puint32; byteCount : uint32) : uint32; + buffer : puint32; byteCount : uint32; + ctx : pointer) : uint32; var clean : pchar; idx : sint32; diff --git a/src/driver/storage/ctl/ide/driver.storage.ctl.ide.types.pas b/src/driver/storage/ctl/ide/driver.storage.ctl.ide.types.pas index 589b5992..b660d0df 100644 --- a/src/driver/storage/ctl/ide/driver.storage.ctl.ide.types.pas +++ b/src/driver/storage/ctl/ide/driver.storage.ctl.ide.types.pas @@ -45,10 +45,12 @@ const ATA_CMD_READ_PIO_EXT = $24; ATA_CMD_READ_DMA = $C8; ATA_CMD_READ_DMA_EXT = $25; + ATA_CMD_READ_FPDMA_QUEUED = $60; ATA_CMD_WRITE_PIO = $30; ATA_CMD_WRITE_PIO_EXT = $34; ATA_CMD_WRITE_DMA = $CA; ATA_CMD_WRITE_DMA_EXT = $35; + ATA_CMD_WRITE_FPDMA_QUEUED = $61; ATA_CMD_CACHE_FLUSH = $E7; ATA_CMD_CACHE_FLUSH_EXT = $EA; ATA_CMD_PACKET = $A0; @@ -146,4 +148,4 @@ type implementation -end. \ No newline at end of file +end. diff --git a/src/driver/storage/driver.storage.fdtable.pas b/src/driver/storage/driver.storage.fdtable.pas index 71067b32..50430d40 100644 --- a/src/driver/storage/driver.storage.fdtable.pas +++ b/src/driver/storage/driver.storage.fdtable.pas @@ -37,6 +37,7 @@ type PFileDescriptor = ^TFileDescriptor; TFileDescriptor = record InUse : boolean; + AsyncRefs : uint32; { in-flight async ops using this descriptor } Volume : PStorage_Volume; Directory : pchar; { directory path within the volume } FileName : pchar; { filename within that directory } @@ -45,6 +46,8 @@ type DataSize : uint32; { cached file size in bytes (from fileSizeCallback) } DeviceOps : pointer; { PVFSDeviceOps — non-nil for device FDs } DeviceData : pointer; { opaque data passed to device callbacks } + FSPrivate : pointer; { opaque FS-level per-file metadata (from openFileCallback) } + FSCloseHook : pointer; { procedure(pointer) — frees FSPrivate on close } end; PFDTable = ^TFDTable; @@ -68,6 +71,9 @@ function fd_alloc(table : PFDTable) : uint32; { Get a pointer to the descriptor for a 1-based handle. Returns nil if handle is out of range or the slot is not in use. } function fd_get(table : PFDTable; handle : uint32) : PFileDescriptor; +function fd_begin_async(entry : PFileDescriptor) : boolean; +procedure fd_end_async(entry : PFileDescriptor); +function fd_has_async(entry : PFileDescriptor) : boolean; { Close a single file descriptor: free its buffers and core.strings, mark the slot as not-in-use. @@ -94,11 +100,20 @@ begin fd_table_new := tbl; end; +type + TFSPrivateClose = procedure(data : pointer); + procedure fd_free_entry(entry : PFileDescriptor); begin if entry = nil then exit; if not entry^.InUse then exit; + { Free FS-level per-file metadata first } + if (entry^.FSPrivate <> nil) and (entry^.FSCloseHook <> nil) then + TFSPrivateClose(entry^.FSCloseHook)(entry^.FSPrivate); + entry^.FSPrivate := nil; + entry^.FSCloseHook := nil; + if entry^.Directory <> nil then begin kfree(void(entry^.Directory)); entry^.Directory := nil; @@ -113,6 +128,7 @@ begin end; entry^.InUse := false; + entry^.AsyncRefs := 0; entry^.DataSize := 0; entry^.DeviceOps := nil; entry^.DeviceData := nil; @@ -148,6 +164,34 @@ begin fd_get := @table^.Entries[handle - 1]; end; +function fd_begin_async(entry : PFileDescriptor) : boolean; +begin + fd_begin_async := false; + if entry = nil then exit; + asm pushf; cli end; + if entry^.InUse then begin + entry^.AsyncRefs := entry^.AsyncRefs + 1; + fd_begin_async := true; + end; + asm popf end; +end; + +procedure fd_end_async(entry : PFileDescriptor); +begin + if entry = nil then exit; + asm pushf; cli end; + if entry^.AsyncRefs > 0 then + entry^.AsyncRefs := entry^.AsyncRefs - 1; + asm popf end; +end; + +function fd_has_async(entry : PFileDescriptor) : boolean; +begin + fd_has_async := false; + if entry = nil then exit; + fd_has_async := entry^.AsyncRefs > 0; +end; + function fd_close(table : PFDTable; handle : uint32) : boolean; begin fd_close := false; diff --git a/src/driver/storage/driver.storage.iorequest.pas b/src/driver/storage/driver.storage.iorequest.pas index 7b8a8aed..daf680f1 100644 --- a/src/driver/storage/driver.storage.iorequest.pas +++ b/src/driver/storage/driver.storage.iorequest.pas @@ -14,7 +14,7 @@ { Driver->Storage->IORequest - I/O request allocation and lifecycle helpers. - Provides heap allocation/free for TIORequest records used by the + Provides fixed-pool allocation/free for TIORequest records used by the submit_io / complete_io path in driver.storage.mgr.pas. @author(Aaron Hance ) @@ -27,7 +27,7 @@ uses memory.heap, driver.storage.types; -{ Allocate a new TIORequest on the heap and initialise its core fields. +{ Allocate a new TIORequest from the fixed request pool and initialise its core fields. Caller, State, Error and ByteCount are zeroed (caller fills as needed). } function ioreq_alloc(reqType : TIORequestType; device : PStorage_Device; @@ -35,16 +35,90 @@ function ioreq_alloc(reqType : TIORequestType; sectors : uint32; buf : pointer) : PIORequest; -{ Free a heap-allocated TIORequest. Safe to call with nil. } +{ Return a pooled TIORequest to the free list. Safe to call with nil. } procedure ioreq_free(req : PIORequest); -{ Copy an existing TIORequest into a new heap allocation (deep copy of the +{ Copy an existing TIORequest into a new pooled allocation (deep copy of the record, NOT the buffer it points to). Used by dispatch_next to create an ISR-safe copy from the CFIFO value-element. } function ioreq_copy(src : PIORequest) : PIORequest; implementation +const + IOREQ_POOL_CAPACITY = 512; + IOREQ_REQ_OFFSET = sizeof(pointer); + +type + PIORequestPoolNode = ^TIORequestPoolNode; + TIORequestPoolNode = record + Next : PIORequestPoolNode; + Req : TIORequest; + end; + +var + ioreqPoolReady : boolean; + ioreqPoolBuf : pointer; + ioreqFreeList : PIORequestPoolNode; + +procedure ioreq_pool_init(); +var + i : uint32; + base : uint32; + node : PIORequestPoolNode; +begin + if ioreqPoolReady then exit; + + ioreqPoolBuf := kalloc(IOREQ_POOL_CAPACITY * sizeof(TIORequestPoolNode)); + if ioreqPoolBuf = nil then exit; + + ioreqFreeList := nil; + base := uint32(ioreqPoolBuf); + if IOREQ_POOL_CAPACITY > 0 then + for i := 0 to IOREQ_POOL_CAPACITY - 1 do begin + node := PIORequestPoolNode(base + (i * sizeof(TIORequestPoolNode))); + node^.Next := ioreqFreeList; + ioreqFreeList := node; + end; + + ioreqPoolReady := true; +end; + +function ioreq_take() : PIORequest; +var + node : PIORequestPoolNode; +begin + if not ioreqPoolReady then + ioreq_pool_init(); + if not ioreqPoolReady then begin + ioreq_take := nil; + exit; + end; + + asm pushf; cli end; + node := ioreqFreeList; + if node <> nil then + ioreqFreeList := node^.Next; + asm popf end; + + if node = nil then + ioreq_take := nil + else + ioreq_take := @node^.Req; +end; + +procedure ioreq_release(req : PIORequest); +var + node : PIORequestPoolNode; +begin + if req = nil then exit; + node := PIORequestPoolNode(uint32(req) - IOREQ_REQ_OFFSET); + asm pushf; cli end; + node^.Next := ioreqFreeList; + ioreqFreeList := node; + asm popf end; +end; + function ioreq_alloc(reqType : TIORequestType; device : PStorage_Device; lba : uint32; @@ -53,7 +127,7 @@ function ioreq_alloc(reqType : TIORequestType; var req : PIORequest; begin - req := PIORequest(kalloc(SizeOf(TIORequest))); + req := ioreq_take(); if req = nil then begin ioreq_alloc := nil; exit; @@ -75,15 +149,14 @@ end; procedure ioreq_free(req : PIORequest); begin - if req <> nil then - kfree(void(req)); + ioreq_release(req); end; function ioreq_copy(src : PIORequest) : PIORequest; var dst : PIORequest; begin - dst := PIORequest(kalloc(SizeOf(TIORequest))); + dst := ioreq_take(); if dst = nil then begin ioreq_copy := nil; exit; diff --git a/src/driver/storage/driver.storage.mgr.pas b/src/driver/storage/driver.storage.mgr.pas index e6712bf9..7d141a85 100644 --- a/src/driver/storage/driver.storage.mgr.pas +++ b/src/driver/storage/driver.storage.mgr.pas @@ -135,6 +135,15 @@ type end; PSyncWaitData = ^TSyncWaitData; + { Async MBR-write wrapper. Keeps a sector-sized DMA buffer alive until the + hardware completion fires, then frees it before forwarding the user's callback. } + TMBRWriteAsyncData = record + Buffer : pointer; + UserCallback : TIOCallback; + UserData : pointer; + end; + PMBRWriteAsyncData = ^TMBRWriteAsyncData; + { Callback fired from ISR context by complete_io. Sets the error, wakes the parked process, then sets Done=1 so the pointer-dereferenced spin in submit_io_wait can exit. Safe with IF=0. } @@ -152,6 +161,40 @@ begin puint32(@wait^.Done)^ := 1; end; +function alloc_mbr_io_buffer(device : PStorage_Device; mbr : PMaster_Boot_Record) : puint32; +var + bufSz : uint32; + buf : puint32; +begin + alloc_mbr_io_buffer := nil; + if (device = nil) or (mbr = nil) then exit; + + bufSz := device^.sectorSize; + if bufSz < sizeof(TMaster_Boot_Record) then + bufSz := sizeof(TMaster_Boot_Record); + + buf := puint32(kalloc(bufSz)); + if buf = nil then exit; + + memset(uint32(buf), 0, bufSz); + memcpy(uint32(mbr), uint32(buf), sizeof(TMaster_Boot_Record)); + alloc_mbr_io_buffer := buf; +end; + +procedure mbr_write_async_callback(error : TError; userdata : pointer); +var + ctx : PMBRWriteAsyncData; +begin + ctx := PMBRWriteAsyncData(userdata); + if ctx = nil then exit; + + if ctx^.Buffer <> nil then + kfree(void(ctx^.Buffer)); + if ctx^.UserCallback <> nil then + ctx^.UserCallback(error, ctx^.UserData); + kfree(void(ctx)); +end; + procedure init(); begin push_trace('driver.storage.mgr.init'); @@ -184,7 +227,7 @@ begin { Initialise new Phase 2 fields } device^.removed := false; - device^.requestQueue := CFIFO_New(SizeOf(TIORequest), 32); + device^.requestQueue := CFIFO_New(SizeOf(PIORequest), 128); device^.activeCount := 0; if device^.maxActive = 0 then device^.maxActive := 1; device^.cachedMBR := nil; @@ -294,9 +337,10 @@ end; procedure write_mbr(device : PStorage_Device; mbr : PMaster_Boot_Record); var cache : puint32; + buf : puint32; begin push_trace('driver.storage.mgr.write_mbr'); - if not device^.writable then exit; + if (device = nil) or (mbr = nil) or (not device^.writable) then exit; { Update cache } if device^.cachedMBR <> nil then kfree(device^.cachedMBR); @@ -304,7 +348,11 @@ begin if cache <> nil then memcpy(uint32(mbr), uint32(cache), sizeof(TMaster_Boot_Record)); device^.cachedMBR := pointer(cache); - storage_write(device, 0, 1, puint32(mbr)); + + buf := alloc_mbr_io_buffer(device, mbr); + if buf = nil then exit; + storage_write(device, 0, 1, buf); + kfree(void(buf)); end; function get_cached_mbr(device : PStorage_Device) : PMaster_Boot_Record; @@ -317,8 +365,15 @@ procedure write_mbr_async(device : PStorage_Device; mbr : PMaster_Boot_Record; callback : TIOCallback; callbackData : pointer); var cache : puint32; + buf : puint32; + ctx : PMBRWriteAsyncData; + err : TError; begin push_trace('driver.storage.mgr.write_mbr_async'); + if (device = nil) or (mbr = nil) then begin + if callback <> nil then callback(eInvalidArgument, callbackData); + exit; + end; if not device^.writable then begin if callback <> nil then callback(eReadOnly, callbackData); exit; @@ -330,7 +385,29 @@ begin if cache <> nil then memcpy(uint32(mbr), uint32(cache), sizeof(TMaster_Boot_Record)); device^.cachedMBR := pointer(cache); - storage_write_async(device, 0, 1, puint32(mbr), callback, callbackData); + + buf := alloc_mbr_io_buffer(device, mbr); + if buf = nil then begin + if callback <> nil then callback(eOutOfMemory, callbackData); + exit; + end; + + ctx := PMBRWriteAsyncData(kalloc(sizeof(TMBRWriteAsyncData))); + if ctx = nil then begin + kfree(void(buf)); + if callback <> nil then callback(eOutOfMemory, callbackData); + exit; + end; + ctx^.Buffer := pointer(buf); + ctx^.UserCallback := callback; + ctx^.UserData := callbackData; + + err := storage_write_async(device, 0, 1, buf, @mbr_write_async_callback, ctx); + if err <> eNone then begin + kfree(void(buf)); + kfree(void(ctx)); + if callback <> nil then callback(err, callbackData); + end; end; { ========================================================================== } @@ -341,21 +418,13 @@ end; Called with interrupts disabled (CLI context) or from ISR context. } procedure dispatch_next(device : PStorage_Device); var - reqBuf : TIORequest; heapReq : PIORequest; begin { Dispatch queued requests up to maxActive hardware slots } while device^.activeCount < device^.maxActive do begin - if not CFIFO_Dequeue(device^.requestQueue, @reqBuf) then break; { queue empty } - - { Heap copy for ISR safety — driver completion may fire on any stack } - heapReq := ioreq_copy(@reqBuf); - if heapReq = nil then begin - { Out of memory — notify caller via callback } - if reqBuf.Callback <> nil then - reqBuf.Callback(eOutOfMemory, reqBuf.CallbackData); + if not CFIFO_Dequeue(device^.requestQueue, @heapReq) then break; { queue empty } + if heapReq = nil then break; - end; device^.activeCount := device^.activeCount + 1; heapReq^.State := iosDispatched; @@ -380,7 +449,8 @@ end; function submit_io(request : PIORequest) : TError; var - device : PStorage_Device; + device : PStorage_Device; + heapReq : PIORequest; begin { Pre-checks — fail fast before touching the queue } if request = nil then begin submit_io := eInvalidArgument; exit; end; @@ -400,9 +470,47 @@ begin request^.State := iosPending; request^.Error := eNone; - { Enqueue and attempt dispatch — must be ISR-safe } + { Copy once into the pooled request arena so the driver/ISR path never + depends on a caller-owned stack record. The queue stores pointers to + these owned requests; complete_io returns them to the pool. } + heapReq := ioreq_copy(request); + if heapReq = nil then begin + submit_io := eOutOfMemory; + exit; + end; + + { Enqueue and attempt dispatch — must be ISR-safe and bounded. + Reject once the fixed queue is full instead of growing with CLI held. } asm pushf; cli end; - CFIFO_Enqueue(device^.requestQueue, @request^); + if (device^.activeCount < device^.maxActive) and (CFIFO_Size(device^.requestQueue) = 0) then begin + device^.activeCount := device^.activeCount + 1; + heapReq^.State := iosDispatched; + asm popf end; + case heapReq^.RequestType of + ioRead: begin + if device^.dispatchRead <> nil then + device^.dispatchRead(device, heapReq) + else + complete_io(heapReq, false, eDeviceNotFound); + end; + ioWrite: begin + if device^.dispatchWrite <> nil then + device^.dispatchWrite(device, heapReq) + else + complete_io(heapReq, false, eDeviceNotFound); + end; + end; + submit_io := eNone; + exit; + end; + + if CFIFO_Size(device^.requestQueue) >= CFIFO_Capacity(device^.requestQueue) then begin + asm popf end; + ioreq_free(heapReq); + submit_io := eQueueFull; + exit; + end; + CFIFO_Enqueue(device^.requestQueue, @heapReq); dispatch_next(device); asm popf end; diff --git a/src/driver/storage/driver.storage.test.pas b/src/driver/storage/driver.storage.test.pas index efd3f656..8400c885 100644 --- a/src/driver/storage/driver.storage.test.pas +++ b/src/driver/storage/driver.storage.test.pas @@ -46,10 +46,31 @@ uses core.strings, io.syslog, debug.tracer, - core.util, arch.x86.util, + core.util, arch.x86.util, arch.x86.bda, driver.storage.vfs, driver.storage.vol.mgr; +const + STORBENCH_DEFAULT_MB = 32; + STORBENCH_BUFFER_BYTES = 1024 * 1024; + STORBENCH_TICK_HZ = 1024; + +function isNumericStr(s : pchar) : boolean; +var + i : uint32; +begin + isNumericStr := false; + if (s = nil) or (s[0] = char(0)) then + exit; + i := 0; + while s[i] <> char(0) do begin + if (s[i] < '0') or (s[i] > '9') then + exit; + i := i + 1; + end; + isNumericStr := true; +end; + { ============================================================ run_tests — shared test body called by both UnitTest (boot) and the STORTEST shell command (runtime). @@ -475,8 +496,158 @@ begin kfree(void(fStr)); end; +procedure cmd_storbench(params : PParamList; stdin_buf, stdout_buf, stderr_buf : POutBuf); +var + disk : PStorage_Device; + devIdx : uint32; + mbToRead : uint32; + totalBytes : uint32; + buf : puint32; + chunkSectors: uint32; + sectorsLeft : uint32; + sectorsNow : uint32; + startLBA : uint32; + lba : uint32; + t0, t1 : uint32; + tLast : uint32; + elapsed : uint32; + bytesInSec : uint32; + doneBytes : uint32; + kbps : uint32; + err : TError; + + procedure writeThroughput(kbpsValue : uint32); + begin + if kbpsValue >= 1024 then begin + io.stdio.bufWriteInt(stdout_buf, kbpsValue div 1024); + io.stdio.bufWriteStr(stdout_buf, '.'); + io.stdio.bufWriteInt(stdout_buf, ((kbpsValue mod 1024) * 10) div 1024); + io.stdio.bufWriteStr(stdout_buf, ' MB/s'); + end else begin + io.stdio.bufWriteInt(stdout_buf, kbpsValue); + io.stdio.bufWriteStr(stdout_buf, ' KB/s'); + end; + end; +begin + devIdx := 0; + mbToRead := STORBENCH_DEFAULT_MB; + + if paramCount(params) >= 1 then begin + if not isNumericStr(getParam(0, params)) then begin + io.stdio.bufWriteStrLn(stderr_buf, 'Usage: STORBENCH [device_index] [mb]'); + exit; + end; + devIdx := stringToInt(getParam(0, params)); + end; + + if paramCount(params) >= 2 then begin + if not isNumericStr(getParam(1, params)) then begin + io.stdio.bufWriteStrLn(stderr_buf, 'Usage: STORBENCH [device_index] [mb]'); + exit; + end; + mbToRead := stringToInt(getParam(1, params)); + if mbToRead = 0 then + mbToRead := STORBENCH_DEFAULT_MB; + end; + + disk := driver.storage.mgr.get_device(devIdx); + if disk = nil then begin + io.stdio.bufWriteStr(stderr_buf, 'No storage device at index '); + io.stdio.bufWriteIntLn(stderr_buf, devIdx); + exit; + end; + + if disk^.sectorSize = 0 then begin + io.stdio.bufWriteStrLn(stderr_buf, 'Device has invalid sector size.'); + exit; + end; + + totalBytes := mbToRead * 1024 * 1024; + chunkSectors := STORBENCH_BUFFER_BYTES div disk^.sectorSize; + if chunkSectors = 0 then + chunkSectors := 1; + + buf := puint32(kalloc(chunkSectors * disk^.sectorSize)); + if buf = nil then begin + io.stdio.bufWriteStrLn(stderr_buf, 'Out of memory allocating benchmark buffer.'); + exit; + end; + + sectorsLeft := totalBytes div disk^.sectorSize; + if sectorsLeft = 0 then + sectorsLeft := 1; + if sectorsLeft > disk^.maxSectorCount then + sectorsLeft := disk^.maxSectorCount; + + if disk^.maxSectorCount > (sectorsLeft + 2048) then + startLBA := 2048 + else + startLBA := 0; + lba := startLBA; + + io.stdio.bufWriteStr(stdout_buf, 'Raw read benchmark: disk '); + io.stdio.bufWriteInt(stdout_buf, devIdx); + io.stdio.bufWriteStr(stdout_buf, ', sectorSize='); + io.stdio.bufWriteInt(stdout_buf, disk^.sectorSize); + io.stdio.bufWriteStr(stdout_buf, ', maxActive='); + io.stdio.bufWriteInt(stdout_buf, disk^.maxActive); + io.stdio.bufWriteNewLine(stdout_buf); + + t0 := arch.x86.bda.Counters.c32; + tLast := t0; + bytesInSec := 0; + doneBytes := 0; + + while sectorsLeft > 0 do begin + sectorsNow := chunkSectors; + if sectorsNow > sectorsLeft then + sectorsNow := sectorsLeft; + + err := driver.storage.mgr.storage_read(disk, lba, sectorsNow, buf); + if err <> eNone then begin + io.stdio.bufWriteStr(stderr_buf, 'Raw read failed at LBA '); + io.stdio.bufWriteInt(stderr_buf, lba); + io.stdio.bufWriteStr(stderr_buf, ' error='); + io.stdio.bufWriteIntLn(stderr_buf, ord(err)); + kfree(void(buf)); + exit; + end; + + lba := lba + sectorsNow; + sectorsLeft := sectorsLeft - sectorsNow; + doneBytes := doneBytes + (sectorsNow * disk^.sectorSize); + bytesInSec := bytesInSec + (sectorsNow * disk^.sectorSize); + + t1 := arch.x86.bda.Counters.c32; + if (t1 - tLast) >= STORBENCH_TICK_HZ then begin + kbps := (bytesInSec div 1024) * STORBENCH_TICK_HZ div (t1 - tLast); + io.stdio.bufWriteStr(stdout_buf, ' RAW '); + io.stdio.bufWriteInt(stdout_buf, doneBytes div 1024); + io.stdio.bufWriteStr(stdout_buf, ' KB '); + writeThroughput(kbps); + io.stdio.bufWriteNewLine(stdout_buf); + tLast := t1; + bytesInSec := 0; + end; + end; + + t1 := arch.x86.bda.Counters.c32; + elapsed := t1 - t0; + io.stdio.bufWriteStr(stdout_buf, 'Raw read complete: '); + if elapsed > 0 then begin + kbps := ((doneBytes div 1024) * STORBENCH_TICK_HZ) div elapsed; + writeThroughput(kbps); + io.stdio.bufWriteStr(stdout_buf, ' ('); + io.stdio.bufWriteInt(stdout_buf, (elapsed * 1000) div STORBENCH_TICK_HZ); + io.stdio.bufWriteStrLn(stdout_buf, ' ms)'); + end else + io.stdio.bufWriteStrLn(stdout_buf, '< 1 tick'); + + kfree(void(buf)); +end; + { ============================================================ - init — register the STORTEST and DISKTEST terminal commands + init — register the storage terminal commands ============================================================ } procedure init; begin @@ -485,6 +656,8 @@ begin 'Run storage subsystem unit tests.'); io.stdio.registerCommand('DISKTEST', @cmd_disktest, 'DESTRUCTIVE: wipe disk 0, format FAT32 4MB, write+read file.'); + io.stdio.registerCommand('STORBENCH', @cmd_storbench, + 'Read-only raw storage benchmark: STORBENCH [device_index] [mb].'); debug.tracer.pop_trace; end; diff --git a/src/driver/storage/driver.storage.types.pas b/src/driver/storage/driver.storage.types.pas index c76108ca..b5e0c0bc 100644 --- a/src/driver/storage/driver.storage.types.pas +++ b/src/driver/storage/driver.storage.types.pas @@ -134,9 +134,11 @@ type { === I/O callback type === } - { Async I/O completion callback — fired from ISR context when a request finishes. + { Completion callback used across storage/VFS async APIs. error = eNone on success; specific code on failure. - userdata is the opaque pointer passed by the submitter. } + userdata is the opaque pointer passed by the submitter. + Context is API-specific: some callers complete from ISR context, others + from task/worker context, and some may invoke the callback immediately. } TIOCallback = procedure(error : TError; userdata : pointer); { Filesystem callback core.types } @@ -150,16 +152,29 @@ type PPIdentifyHook = function(volume : PStorage_Volume) : boolean; PPRenameFileHook = procedure(volume : PStorage_Volume; filePath : pchar; newName : pchar; status : puint32); { Offset-based read: reads byteCount bytes starting at byte offset into the file. - Returns the number of bytes actually read. } - PPReadOffsetHook = function(volume : PStorage_Volume; directory : pchar; fileName : pchar; offset : uint32; buffer : puint32; byteCount : uint32) : uint32; + Returns the number of bytes actually read. ctx is the opaque per-file + context returned by openFileCallback (nil when called without a handle). } + PPReadOffsetHook = function(volume : PStorage_Volume; directory : pchar; fileName : pchar; offset : uint32; buffer : puint32; byteCount : uint32; ctx : pointer) : uint32; { Offset-based write: writes byteCount bytes starting at byte offset into the file. Returns the number of bytes actually written. Filesystems that do not implement this leave the field nil; VFS returns 0 for stream-mode writes on volume FDs. } - PPWriteOffsetHook = function(volume : PStorage_Volume; directory : pchar; fileName : pchar; offset : uint32; buffer : puint32; byteCount : uint32) : uint32; + PPWriteOffsetHook = function(volume : PStorage_Volume; directory : pchar; fileName : pchar; offset : uint32; buffer : puint32; byteCount : uint32; ctx : pointer) : uint32; + { Async offset-based read/write hooks. Implementations must return immediately + and fire callback when the transfer completes. BytesRead/BytesWritten may be + nil; if provided, implementations store the actual completed byte count. } + PPReadOffsetAsyncHook = procedure(volume : PStorage_Volume; directory : pchar; fileName : pchar; offset : uint32; buffer : puint32; byteCount : uint32; ctx : pointer; bytesRead : puint32; callback : TIOCallback; callbackData : pointer); + PPWriteOffsetAsyncHook = procedure(volume : PStorage_Volume; directory : pchar; fileName : pchar; offset : uint32; buffer : puint32; byteCount : uint32; ctx : pointer; bytesWritten : puint32; callback : TIOCallback; callbackData : pointer); { File size query: returns the size in bytes of the named file. Returns 0 if the file does not exist or has zero length. } PPFileSizeHook = function(volume : PStorage_Volume; directory : pchar; fileName : pchar) : uint32; + { Per-file open hook: called by VFS on OpenFile to create FS-level cached + metadata. Returns an opaque pointer stored in the FD and passed as ctx + to read/write hooks. Also returns the current file size through the + var parameter. Returning nil is legal (disables per-call fast-path). } + PPOpenFileHook = function(volume : PStorage_Volume; directory : pchar; fileName : pchar; var fileSize : uint32) : pointer; + { Per-file close hook: frees the context allocated by openFileCallback. } + PPCloseFileHook = procedure(ctx : pointer); { === Async filesystem hook core.types === Each mirrors the corresponding sync hook but receives a TIOCallback + callbackData. @@ -168,8 +183,6 @@ type PPLinkedListBase = ^PLinkedListBase; { needed for PPReadDirAsyncHook parameter } PPCreateDirAsyncHook = procedure(volume : PStorage_Volume; directory : pchar; dirname : pchar; attributes : uint32; status : puint32; callback : TIOCallback; callbackData : pointer); PPReadDirAsyncHook = procedure(volume : PStorage_Volume; directory : pchar; resultList : PPLinkedListBase; status : puint32; callback : TIOCallback; callbackData : pointer); - PPDeleteFileAsyncHook = procedure(volume : PStorage_Volume; filePath : pchar; status : puint32; callback : TIOCallback; callbackData : pointer); - PPDeleteDirAsyncHook = procedure(volume : PStorage_Volume; path : pchar; status : puint32; callback : TIOCallback; callbackData : pointer); { === I/O Request core.types === } @@ -233,15 +246,19 @@ type readOffsetCallback : PPReadOffsetHook; { Offset-based write for streaming mode — nil if not implemented } writeOffsetCallback : PPWriteOffsetHook; + { Async offset-based read/write for streaming mode — nil if not implemented } + readOffsetAsyncCallback : PPReadOffsetAsyncHook; + writeOffsetAsyncCallback : PPWriteOffsetAsyncHook; { File size query — returns size in bytes without loading the file } fileSizeCallback : PPFileSizeHook; { Async format — nil if FS only supports synchronous create } createAsyncCallback : PPCreateAsyncHook; createDirAsyncCallback : PPCreateDirAsyncHook; readDirAsyncCallback : PPReadDirAsyncHook; - deleteFileAsyncCallback : PPDeleteFileAsyncHook; - deleteDirAsyncCallback : PPDeleteDirAsyncHook; renameFileCallback : PPRenameFileHook; + { Per-file open/close — nil if FS does not cache per-file metadata } + openFileCallback : PPOpenFileHook; + closeFileCallback : PPCloseFileHook; end; { Generic storage volume } diff --git a/src/driver/storage/driver.storage.vfs.pas b/src/driver/storage/driver.storage.vfs.pas index ebbb68f1..2222a753 100644 --- a/src/driver/storage/driver.storage.vfs.pas +++ b/src/driver/storage/driver.storage.vfs.pas @@ -89,7 +89,7 @@ function CloseFile(Filehandle : TFileHandle) : TError; { Async public API — return immediately, callback fires on completion. Callers (e.g. LVGL callbacks) must keep all buffers alive until the callback fires. } -procedure WriteFileAsync(FileHandle : TFileHandle; Position : uint32; Buffer : puint8; Length : uint32; Callback : TIOCallback; CallbackData : pointer); +procedure WriteFileAsync(FileHandle : TFileHandle; Position : uint32; Buffer : puint8; Length : uint32; BytesWritten : puint32; Callback : TIOCallback; CallbackData : pointer); procedure ReadFileAsync(FileHandle : TFileHandle; Position : uint32; Buffer : puint8; Length : uint32; BytesRead : puint32; Callback : TIOCallback; CallbackData : pointer); procedure OpenFileAsync(Filename : pchar; OpenMode : TOpenMode; var OutHandle : TFileHandle; Error : PError; Callback : TIOCallback; CallbackData : pointer); procedure DeleteFileAsync(Path : pchar; Error : PError; Callback : TIOCallback; CallbackData : pointer); @@ -177,6 +177,43 @@ type end; PVFSDirAsyncCtx = ^TVFSDirAsyncCtx; + TVFSPathAsyncCtx = record + Path : pchar; + ErrorPtr : PError; + IsDirectory : boolean; + UserCallback : TIOCallback; + UserData : pointer; + end; + PVFSPathAsyncCtx = ^TVFSPathAsyncCtx; + + TVFSWriteAsyncCtx = record + FD : PFileDescriptor; + Position : uint32; + BytesWritten : uint32; + BytesWrittenPtr : puint32; + UserCallback : TIOCallback; + UserData : pointer; + end; + PVFSWriteAsyncCtx = ^TVFSWriteAsyncCtx; + + TVFSReadAsyncCtx = record + FD : PFileDescriptor; + BytesReadPtr : puint32; + UserCallback : TIOCallback; + UserData : pointer; + end; + PVFSReadAsyncCtx = ^TVFSReadAsyncCtx; + + TVFSSyncWaitCtx = record + Done : uint32; + Error : TError; + BytesDone : uint32; + Process : proc.types.PProcessContext; + UserCallback : TIOCallback; + UserData : pointer; + end; + PVFSSyncWaitCtx = ^TVFSSyncWaitCtx; + { Watch/Notify infrastructure } TVFSWatch = record Active : boolean; @@ -207,6 +244,56 @@ var DirCacheTick : uint32; DirCacheReady : boolean; +procedure vfs_write_complete(error : TError; userdata : pointer); +var + ctx : PVFSWriteAsyncCtx; +begin + ctx := PVFSWriteAsyncCtx(userdata); + if ctx = nil then exit; + + if (error = eNone) and (ctx^.FD <> nil) then begin + if ctx^.Position + ctx^.BytesWritten > ctx^.FD^.DataSize then + ctx^.FD^.DataSize := ctx^.Position + ctx^.BytesWritten; + end; + if ctx^.BytesWrittenPtr <> nil then + ctx^.BytesWrittenPtr^ := ctx^.BytesWritten; + + if ctx^.FD <> nil then + fd_end_async(ctx^.FD); + if ctx^.UserCallback <> nil then + ctx^.UserCallback(error, ctx^.UserData); + kfree(void(ctx)); +end; + +procedure vfs_pathop_worker(pctx : proc.types.PProcessContext); forward; + +procedure vfs_read_complete(error : TError; userdata : pointer); +var + ctx : PVFSReadAsyncCtx; +begin + ctx := PVFSReadAsyncCtx(userdata); + if ctx = nil then exit; + if ctx^.FD <> nil then + fd_end_async(ctx^.FD); + if ctx^.UserCallback <> nil then + ctx^.UserCallback(error, ctx^.UserData); + kfree(void(ctx)); +end; + +procedure vfs_sync_complete(error : TError; userdata : pointer); +var + ctx : PVFSSyncWaitCtx; +begin + ctx := PVFSSyncWaitCtx(userdata); + if ctx = nil then exit; + if ctx^.Process <> nil then + ctx^.Process^.State := psReady; + ctx^.Error := error; + if ctx^.UserCallback <> nil then + ctx^.UserCallback(error, ctx^.UserData); + puint32(@ctx^.Done)^ := 1; +end; + { ===================== Watch/Notify infrastructure ========================= } procedure InitWatchTable; @@ -1220,9 +1307,17 @@ begin fd^.VFSDir := vfsParentDir(Filename); fd^.OpenMode := uint8(ord(OpenMode)); fd^.DataSize := 0; + fd^.FSPrivate := nil; + fd^.FSCloseHook := nil; - { Query file size for read modes; for write-only just leave DataSize = 0 } - if (OpenMode = omRead) or (OpenMode = omReadWrite) then begin + { Cache per-file metadata and populate DataSize via FS open hook } + if (vol^.filesystem <> nil) and (vol^.filesystem^.openFileCallback <> nil) then begin + fd^.FSPrivate := vol^.filesystem^.openFileCallback(vol, dir, fname, fd^.DataSize); + if vol^.filesystem^.closeFileCallback <> nil then + fd^.FSCloseHook := pointer(vol^.filesystem^.closeFileCallback); + if Error <> nil then Error^ := eNone; + end else if (OpenMode = omRead) or (OpenMode = omReadWrite) then begin + { Fallback: query file size for read modes } if vol^.filesystem <> nil then begin if vol^.filesystem^.fileSizeCallback <> nil then begin fd^.DataSize := vol^.filesystem^.fileSizeCallback(vol, dir, fname); @@ -1252,88 +1347,73 @@ end; function WriteFile(FileHandle : TFileHandle; Position : uint32; Buffer : puint8; Length : uint32) : uint32; var - fd : PFileDescriptor; - vol : PStorage_Volume; + wait : TVFSSyncWaitCtx; begin debug.tracer.push_trace('driver.storage.vfs.WriteFile.enter'); WriteFile := 0; - fd := fd_get(currentFDTable, FileHandle); - if fd = nil then exit; + memset(uint32(@wait), 0, sizeof(TVFSSyncWaitCtx)); + wait.Error := eUnknown; + wait.Process := CurrentProcess; - if (TOpenMode(fd^.OpenMode) <> omWrite) and (TOpenMode(fd^.OpenMode) <> omCreate) and (TOpenMode(fd^.OpenMode) <> omReadWrite) then exit; - if Buffer = nil then exit; + WriteFileAsync(FileHandle, Position, Buffer, Length, @wait.BytesDone, @vfs_sync_complete, @wait); - { Device dispatch: use the device write callback } - if fd^.DeviceOps <> nil then begin - if PVFSDeviceOps(fd^.DeviceOps)^.Write <> nil then - WriteFile := PVFSDeviceOps(fd^.DeviceOps)^.Write(fd^.DeviceData, Position, Buffer, Length); - debug.tracer.push_trace('driver.storage.vfs.WriteFile.exit'); - exit; + asm pushf; cli end; + if puint32(@wait.Done)^ = 0 then begin + if CurrentProcess <> nil then + CurrentProcess^.State := psAwaiting; end; + asm popf end; - vol := fd^.Volume; - if vol = nil then exit; - if vol^.filesystem = nil then exit; - if vol^.filesystem^.writeOffsetCallback = nil then exit; + while puint32(@wait.Done)^ = 0 do + asm hlt end; - WriteFile := vol^.filesystem^.writeOffsetCallback( - vol, fd^.Directory, fd^.FileName, - Position, puint32(Buffer), Length); - if WriteFile > 0 then begin - { Update cached file size if write extended the file } - if Position + WriteFile > fd^.DataSize then - fd^.DataSize := Position + WriteFile; - VFS_OnMutation(weModified, fd^.VFSDir, fd^.VFSDir, vol, fd^.Directory); - end; + if wait.Error = eNone then + WriteFile := wait.BytesDone; debug.tracer.push_trace('driver.storage.vfs.WriteFile.exit'); end; function ReadFile(FileHandle : TFileHandle; Position : uint32; Buffer : puint8; Length : uint32) : uint32; var - fd : PFileDescriptor; - copyLen : uint32; + wait : TVFSSyncWaitCtx; begin debug.tracer.push_trace('driver.storage.vfs.ReadFile.enter'); ReadFile := 0; - fd := fd_get(currentFDTable, FileHandle); - if fd = nil then exit; + memset(uint32(@wait), 0, sizeof(TVFSSyncWaitCtx)); + wait.Error := eUnknown; + wait.Process := CurrentProcess; - { Device dispatch: use the device read callback } - if fd^.DeviceOps <> nil then begin - if PVFSDeviceOps(fd^.DeviceOps)^.Read = nil then exit; - if Buffer = nil then exit; - ReadFile := PVFSDeviceOps(fd^.DeviceOps)^.Read(fd^.DeviceData, Position, Buffer, Length); - debug.tracer.push_trace('driver.storage.vfs.ReadFile.exit'); - exit; + ReadFileAsync(FileHandle, Position, Buffer, Length, @wait.BytesDone, @vfs_sync_complete, @wait); + + asm pushf; cli end; + if puint32(@wait.Done)^ = 0 then begin + if CurrentProcess <> nil then + CurrentProcess^.State := psAwaiting; end; + asm popf end; - { On-demand read via readOffsetCallback } - if Buffer = nil then exit; - if fd^.Volume = nil then exit; - if fd^.Volume^.filesystem = nil then exit; - if fd^.Volume^.filesystem^.readOffsetCallback = nil then exit; - if Position >= fd^.DataSize then exit; + while puint32(@wait.Done)^ = 0 do + asm hlt end; - { Clamp read length to remaining file size } - copyLen := fd^.DataSize - Position; - if Length < copyLen then - copyLen := Length; - - if copyLen > 0 then - ReadFile := fd^.Volume^.filesystem^.readOffsetCallback( - fd^.Volume, fd^.Directory, fd^.FileName, - Position, puint32(Buffer), copyLen); + if wait.Error = eNone then + ReadFile := wait.BytesDone; debug.tracer.push_trace('driver.storage.vfs.ReadFile.exit'); end; function CloseFile(Filehandle : TFileHandle) : TError; +var + fd : PFileDescriptor; begin debug.tracer.push_trace('driver.storage.vfs.CloseFile.enter'); - if fd_close(currentFDTable, FileHandle) then + fd := fd_get(currentFDTable, FileHandle); + if fd = nil then + CloseFile := eInvalidHandle + else if fd_has_async(fd) then + CloseFile := eFileInUse + else if fd_close(currentFDTable, FileHandle) then CloseFile := eNone else CloseFile := eInvalidHandle; @@ -1364,15 +1444,17 @@ end; { === Async public API === } { WriteFileAsync — set up a write and return immediately. - driver.storage.fs.fat32 deep-copies the buffer on entry so the caller can free it after this returns. - Callback fires with eNone on success, or a TError code on failure. } -procedure WriteFileAsync(FileHandle : TFileHandle; Position : uint32; Buffer : puint8; Length : uint32; Callback : TIOCallback; CallbackData : pointer); + Callers must keep Buffer alive until Callback fires. + BytesWritten may be nil; when provided it is filled before Callback. } +procedure WriteFileAsync(FileHandle : TFileHandle; Position : uint32; Buffer : puint8; Length : uint32; BytesWritten : puint32; Callback : TIOCallback; CallbackData : pointer); var fd : PFileDescriptor; vol : PStorage_Volume; padSize : uint32; + ctx : PVFSWriteAsyncCtx; begin debug.tracer.push_trace('driver.storage.vfs.WriteFileAsync.enter'); + if BytesWritten <> nil then BytesWritten^ := 0; fd := fd_get(currentFDTable, FileHandle); if fd = nil then begin @@ -1396,6 +1478,7 @@ begin padSize := PVFSDeviceOps(fd^.DeviceOps)^.Write(fd^.DeviceData, Position, Buffer, Length) else padSize := 0; + if BytesWritten <> nil then BytesWritten^ := padSize; if Callback <> nil then Callback(eNone, CallbackData); debug.tracer.push_trace('driver.storage.vfs.WriteFileAsync.exit'); exit; @@ -1413,14 +1496,38 @@ begin finished and the fresh read will return the updated listing. } VFS_OnMutation(weModified, fd^.VFSDir, fd^.VFSDir, vol, fd^.Directory); - if vol^.filesystem^.writeOffsetCallback <> nil then begin + if vol^.filesystem^.writeOffsetAsyncCallback <> nil then begin + if not fd_begin_async(fd) then begin + if Callback <> nil then Callback(eInvalidHandle, CallbackData); + debug.tracer.push_trace('driver.storage.vfs.WriteFileAsync.exit'); + exit; + end; + ctx := PVFSWriteAsyncCtx(kalloc(sizeof(TVFSWriteAsyncCtx))); + if ctx = nil then begin + fd_end_async(fd); + if Callback <> nil then Callback(eOutOfMemory, CallbackData); + debug.tracer.push_trace('driver.storage.vfs.WriteFileAsync.exit'); + exit; + end; + memset(uint32(ctx), 0, sizeof(TVFSWriteAsyncCtx)); + ctx^.FD := fd; + ctx^.Position := Position; + ctx^.BytesWrittenPtr := BytesWritten; + ctx^.UserCallback := Callback; + ctx^.UserData := CallbackData; + vol^.filesystem^.writeOffsetAsyncCallback( + vol, fd^.Directory, fd^.FileName, + Position, puint32(Buffer), Length, fd^.FSPrivate, @ctx^.BytesWritten, + @vfs_write_complete, ctx); + end else if vol^.filesystem^.writeOffsetCallback <> nil then begin padSize := vol^.filesystem^.writeOffsetCallback( vol, fd^.Directory, fd^.FileName, - Position, puint32(Buffer), Length); + Position, puint32(Buffer), Length, fd^.FSPrivate); if padSize > 0 then begin if Position + padSize > fd^.DataSize then fd^.DataSize := Position + padSize; end; + if BytesWritten <> nil then BytesWritten^ := padSize; if Callback <> nil then Callback(eNone, CallbackData); end else begin if Callback <> nil then Callback(eNotSupported, CallbackData); @@ -1486,9 +1593,17 @@ begin fd^.VFSDir := vfsParentDir(Filename); fd^.OpenMode := uint8(ord(OpenMode)); fd^.DataSize := 0; + fd^.FSPrivate := nil; + fd^.FSCloseHook := nil; OutHandle := slot; - if (OpenMode = omRead) or (OpenMode = omReadWrite) then begin + if (vol^.filesystem <> nil) and (vol^.filesystem^.openFileCallback <> nil) then begin + fd^.FSPrivate := vol^.filesystem^.openFileCallback(vol, dir, fname, fd^.DataSize); + if vol^.filesystem^.closeFileCallback <> nil then + fd^.FSCloseHook := pointer(vol^.filesystem^.closeFileCallback); + if Error <> nil then Error^ := eNone; + if Callback <> nil then Callback(eNone, CallbackData); + end else if (OpenMode = omRead) or (OpenMode = omReadWrite) then begin if (vol^.filesystem <> nil) and (vol^.filesystem^.fileSizeCallback <> nil) then begin fd^.DataSize := vol^.filesystem^.fileSizeCallback(vol, dir, fname); if Error <> nil then Error^ := eNone; @@ -2168,6 +2283,7 @@ procedure ReadFileAsync(FileHandle : TFileHandle; Position : uint32; Buffer : pu var fd : PFileDescriptor; copyLen : uint32; + ctx : PVFSReadAsyncCtx; begin debug.tracer.push_trace('driver.storage.vfs.ReadFileAsync.enter'); @@ -2199,7 +2315,8 @@ begin { On-demand read via readOffsetCallback } if (fd^.Volume = nil) or (fd^.Volume^.filesystem = nil) - or (fd^.Volume^.filesystem^.readOffsetCallback = nil) then begin + or ((fd^.Volume^.filesystem^.readOffsetAsyncCallback = nil) + and (fd^.Volume^.filesystem^.readOffsetCallback = nil)) then begin if BytesRead <> nil then BytesRead^ := 0; if Callback <> nil then Callback(eNotSupported, CallbackData); exit; @@ -2215,13 +2332,37 @@ begin if Length < copyLen then copyLen := Length; - if copyLen > 0 then - copyLen := fd^.Volume^.filesystem^.readOffsetCallback( + if (copyLen > 0) and (fd^.Volume^.filesystem^.readOffsetAsyncCallback <> nil) then begin + if not fd_begin_async(fd) then begin + if BytesRead <> nil then BytesRead^ := 0; + if Callback <> nil then Callback(eInvalidHandle, CallbackData); + exit; + end; + ctx := PVFSReadAsyncCtx(kalloc(sizeof(TVFSReadAsyncCtx))); + if ctx = nil then begin + fd_end_async(fd); + if BytesRead <> nil then BytesRead^ := 0; + if Callback <> nil then Callback(eOutOfMemory, CallbackData); + exit; + end; + memset(uint32(ctx), 0, sizeof(TVFSReadAsyncCtx)); + ctx^.FD := fd; + ctx^.BytesReadPtr := BytesRead; + ctx^.UserCallback := Callback; + ctx^.UserData := CallbackData; + fd^.Volume^.filesystem^.readOffsetAsyncCallback( fd^.Volume, fd^.Directory, fd^.FileName, - Position, puint32(Buffer), copyLen); + Position, puint32(Buffer), copyLen, fd^.FSPrivate, BytesRead, + @vfs_read_complete, ctx); + end else begin + if copyLen > 0 then + copyLen := fd^.Volume^.filesystem^.readOffsetCallback( + fd^.Volume, fd^.Directory, fd^.FileName, + Position, puint32(Buffer), copyLen, fd^.FSPrivate); - if BytesRead <> nil then BytesRead^ := copyLen; - if Callback <> nil then Callback(eNone, CallbackData); + if BytesRead <> nil then BytesRead^ := copyLen; + if Callback <> nil then Callback(eNone, CallbackData); + end; debug.tracer.push_trace('driver.storage.vfs.ReadFileAsync.exit'); end; @@ -2229,13 +2370,7 @@ end; procedure DeleteFileAsync(Path : pchar; Error : PError; Callback : TIOCallback; CallbackData : pointer); var - vol : PStorage_Volume; - dir : pchar; - fname : pchar; - fullPath : pchar; - tmpPath : pchar; - status : puint32; - errCode : TError; + ctx : PVFSPathAsyncCtx; begin debug.tracer.push_trace('driver.storage.vfs.DeleteFileAsync.enter'); if Error <> nil then Error^ := eUnknown; @@ -2246,74 +2381,32 @@ begin exit; end; - if not ResolveFilePath(Path, PPStorage_Volume(@vol), PPChar(@dir), PPChar(@fname)) then begin - if Error <> nil then Error^ := eInvalidPath; - if Callback <> nil then Callback(eInvalidPath, CallbackData); + ctx := PVFSPathAsyncCtx(kalloc(sizeof(TVFSPathAsyncCtx))); + if ctx = nil then begin + if Error <> nil then Error^ := eOutOfMemory; + if Callback <> nil then Callback(eOutOfMemory, CallbackData); exit; end; - - if (vol^.filesystem = nil) then begin - if Error <> nil then Error^ := eNotSupported; - kfree(void(dir)); - kfree(void(fname)); - if Callback <> nil then Callback(eNotSupported, CallbackData); + memset(uint32(ctx), 0, sizeof(TVFSPathAsyncCtx)); + ctx^.Path := stringCopy(Path); + if ctx^.Path = nil then begin + kfree(void(ctx)); + if Error <> nil then Error^ := eOutOfMemory; + if Callback <> nil then Callback(eOutOfMemory, CallbackData); exit; end; - - { Build full relative path } - if (dir <> nil) and (stringSize(dir) > 0) then begin - tmpPath := stringConcat(dir, '/'); - fullPath := stringConcat(tmpPath, fname); - kfree(void(tmpPath)); - kfree(void(dir)); - dir := nil; - end else begin - if dir <> nil then kfree(void(dir)); - fullPath := stringCopy(fname); - end; - - { Try async hook first, then sync fallback } - if vol^.filesystem^.deleteFileAsyncCallback <> nil then begin - status := puint32(kalloc(4)); - status^ := 0; - vol^.filesystem^.deleteFileAsyncCallback(vol, fullPath, status, Callback, CallbackData); - { Note: status/fullPath/fname ownership — async callback must handle. - Since no FS currently implements this async hook, this is a placeholder. } - kfree(puint32(status)); - kfree(void(fullPath)); - kfree(void(fname)); - end else if vol^.filesystem^.deleteFileCallback <> nil then begin - status := puint32(kalloc(4)); - status^ := 0; - vol^.filesystem^.deleteFileCallback(vol, fullPath, status); - errCode := TError(status^); - if Error <> nil then Error^ := errCode; - { Fire watch event on success } - if errCode = eNone then - VFS_OnMutation(weDeleted, Path, Path, vol, fullPath); - kfree(puint32(status)); - kfree(void(fullPath)); - kfree(void(fname)); - if Callback <> nil then Callback(errCode, CallbackData); - end else begin - if Error <> nil then Error^ := eNotSupported; - kfree(void(fullPath)); - kfree(void(fname)); - if Callback <> nil then Callback(eNotSupported, CallbackData); - end; + ctx^.ErrorPtr := Error; + ctx^.IsDirectory := false; + ctx^.UserCallback := Callback; + ctx^.UserData := CallbackData; + proc.mgr.create('vfs.delf', @vfs_pathop_worker, void(ctx), 1); debug.tracer.push_trace('driver.storage.vfs.DeleteFileAsync.exit'); end; procedure DeleteDirectoryAsync(Path : pchar; Error : PError; Callback : TIOCallback; CallbackData : pointer); var - vol : PStorage_Volume; - dir : pchar; - dirName : pchar; - fullPath : pchar; - tmpPath : pchar; - status : puint32; - errCode : TError; + ctx : PVFSPathAsyncCtx; begin debug.tracer.push_trace('driver.storage.vfs.DeleteDirectoryAsync.enter'); if Error <> nil then Error^ := eUnknown; @@ -2324,58 +2417,25 @@ begin exit; end; - if not ResolveFilePath(Path, PPStorage_Volume(@vol), PPChar(@dir), PPChar(@dirName)) then begin - if Error <> nil then Error^ := eInvalidPath; - if Callback <> nil then Callback(eInvalidPath, CallbackData); + ctx := PVFSPathAsyncCtx(kalloc(sizeof(TVFSPathAsyncCtx))); + if ctx = nil then begin + if Error <> nil then Error^ := eOutOfMemory; + if Callback <> nil then Callback(eOutOfMemory, CallbackData); exit; end; - - if (vol^.filesystem = nil) then begin - if Error <> nil then Error^ := eNotSupported; - kfree(void(dir)); - kfree(void(dirName)); - if Callback <> nil then Callback(eNotSupported, CallbackData); + memset(uint32(ctx), 0, sizeof(TVFSPathAsyncCtx)); + ctx^.Path := stringCopy(Path); + if ctx^.Path = nil then begin + kfree(void(ctx)); + if Error <> nil then Error^ := eOutOfMemory; + if Callback <> nil then Callback(eOutOfMemory, CallbackData); exit; end; - - { Build full relative path } - if (dir <> nil) and (stringSize(dir) > 0) then begin - tmpPath := stringConcat(dir, '/'); - fullPath := stringConcat(tmpPath, dirName); - kfree(void(tmpPath)); - kfree(void(dir)); - dir := nil; - end else begin - if dir <> nil then kfree(void(dir)); - fullPath := stringCopy(dirName); - end; - - { Try async hook first, then sync fallback } - if vol^.filesystem^.deleteDirAsyncCallback <> nil then begin - status := puint32(kalloc(4)); - status^ := 0; - vol^.filesystem^.deleteDirAsyncCallback(vol, fullPath, status, Callback, CallbackData); - kfree(puint32(status)); - kfree(void(fullPath)); - kfree(void(dirName)); - end else if vol^.filesystem^.deleteDirCallback <> nil then begin - status := puint32(kalloc(4)); - status^ := 0; - vol^.filesystem^.deleteDirCallback(vol, fullPath, status); - errCode := TError(status^); - if Error <> nil then Error^ := errCode; - if errCode = eNone then - VFS_OnMutation(weDeleted, Path, Path, vol, fullPath); - kfree(puint32(status)); - kfree(void(fullPath)); - kfree(void(dirName)); - if Callback <> nil then Callback(errCode, CallbackData); - end else begin - if Error <> nil then Error^ := eNotSupported; - kfree(void(fullPath)); - kfree(void(dirName)); - if Callback <> nil then Callback(eNotSupported, CallbackData); - end; + ctx^.ErrorPtr := Error; + ctx^.IsDirectory := true; + ctx^.UserCallback := Callback; + ctx^.UserData := CallbackData; + proc.mgr.create('vfs.deld', @vfs_pathop_worker, void(ctx), 1); debug.tracer.push_trace('driver.storage.vfs.DeleteDirectoryAsync.exit'); end; @@ -2410,6 +2470,30 @@ begin proc.mgr.proc_exit(0); end; +procedure vfs_pathop_worker(pctx : proc.types.PProcessContext); +var + ctx : PVFSPathAsyncCtx; + err : TError; +begin + ctx := PVFSPathAsyncCtx(pctx^.Local); + if ctx = nil then begin + proc.mgr.proc_exit(1); + exit; + end; + + if ctx^.IsDirectory then + err := DeleteDirectory(ctx^.Path, ctx^.ErrorPtr) + else + err := DeleteFile(ctx^.Path, ctx^.ErrorPtr); + + if ctx^.UserCallback <> nil then + ctx^.UserCallback(err, ctx^.UserData); + if ctx^.Path <> nil then + kfree(void(ctx^.Path)); + kfree(void(ctx)); + proc.mgr.proc_exit(0); +end; + procedure GetDirectoryListingAsync(Path : pchar; ResultMap : PPHashMap; Callback : TIOCallback; CallbackData : pointer); var ctx : PVFSDirAsyncCtx; @@ -3286,7 +3370,7 @@ begin if mntLen > 0 then begin mntPath := pchar(kalloc(mntLen + 1)); - readErr := vol^.filesystem^.readOffsetCallback(vol, '', 'MOUNT.ASR', 0, puint32(mntPath), mntLen); + readErr := vol^.filesystem^.readOffsetCallback(vol, '', 'MOUNT.ASR', 0, puint32(mntPath), mntLen, nil); mntPath[mntLen] := char(0); if readErr > 0 then begin @@ -3881,7 +3965,7 @@ begin { /dev/null — write via WriteFileAsync } devHandle := OpenFile('/dev/null', omWrite, @devErr); Assert((devHandle <> 0) and (devErr = eNone), 'OpenFile /dev/null write async ok'); - WriteFileAsync(devHandle, 0, devBuf, 16, nil, nil); + WriteFileAsync(devHandle, 0, devBuf, 16, nil, nil, nil); CloseFile(devHandle); { /dev/zero — write: accepted (discards data) } @@ -3904,4 +3988,4 @@ initialization boot.mgr.registerBoot('driver.storage.vfs', @init, 'Virtual File System', BOOT_MGR_BARRIER_STORAGE); boot.mgr.registerBoot('driver.storage.vol.mount', @auto_mount_volumes, 'Mount Storage Volumes', BOOT_MGR_BARRIER_LATE); -end. \ No newline at end of file +end. diff --git a/src/driver/storage/fs/driver.storage.fs.fat32.pas b/src/driver/storage/fs/driver.storage.fs.fat32.pas index 50d75947..93aa9e15 100644 --- a/src/driver/storage/fs/driver.storage.fs.fat32.pas +++ b/src/driver/storage/fs/driver.storage.fs.fat32.pas @@ -107,11 +107,24 @@ type end; PFatVolumeInfo = ^TFatVolumeInfo; + TClusterExtent = record + StartCluster : uint32; + ClusterCount : uint32; + end; + PClusterExtent = ^TClusterExtent; + const - { FAT sector cache — 2-way set-associative, 128 sets, LRU replacement } - FAT_CACHE_SETS = 128; + { FAT sector cache — 2-way set-associative, 256 sets, LRU replacement. + This keeps small-cluster sequential files from churning the FAT cache + once they grow beyond a few megabytes. } + FAT_CACHE_SETS = 256; FAT_CACHE_WAYS = 2; - FAT_CACHE_LINES = 256; { SETS * WAYS } + FAT_CACHE_LINES = 512; { SETS * WAYS } + FAT_TRANSFER_POOL_CAPACITY = 64; + FAT_WORK_QUEUE_CAPACITY = FAT_TRANSFER_POOL_CAPACITY; + FAT_WRITE_PREALLOC_BYTES = 8 * 1024 * 1024; + FAT_WRITE_PREALLOC_MAX_CLUSTERS = 2048; + FAT_WRITE_PREALLOC_MIN_CLUSTERS = 64; type TFATCacheLine = record @@ -124,7 +137,7 @@ type PFATCache = ^TFATCache; TFATCache = record Lines : array[0..FAT_CACHE_LINES - 1] of TFATCacheLine; - Data : puint32; { kalloc'd 256*512 = 128 KB of FAT sector data } + Data : puint32; { kalloc'd FAT_CACHE_LINES * 512 bytes of FAT sector data } EvictBuf : puint32; { kalloc'd 512-byte scratch for eviction writes } Busy : boolean; { spinlock: serializes miss-path eviction+load } FatStart : uint32; { LBA of first FAT sector on disk } @@ -132,6 +145,82 @@ type BootRecord : PBootRecord; { cached boot record (read once) } end; + TFATAsyncMode = (famRead, famWrite); + + TFATTransferState = ( + ftsIdle, + ftsPrepare, + ftsIssueIO, + ftsAwaitIO, + ftsAdvance, + ftsComplete, + ftsError + ); + + { Per-file cached metadata created by fat32OpenFile and stored in the + file descriptor's FSPrivate pointer. Avoids re-reading boot record, + directory listing, and searching for the file on every read/write. } + PFATOpenFile = ^TFATOpenFile; + PFATVolumeCtx = ^TFATVolumeCtx; + PFATFileIOAsyncCtx = ^TFATFileIOAsyncCtx; + + TFATOpenFile = record + Volume : PStorage_Volume; + BootRecord : PBootRecord; { borrows from TFATCache — do NOT kfree } + DataStart : uint32; { fatDataStartLBA result } + FirstCluster : uint32; { file's start cluster (0 if new/empty) } + ByteSize : uint32; { current file size in bytes } + AllocClusters: uint32; { current reserved cluster count for appends } + Extents : PLinkedListBase;{ cached cluster extents for fast sequential I/O } + DirCluster : uint32; { parent directory's start cluster } + CleanName : byteArray8; { cleaned 8.3 filename } + ExtPart : pchar; { heap-allocated extension string — owned } + ScratchSector: puint32; { per-open-file sector scratch buffer } + ScratchSize : uint32; { size of ScratchSector in bytes } + Exists : boolean; { true if file existed at open time } + FatDirty : boolean; { true if FAT cache must be flushed on close } + MetaDirty : boolean; { true if dir entry size/cluster needs sync on close } + end; + + TFATVolumeCtx = record + Cache : TFATCache; + TransferPoolBuf : pointer; + TransferFreeList : pointer; + Worker : proc.types.PProcessContext; + WorkHead : uint32; + WorkTail : uint32; + WorkCount : uint32; + WorkQueue : array[0..FAT_WORK_QUEUE_CAPACITY - 1] of PFATFileIOAsyncCtx; + end; + + TFATFileIOAsyncCtx = record + Next : PFATFileIOAsyncCtx; + VolumeCtx : PFATVolumeCtx; + State : TFATTransferState; + Mode : TFATAsyncMode; + Volume : PStorage_Volume; + OpenFile : PFATOpenFile; + Buffer : puint32; + Offset : uint32; + ByteCount : uint32; + BytesDone : uint32; + BytesOut : puint32; + Callback : TIOCallback; + CallbackData : pointer; + Cluster : uint32; + OrigByteSize : uint32; + DataStart : uint32; + BootRecord : PBootRecord; + ChunkBytes : uint32; + LastError : TError; + CursorValid : boolean; + CursorIndex : uint32; + CursorCluster: uint32; + ExtentValid : boolean; + ExtentIdx : uint32; + ExtentStartIdx : uint32; + end; + { Async format state machine steps } TFmtStep = ( fmtBootSector, @@ -161,21 +250,6 @@ type CallbackData : pointer; end; - { Per-file cached metadata created by fat32OpenFile and stored in the - file descriptor's FSPrivate pointer. Avoids re-reading boot record, - directory listing, and searching for the file on every read/write. } - PFATOpenFile = ^TFATOpenFile; - TFATOpenFile = record - BootRecord : PBootRecord; { borrows from TFATCache — do NOT kfree } - DataStart : uint32; { fatDataStartLBA result } - FirstCluster : uint32; { file's start cluster (0 if new/empty) } - ByteSize : uint32; { current file size in bytes } - DirCluster : uint32; { parent directory's start cluster } - CleanName : byteArray8; { cleaned 8.3 filename } - ExtPart : pchar; { heap-allocated extension string — owned } - Exists : boolean; { true if file existed at open time } - end; - var filesystem : TFilesystem; @@ -376,36 +450,227 @@ end; { ---- FAT sector cache ---- } -{ Get or lazily create the per-volume FAT cache. - Stores the cache pointer in volume^.fsPrivate. } -function fatCacheGet(volume : PStorage_Volume) : PFATCache; +{ Get or lazily create the per-volume FAT32 context. + Stores the context pointer in volume^.fsPrivate. } +function fatVolumeCtxGet(volume : PStorage_Volume) : PFATVolumeCtx; var - cache : PFATCache; - br : PBootRecord; - i : uint32; + ctx : PFATVolumeCtx; + br : PBootRecord; + i : uint32; + base : uint32; + tx : PFATFileIOAsyncCtx; begin + fatVolumeCtxGet := nil; + if volume = nil then exit; + if volume^.fsPrivate <> nil then begin - fatCacheGet := PFATCache(volume^.fsPrivate); + fatVolumeCtxGet := PFATVolumeCtx(volume^.fsPrivate); exit; end; - cache := PFATCache(kalloc(sizeof(TFATCache))); - memset(uint32(cache), 0, sizeof(TFATCache)); - cache^.Data := puint32(kalloc(FAT_CACHE_LINES * 512)); - memset(uint32(cache^.Data), 0, FAT_CACHE_LINES * 512); - cache^.EvictBuf := puint32(kalloc(512)); - memset(uint32(cache^.EvictBuf), 0, 512); - cache^.Busy := false; - for i := 0 to FAT_CACHE_LINES - 1 do begin - cache^.Lines[i].Valid := false; - cache^.Lines[i].Dirty := false; - cache^.Lines[i].LRU := uint8(i mod FAT_CACHE_WAYS); + + ctx := PFATVolumeCtx(kalloc(sizeof(TFATVolumeCtx))); + if ctx = nil then exit; + memset(uint32(ctx), 0, sizeof(TFATVolumeCtx)); + + ctx^.Cache.Data := puint32(kalloc(FAT_CACHE_LINES * 512)); + if ctx^.Cache.Data = nil then begin + kfree(puint32(ctx)); + exit; end; + memset(uint32(ctx^.Cache.Data), 0, FAT_CACHE_LINES * 512); + + ctx^.Cache.EvictBuf := puint32(kalloc(512)); + if ctx^.Cache.EvictBuf = nil then begin + kfree(ctx^.Cache.Data); + kfree(puint32(ctx)); + exit; + end; + memset(uint32(ctx^.Cache.EvictBuf), 0, 512); + + ctx^.Cache.Busy := false; + for i := 0 to FAT_CACHE_LINES - 1 do begin + ctx^.Cache.Lines[i].Valid := false; + ctx^.Cache.Lines[i].Dirty := false; + ctx^.Cache.Lines[i].LRU := uint8(i mod FAT_CACHE_WAYS); + end; + br := readBootRecord(volume); - cache^.BootRecord := br; - cache^.FatStart := volume^.sectorStart + 1 + br^.rsvSectors; - cache^.Device := volume^.device; - volume^.fsPrivate := pointer(cache); - fatCacheGet := cache; + ctx^.Cache.BootRecord := br; + ctx^.Cache.FatStart := volume^.sectorStart + 1 + br^.rsvSectors; + ctx^.Cache.Device := volume^.device; + + ctx^.TransferPoolBuf := kalloc(FAT_TRANSFER_POOL_CAPACITY * sizeof(TFATFileIOAsyncCtx)); + if ctx^.TransferPoolBuf <> nil then begin + base := uint32(ctx^.TransferPoolBuf); + if FAT_TRANSFER_POOL_CAPACITY > 0 then + for i := 0 to FAT_TRANSFER_POOL_CAPACITY - 1 do begin + tx := PFATFileIOAsyncCtx(base + (i * sizeof(TFATFileIOAsyncCtx))); + tx^.Next := PFATFileIOAsyncCtx(ctx^.TransferFreeList); + tx^.VolumeCtx := ctx; + tx^.State := ftsIdle; + ctx^.TransferFreeList := pointer(tx); + end; + end; + + volume^.fsPrivate := pointer(ctx); + fatVolumeCtxGet := ctx; +end; + +{ Get or lazily create the per-volume FAT cache. } +function fatCacheGet(volume : PStorage_Volume) : PFATCache; +var + ctx : PFATVolumeCtx; +begin + ctx := fatVolumeCtxGet(volume); + if ctx = nil then + fatCacheGet := nil + else + fatCacheGet := @ctx^.Cache; +end; + +function fatTransferAlloc(volume : PStorage_Volume) : PFATFileIOAsyncCtx; +var + ctx : PFATVolumeCtx; + transfer : PFATFileIOAsyncCtx; +begin + fatTransferAlloc := nil; + ctx := fatVolumeCtxGet(volume); + if ctx = nil then exit; + + asm pushf; cli end; + transfer := PFATFileIOAsyncCtx(ctx^.TransferFreeList); + if transfer <> nil then + ctx^.TransferFreeList := pointer(transfer^.Next); + asm popf end; + + if transfer = nil then exit; + + memset(uint32(transfer), 0, sizeof(TFATFileIOAsyncCtx)); + transfer^.VolumeCtx := ctx; + transfer^.State := ftsIdle; + fatTransferAlloc := transfer; +end; + +procedure fatTransferFree(ctx : PFATFileIOAsyncCtx); +var + volCtx : PFATVolumeCtx; +begin + if ctx = nil then exit; + volCtx := ctx^.VolumeCtx; + if volCtx = nil then exit; + + asm pushf; cli end; + ctx^.Next := PFATFileIOAsyncCtx(volCtx^.TransferFreeList); + volCtx^.TransferFreeList := pointer(ctx); + asm popf end; +end; + +function fatWorkerQueuePush(volCtx : PFATVolumeCtx; transfer : PFATFileIOAsyncCtx) : boolean; +begin + fatWorkerQueuePush := false; + if (volCtx = nil) or (transfer = nil) then exit; + + asm pushf; cli end; + if volCtx^.WorkCount < FAT_WORK_QUEUE_CAPACITY then begin + volCtx^.WorkQueue[volCtx^.WorkTail] := transfer; + volCtx^.WorkTail := (volCtx^.WorkTail + 1) mod FAT_WORK_QUEUE_CAPACITY; + volCtx^.WorkCount := volCtx^.WorkCount + 1; + fatWorkerQueuePush := true; + end; + asm popf end; +end; + +function fatWorkerQueuePop(volCtx : PFATVolumeCtx; var transfer : PFATFileIOAsyncCtx) : boolean; +begin + fatWorkerQueuePop := false; + transfer := nil; + if volCtx = nil then exit; + + asm pushf; cli end; + if volCtx^.WorkCount > 0 then begin + transfer := volCtx^.WorkQueue[volCtx^.WorkHead]; + volCtx^.WorkQueue[volCtx^.WorkHead] := nil; + volCtx^.WorkHead := (volCtx^.WorkHead + 1) mod FAT_WORK_QUEUE_CAPACITY; + volCtx^.WorkCount := volCtx^.WorkCount - 1; + fatWorkerQueuePop := true; + end; + asm popf end; +end; + +procedure fatWorkerWake(volCtx : PFATVolumeCtx); +begin + if (volCtx = nil) or (volCtx^.Worker = nil) then exit; + + asm pushf; cli end; + volCtx^.Worker^.PendingMsg := smCustom; + volCtx^.Worker^.MsgData := nil; + if volCtx^.Worker^.State = psAwaiting then + volCtx^.Worker^.State := psReady; + asm popf end; +end; + +procedure fat32_fileio_async_run_next(ctx : PFATFileIOAsyncCtx); forward; + +procedure fat32_transfer_worker(pctx : proc.types.PProcessContext); +var + volCtx : PFATVolumeCtx; + transfer : PFATFileIOAsyncCtx; +begin + volCtx := PFATVolumeCtx(pctx^.Local); + if volCtx = nil then begin + proc.mgr.proc_exit(1); + exit; + end; + + while true do begin + if pctx^.PendingMsg = smTerminate then begin + pctx^.PendingMsg := smNone; + proc.mgr.proc_exit(0); + exit; + end; + + if fatWorkerQueuePop(volCtx, transfer) then begin + fat32_fileio_async_run_next(transfer); + continue; + end; + + asm pushf; cli end; + if volCtx^.WorkCount = 0 then begin + pctx^.PendingMsg := smNone; + pctx^.State := psAwaiting; + end; + asm popf end; + + if pctx^.State = psAwaiting then + proc.mgr.proc_await; + end; +end; + +procedure fatWorkerEnsure(volume : PStorage_Volume); +var + volCtx : PFATVolumeCtx; +begin + volCtx := fatVolumeCtxGet(volume); + if volCtx = nil then exit; + if volCtx^.Worker <> nil then exit; + volCtx^.Worker := proc.mgr.create('fat32.xfer', @fat32_transfer_worker, void(volCtx), 1); +end; + +function fatScheduleTransfer(ctx : PFATFileIOAsyncCtx) : boolean; +var + volCtx : PFATVolumeCtx; +begin + fatScheduleTransfer := false; + if ctx = nil then exit; + + volCtx := ctx^.VolumeCtx; + if volCtx = nil then exit; + if volCtx^.Worker = nil then exit; + + if fatWorkerQueuePush(volCtx, ctx) then begin + fatWorkerWake(volCtx); + fatScheduleTransfer := true; + end; end; { Flush all dirty cache lines to disk. @@ -470,7 +735,7 @@ begin end; { Read a single FAT entry using the sector cache. - 128 FAT entries per 512-byte sector. 2-way set-associative: set = fatSector mod 128. + 128 FAT entries per 512-byte sector. 2-way set-associative: set = fatSector mod FAT_CACHE_SETS. CLI/STI protects cache metadata; disk I/O runs with interrupts enabled. } function readFat(volume : PStorage_volume; cluster : uint32; bootRecord : PBootRecord) : uint32; var @@ -570,7 +835,7 @@ begin end; { Write a single FAT entry into cache. No immediate disk I/O — call fatCacheFlush later. - 2-way set-associative: set = fatSector mod 128. + 2-way set-associative: set = fatSector mod FAT_CACHE_SETS. CLI/STI protects cache metadata; disk I/O runs with interrupts enabled. } procedure writeFat(volume : PStorage_volume; cluster : uint32; value : uint32; bootRecord : PBootRecord); var @@ -686,12 +951,21 @@ var currentClusterValue : uint32; clusters : PLinkedListBase; dirElm : puint32; + maxCluster : uint32; + iterCount : uint32; begin clusters:= LL_New(sizeof(uint32)); currentCluster:= cluster; currentClusterValue:= cluster; + if bootRecord^.sectorSize > 0 then + maxCluster := (bootRecord^.FATSize * bootRecord^.sectorSize) div 4 + else + maxCluster := 0; + iterCount := 0; while true do begin + if (maxCluster > 0) and (iterCount >= maxCluster) then + break; currentClusterValue:= readFat(volume, currentClusterValue, bootRecord); if (currentClusterValue and $0FFFFFFF) = $0FFFFFF7 then begin @@ -708,74 +982,415 @@ begin end; currentCluster := currentClusterValue; + iterCount := iterCount + 1; end; getFatChain:= clusters; end; -//TODO improve with FSINFO -function findFreeClusters(volume : PStorage_volume; amount : uint32; bootRecord : PBootRecord) : PLinkedListBase; -var - i : uint32; - currentClusterValue : uint32; - currentAmount : uint32 = 0; - maxCluster : uint32; - clusters : PLinkedListBase; - dirElm : puint32; +function fatMaxClusterCount(bootRecord : PBootRecord) : uint32; begin - clusters := LL_New(8); - - { Calculate total number of data clusters on this volume } - if bootRecord^.sectorsize > 0 then - maxCluster := (bootRecord^.FATSize * bootRecord^.sectorsize) div 4 + if (bootRecord <> nil) and (bootRecord^.sectorSize > 0) then + fatMaxClusterCount := (bootRecord^.FATSize * bootRecord^.sectorSize) div 4 else - maxCluster := 0; + fatMaxClusterCount := 0; +end; - { Start from allocation hint if valid for this volume } - if (sioc_allocVol = volume) and (sioc_allocHint >= 2) and (sioc_allocHint < maxCluster) then - i := sioc_allocHint - else - i := 2; - - while (i < maxCluster) do begin - if currentAmount = amount then break; - currentClusterValue:= readFat(volume, i, bootRecord); - if currentClusterValue = 0 then begin - dirElm:= LL_add(clusters); - dirElm^:= i; - currentAmount+=1; - end; - i+=1; - end; - - { Fallback: if hint caused us to miss free clusters, scan from 2 } - if (currentAmount < amount) and (sioc_allocVol = volume) and (sioc_allocHint > 2) then begin - i := 2; - while (i < sioc_allocHint) and (currentAmount < amount) do begin - currentClusterValue := readFat(volume, i, bootRecord); - if currentClusterValue = 0 then begin - dirElm := LL_add(clusters); - dirElm^ := i; - currentAmount += 1; - end; - i += 1; - end; - end; - - { Update allocation hint for next call } - if currentAmount > 0 then begin - sioc_allocHint := puint32(LL_Get(clusters, currentAmount - 1))^ + 1; - sioc_allocVol := volume; - end; - - { If we couldn't find enough free clusters, the disk is full } - if currentAmount < amount then begin - LL_Free(clusters); - findFreeClusters := nil; +function fatDefaultSPC(sectors : uint32; sectorSize : uint32) : uint32; +begin + if sectorSize = 0 then begin + fatDefaultSPC := 1; exit; end; - findFreeClusters:= clusters; + { Default to 4 KiB clusters for sane FAT32 throughput without excessive + slack on small files. Callers can still override via config. } + fatDefaultSPC := 4096 div sectorSize; + if fatDefaultSPC = 0 then + fatDefaultSPC := 1; + if fatDefaultSPC > 64 then + fatDefaultSPC := 64; +end; + +function fatComputeFATSize(sectors : uint32; spc : uint32; sectorSize : uint32) : uint32; +var + clusterCount : uint32; + fatBytes : uint32; +begin + if spc = 0 then + spc := 1; + if sectorSize = 0 then begin + fatComputeFATSize := 0; + exit; + end; + + clusterCount := (sectors + spc - 1) div spc; + fatBytes := clusterCount * 4; + fatComputeFATSize := (fatBytes + sectorSize - 1) div sectorSize; + if fatComputeFATSize = 0 then + fatComputeFATSize := 1; +end; + +function fatClustersForBytes(byteCount : uint32; bytesPerCluster : uint32) : uint32; +begin + if (byteCount = 0) or (bytesPerCluster = 0) then begin + fatClustersForBytes := 0; + exit; + end; + + fatClustersForBytes := (byteCount + bytesPerCluster - 1) div bytesPerCluster; +end; + +function fatPreallocClusterCount(bytesPerCluster : uint32) : uint32; +begin + if bytesPerCluster = 0 then begin + fatPreallocClusterCount := 1; + exit; + end; + + fatPreallocClusterCount := FAT_WRITE_PREALLOC_BYTES div bytesPerCluster; + if fatPreallocClusterCount = 0 then + fatPreallocClusterCount := 1; + if fatPreallocClusterCount < FAT_WRITE_PREALLOC_MIN_CLUSTERS then + fatPreallocClusterCount := FAT_WRITE_PREALLOC_MIN_CLUSTERS; + if fatPreallocClusterCount > FAT_WRITE_PREALLOC_MAX_CLUSTERS then + fatPreallocClusterCount := FAT_WRITE_PREALLOC_MAX_CLUSTERS; +end; + +procedure fatUpdateAllocHint(volume : PStorage_volume; bootRecord : PBootRecord; nextCluster : uint32); +var + maxCluster : uint32; +begin + maxCluster := fatMaxClusterCount(bootRecord); + if maxCluster <= 2 then begin + sioc_allocHint := 2; + sioc_allocVol := volume; + exit; + end; + + if nextCluster < 2 then + nextCluster := 2; + if nextCluster >= maxCluster then + nextCluster := 2; + + sioc_allocHint := nextCluster; + sioc_allocVol := volume; +end; + +function fatTryContiguousRange(volume : PStorage_volume; bootRecord : PBootRecord; + rangeStart : uint32; rangeEnd : uint32; needed : uint32; + var foundStart : uint32) : boolean; +var + scanPos : uint32; + runLen : uint32; + runPos : uint32; + fatVal : uint32; +begin + fatTryContiguousRange := false; + if (needed = 0) or (bootRecord = nil) or (rangeStart >= rangeEnd) then + exit; + + scanPos := rangeStart; + runLen := 0; + runPos := 0; + + while scanPos < rangeEnd do begin + fatVal := readFat(volume, scanPos, bootRecord); + if fatVal = 0 then begin + if runLen = 0 then + runPos := scanPos; + runLen := runLen + 1; + if runLen >= needed then begin + foundStart := runPos; + fatTryContiguousRange := true; + exit; + end; + end else + runLen := 0; + scanPos := scanPos + 1; + end; +end; + +function fatFindContiguousFreeRange(volume : PStorage_volume; amount : uint32; + bootRecord : PBootRecord; var runStart : uint32) : boolean; +var + scanStart : uint32; + maxCluster : uint32; +begin + fatFindContiguousFreeRange := false; + if amount = 0 then exit; + + maxCluster := fatMaxClusterCount(bootRecord); + if maxCluster <= 2 then exit; + + if (sioc_allocVol = volume) and (sioc_allocHint >= 2) and (sioc_allocHint < maxCluster) then + scanStart := sioc_allocHint + else + scanStart := 2; + + if fatTryContiguousRange(volume, bootRecord, scanStart, maxCluster, amount, runStart) or + ((scanStart > 2) and fatTryContiguousRange(volume, bootRecord, 2, scanStart, amount, runStart)) then + fatFindContiguousFreeRange := true; +end; + +procedure fatLinkContiguousRange(volume : PStorage_volume; bootRecord : PBootRecord; + previousTail : uint32; firstCluster : uint32; clusterCount : uint32; + var lastCluster : uint32); +var + cur : uint32; + i : uint32; +begin + lastCluster := previousTail; + if clusterCount = 0 then exit; + + if previousTail <> 0 then + writeFat(volume, previousTail, firstCluster, bootRecord); + + cur := firstCluster; + i := 1; + while i < clusterCount do begin + writeFat(volume, cur, cur + 1, bootRecord); + cur := cur + 1; + i := i + 1; + end; + + writeFat(volume, cur, $FFFFFFF8, bootRecord); + lastCluster := cur; + fatUpdateAllocHint(volume, bootRecord, lastCluster + 1); +end; + +function fatAppendExtent(extents : PLinkedListBase; firstCluster : uint32; + clusterCount : uint32) : boolean; +var + ext : PClusterExtent; + lastExt : PClusterExtent; + extCount : uint32; +begin + fatAppendExtent := false; + if (extents = nil) or (clusterCount = 0) then + exit; + + extCount := LL_Size(extents); + if extCount > 0 then begin + lastExt := PClusterExtent(LL_Get(extents, extCount - 1)); + if (lastExt <> nil) and ((lastExt^.StartCluster + lastExt^.ClusterCount) = firstCluster) then begin + lastExt^.ClusterCount := lastExt^.ClusterCount + clusterCount; + fatAppendExtent := true; + exit; + end; + end; + + ext := PClusterExtent(LL_Add(extents)); + if ext = nil then + exit; + ext^.StartCluster := firstCluster; + ext^.ClusterCount := clusterCount; + fatAppendExtent := true; +end; + +function fatBuildExtentList(volume : PStorage_volume; cluster : uint32; + bootRecord : PBootRecord) : PLinkedListBase; +var + extents : PLinkedListBase; + current : uint32; + nextCluster : uint32; + runStart : uint32; + runCount : uint32; + maxCluster : uint32; + iterCount : uint32; +begin + fatBuildExtentList := nil; + if (bootRecord = nil) or (cluster < 2) then + exit; + + extents := LL_New(sizeof(TClusterExtent)); + if extents = nil then + exit; + + if bootRecord^.sectorSize > 0 then + maxCluster := (bootRecord^.FATSize * bootRecord^.sectorSize) div 4 + else + maxCluster := 0; + + current := cluster; + runStart := cluster; + runCount := 1; + iterCount := 0; + + while true do begin + if (maxCluster > 0) and (iterCount >= maxCluster) then + break; + + nextCluster := readFat(volume, current, bootRecord); + if (nextCluster = 0) or ((nextCluster and $0FFFFFFF) = $0FFFFFF7) then + break; + + if (nextCluster and $0FFFFFFF) >= $0FFFFFF8 then begin + if not fatAppendExtent(extents, runStart, runCount) then begin + LL_Free(extents); + exit; + end; + fatBuildExtentList := extents; + exit; + end; + + if nextCluster = (current + 1) then + runCount := runCount + 1 + else begin + if not fatAppendExtent(extents, runStart, runCount) then begin + LL_Free(extents); + exit; + end; + runStart := nextCluster; + runCount := 1; + end; + + current := nextCluster; + iterCount := iterCount + 1; + end; + + if runCount > 0 then begin + if not fatAppendExtent(extents, runStart, runCount) then begin + LL_Free(extents); + exit; + end; + end; + + fatBuildExtentList := extents; +end; + +function fatLocateExtentRun(extents : PLinkedListBase; targetIdx : uint32; + hintValid : boolean; hintExtentIdx : uint32; + hintStartIdx : uint32; var runStartCluster : uint32; + var runClusterCount : uint32; var outExtentIdx : uint32; + var outExtentStartIdx : uint32) : boolean; +var + i : uint32; + clusterIx : uint32; + extCount : uint32; + ext : PClusterExtent; +begin + fatLocateExtentRun := false; + if extents = nil then + exit; + + extCount := LL_Size(extents); + if extCount = 0 then + exit; + + if hintValid and (hintExtentIdx < extCount) and (hintStartIdx <= targetIdx) then begin + i := hintExtentIdx; + clusterIx := hintStartIdx; + end else begin + i := 0; + clusterIx := 0; + end; + + while i < extCount do begin + ext := PClusterExtent(LL_Get(extents, i)); + if ext = nil then + exit; + + if targetIdx < (clusterIx + ext^.ClusterCount) then begin + runStartCluster := ext^.StartCluster + (targetIdx - clusterIx); + runClusterCount := ext^.ClusterCount - (targetIdx - clusterIx); + outExtentIdx := i; + outExtentStartIdx := clusterIx; + fatLocateExtentRun := true; + exit; + end; + + clusterIx := clusterIx + ext^.ClusterCount; + i := i + 1; + end; +end; + +function findFreeClusterExtents(volume : PStorage_volume; amount : uint32; + bootRecord : PBootRecord) : PLinkedListBase; +var + maxCluster : uint32; + scanStart : uint32; + currentAmount : uint32; + lastAllocated : uint32; + extents : PLinkedListBase; + + procedure appendExtent(firstCluster : uint32; clusterCount : uint32); + var + ext : PClusterExtent; + begin + ext := PClusterExtent(LL_Add(extents)); + ext^.StartCluster := firstCluster; + ext^.ClusterCount := clusterCount; + currentAmount := currentAmount + clusterCount; + lastAllocated := firstCluster + clusterCount - 1; + end; + + procedure scanRange(rangeStart : uint32; rangeEnd : uint32); + var + scanPos : uint32; + runStart : uint32; + runLen : uint32; + takeCount : uint32; + fatVal : uint32; + begin + scanPos := rangeStart; + while (scanPos < rangeEnd) and (currentAmount < amount) do begin + fatVal := readFat(volume, scanPos, bootRecord); + if fatVal <> 0 then begin + scanPos := scanPos + 1; + continue; + end; + + runStart := scanPos; + runLen := 0; + while scanPos < rangeEnd do begin + fatVal := readFat(volume, scanPos, bootRecord); + if fatVal <> 0 then + break; + runLen := runLen + 1; + scanPos := scanPos + 1; + end; + + takeCount := amount - currentAmount; + if runLen < takeCount then + takeCount := runLen; + appendExtent(runStart, takeCount); + end; + end; +begin + extents := LL_New(sizeof(TClusterExtent)); + currentAmount := 0; + lastAllocated := 0; + + if amount = 0 then begin + findFreeClusterExtents := extents; + exit; + end; + + maxCluster := fatMaxClusterCount(bootRecord); + if maxCluster <= 2 then begin + LL_Free(extents); + findFreeClusterExtents := nil; + exit; + end; + + if (sioc_allocVol = volume) and (sioc_allocHint >= 2) and (sioc_allocHint < maxCluster) then + scanStart := sioc_allocHint + else + scanStart := 2; + + scanRange(scanStart, maxCluster); + if (currentAmount < amount) and (scanStart > 2) then + scanRange(2, scanStart); + + if currentAmount < amount then begin + LL_Free(extents); + findFreeClusterExtents := nil; + exit; + end; + + fatUpdateAllocHint(volume, bootRecord, lastAllocated + 1); + findFreeClusterExtents := extents; end; function compareByteArray8(str1 : byteArray8; str2 : byteArray8) : boolean; @@ -1055,6 +1670,316 @@ begin buildContiguousRun := runLen; end; +procedure updateSIOC( + volume : PStorage_Volume; + fileStartCluster : uint32; + chainIdx : uint32; + chainCluster : uint32 +); forward; + +function seekToClusterIndex( + volume : PStorage_Volume; + fileStartCluster : uint32; + targetIdx : uint32; + bootRecord : PBootRecord; + var curCluster : uint32; + var chainPos : uint32 +) : boolean; forward; + +function seekToClusterIndexHinted( + volume : PStorage_Volume; + fileStartCluster : uint32; + targetIdx : uint32; + bootRecord : PBootRecord; + hintValid : boolean; + hintIdx : uint32; + hintCluster : uint32; + var curCluster : uint32; + var chainPos : uint32 +) : boolean; forward; + +procedure fat32_fileio_async_complete(ctx : PFATFileIOAsyncCtx; error : TError); +begin + if ctx = nil then exit; + if ctx^.BytesOut <> nil then + ctx^.BytesOut^ := ctx^.BytesDone; + if ctx^.Callback <> nil then + ctx^.Callback(error, ctx^.CallbackData); + fatTransferFree(ctx); +end; + +function fat32_fileio_prepare_single_run(ctx : PFATFileIOAsyncCtx; + var sectorLBA : uint32; var chunkSectors : uint32) : boolean; +const + MAX_RUN_SECTORS = 2048; +var + run : TRunInfo; + curCluster : uint32; + chainPos : uint32; + currentOffset : uint32; + bytesPerCluster : uint32; + startClusterIdx : uint32; + inClusterOffset : uint32; + remaining : uint32; + skipBytes : uint32; + copyBytes : uint32; + secSize : uint32; + spc : uint32; + lastBytePos : uint32; + lastClusterUsed : uint32; + lastClusterIdx : uint32; + maxRunClusters : uint32; + extentIdx : uint32; + extentStartIdx : uint32; +begin + fat32_fileio_prepare_single_run := false; + if ctx = nil then + exit; + + currentOffset := ctx^.Offset + ctx^.BytesDone; + remaining := ctx^.ByteCount - ctx^.BytesDone; + spc := uint32(ctx^.BootRecord^.spc); + secSize := uint32(ctx^.BootRecord^.sectorSize); + bytesPerCluster := spc * secSize; + startClusterIdx := currentOffset div bytesPerCluster; + inClusterOffset := currentOffset mod bytesPerCluster; + + maxRunClusters := MAX_RUN_SECTORS div spc; + if maxRunClusters = 0 then + maxRunClusters := 1; + + if (ctx^.OpenFile <> nil) and (ctx^.OpenFile^.Extents <> nil) and + fatLocateExtentRun(ctx^.OpenFile^.Extents, startClusterIdx, + ctx^.ExtentValid, ctx^.ExtentIdx, ctx^.ExtentStartIdx, + run.StartCluster, run.RunLen, extentIdx, extentStartIdx) then begin + if run.RunLen > maxRunClusters then + run.RunLen := maxRunClusters; + run.LastCluster := run.StartCluster + run.RunLen - 1; + ctx^.ExtentValid := true; + ctx^.ExtentIdx := extentIdx; + ctx^.ExtentStartIdx := extentStartIdx; + curCluster := run.LastCluster; + chainPos := startClusterIdx + run.RunLen - 1; + end else begin + if not seekToClusterIndexHinted( + ctx^.Volume, ctx^.Cluster, startClusterIdx, ctx^.BootRecord, + ctx^.CursorValid, ctx^.CursorIndex, ctx^.CursorCluster, + curCluster, chainPos) then + exit; + + run.StartCluster := curCluster; + buildContiguousRun(ctx^.Volume, curCluster, spc, MAX_RUN_SECTORS, ctx^.BootRecord, run.RunLen, run.LastCluster); + end; + + run.RunSectors := run.RunLen * spc; + run.RunBytes := run.RunSectors * secSize; + run.LBA := clusterToLBA(ctx^.DataStart, spc, run.StartCluster); + + skipBytes := inClusterOffset; + copyBytes := run.RunBytes - skipBytes; + if copyBytes > remaining then + copyBytes := remaining; + + if (copyBytes = 0) or ((copyBytes mod secSize) <> 0) or ((skipBytes mod secSize) <> 0) then + exit; + + sectorLBA := run.LBA + (skipBytes div secSize); + chunkSectors := copyBytes div secSize; + ctx^.ChunkBytes := copyBytes; + + lastBytePos := skipBytes + copyBytes - 1; + lastClusterIdx := startClusterIdx + (lastBytePos div bytesPerCluster); + lastClusterUsed := run.StartCluster + (lastBytePos div bytesPerCluster); + updateSIOC(ctx^.Volume, ctx^.Cluster, lastClusterIdx, lastClusterUsed); + ctx^.CursorValid := true; + ctx^.CursorIndex := lastClusterIdx; + ctx^.CursorCluster := lastClusterUsed; + + fat32_fileio_prepare_single_run := true; +end; + +procedure fat32_fileio_direct_complete(error : TError; userdata : pointer); +var + ctx : PFATFileIOAsyncCtx; +begin + ctx := PFATFileIOAsyncCtx(userdata); + if ctx = nil then exit; + + if error = eNone then begin + ctx^.BytesDone := ctx^.BytesDone + ctx^.ChunkBytes; + if (ctx^.Mode = famWrite) and (ctx^.OpenFile <> nil) then begin + if (ctx^.BytesDone > 0) and ((ctx^.Offset + ctx^.BytesDone) > ctx^.OpenFile^.ByteSize) then begin + ctx^.OpenFile^.ByteSize := ctx^.Offset + ctx^.BytesDone; + ctx^.OpenFile^.MetaDirty := true; + end; + if (ctx^.OpenFile^.FirstCluster = 0) and (ctx^.Cluster <> 0) then begin + ctx^.OpenFile^.FirstCluster := ctx^.Cluster; + ctx^.OpenFile^.Exists := true; + ctx^.OpenFile^.MetaDirty := true; + end; + end; + end; + + fat32_fileio_async_complete(ctx, error); +end; + +procedure fat32_fileio_async_step(error : TError; userdata : pointer); forward; + +procedure fat32_fileio_async_run_next(ctx : PFATFileIOAsyncCtx); +const + MAX_RUN_SECTORS = 2048; +var + run : TRunInfo; + curCluster : uint32; + chainPos : uint32; + currentOffset : uint32; + bytesPerCluster : uint32; + startClusterIdx : uint32; + inClusterOffset : uint32; + remaining : uint32; + skipBytes : uint32; + copyBytes : uint32; + secSize : uint32; + spc : uint32; + sectorLBA : uint32; + chunkSectors : uint32; + lastBytePos : uint32; + lastClusterUsed : uint32; + lastClusterIdx : uint32; + maxRunClusters : uint32; + extentIdx : uint32; + extentStartIdx : uint32; + err : TError; +begin + if ctx = nil then exit; + + if ctx^.State = ftsAwaitIO then begin + if ctx^.LastError <> eNone then begin + ctx^.State := ftsError; + fat32_fileio_async_complete(ctx, ctx^.LastError); + exit; + end; + + ctx^.State := ftsAdvance; + ctx^.BytesDone := ctx^.BytesDone + ctx^.ChunkBytes; + + if (ctx^.Mode = famWrite) and (ctx^.OpenFile <> nil) then begin + if (ctx^.BytesDone > 0) and ((ctx^.Offset + ctx^.BytesDone) > ctx^.OpenFile^.ByteSize) then begin + ctx^.OpenFile^.ByteSize := ctx^.Offset + ctx^.BytesDone; + ctx^.OpenFile^.MetaDirty := true; + end; + if (ctx^.OpenFile^.FirstCluster = 0) and (ctx^.Cluster <> 0) then begin + ctx^.OpenFile^.FirstCluster := ctx^.Cluster; + ctx^.OpenFile^.Exists := true; + ctx^.OpenFile^.MetaDirty := true; + end; + end; + end; + + if ctx^.BytesDone >= ctx^.ByteCount then begin + ctx^.State := ftsComplete; + fat32_fileio_async_complete(ctx, eNone); + exit; + end; + + ctx^.State := ftsPrepare; + + currentOffset := ctx^.Offset + ctx^.BytesDone; + remaining := ctx^.ByteCount - ctx^.BytesDone; + spc := uint32(ctx^.BootRecord^.spc); + secSize := uint32(ctx^.BootRecord^.sectorSize); + bytesPerCluster := spc * secSize; + startClusterIdx := currentOffset div bytesPerCluster; + inClusterOffset := currentOffset mod bytesPerCluster; + + maxRunClusters := MAX_RUN_SECTORS div spc; + if maxRunClusters = 0 then + maxRunClusters := 1; + + if (ctx^.OpenFile <> nil) and (ctx^.OpenFile^.Extents <> nil) and + fatLocateExtentRun(ctx^.OpenFile^.Extents, startClusterIdx, + ctx^.ExtentValid, ctx^.ExtentIdx, ctx^.ExtentStartIdx, + run.StartCluster, run.RunLen, extentIdx, extentStartIdx) then begin + if run.RunLen > maxRunClusters then + run.RunLen := maxRunClusters; + run.LastCluster := run.StartCluster + run.RunLen - 1; + ctx^.ExtentValid := true; + ctx^.ExtentIdx := extentIdx; + ctx^.ExtentStartIdx := extentStartIdx; + curCluster := run.LastCluster; + chainPos := startClusterIdx + run.RunLen - 1; + end else begin + if not seekToClusterIndexHinted( + ctx^.Volume, ctx^.Cluster, startClusterIdx, ctx^.BootRecord, + ctx^.CursorValid, ctx^.CursorIndex, ctx^.CursorCluster, + curCluster, chainPos) then begin + ctx^.State := ftsError; + fat32_fileio_async_complete(ctx, eCorruptFilesystem); + exit; + end; + + run.StartCluster := curCluster; + buildContiguousRun(ctx^.Volume, curCluster, spc, MAX_RUN_SECTORS, ctx^.BootRecord, run.RunLen, run.LastCluster); + end; + run.RunSectors := run.RunLen * spc; + run.RunBytes := run.RunSectors * secSize; + run.LBA := clusterToLBA(ctx^.DataStart, spc, run.StartCluster); + + skipBytes := inClusterOffset; + copyBytes := run.RunBytes - skipBytes; + if copyBytes > remaining then + copyBytes := remaining; + + if (copyBytes = 0) or ((copyBytes mod secSize) <> 0) or ((skipBytes mod secSize) <> 0) then begin + ctx^.State := ftsError; + fat32_fileio_async_complete(ctx, eInvalidArgument); + exit; + end; + + sectorLBA := run.LBA + (skipBytes div secSize); + chunkSectors := copyBytes div secSize; + ctx^.ChunkBytes := copyBytes; + + lastBytePos := skipBytes + copyBytes - 1; + lastClusterIdx := startClusterIdx + (lastBytePos div bytesPerCluster); + lastClusterUsed := run.StartCluster + (lastBytePos div bytesPerCluster); + updateSIOC(ctx^.Volume, ctx^.Cluster, lastClusterIdx, lastClusterUsed); + ctx^.CursorValid := true; + ctx^.CursorIndex := lastClusterIdx; + ctx^.CursorCluster := lastClusterUsed; + + ctx^.State := ftsIssueIO; + + if ctx^.Mode = famRead then + err := driver.storage.mgr.storage_read_async( + ctx^.Volume^.device, sectorLBA, chunkSectors, + puint32(uint32(ctx^.Buffer) + ctx^.BytesDone), @fat32_fileio_async_step, ctx) + else + err := driver.storage.mgr.storage_write_async( + ctx^.Volume^.device, sectorLBA, chunkSectors, + puint32(uint32(ctx^.Buffer) + ctx^.BytesDone), @fat32_fileio_async_step, ctx); + + if err = eNone then + ctx^.State := ftsAwaitIO + else + ctx^.State := ftsError; + if err <> eNone then + fat32_fileio_async_complete(ctx, err); +end; + +procedure fat32_fileio_async_step(error : TError; userdata : pointer); +var + ctx : PFATFileIOAsyncCtx; +begin + ctx := PFATFileIOAsyncCtx(userdata); + if ctx = nil then exit; + ctx^.LastError := error; + if not fatScheduleTransfer(ctx) then begin + ctx^.State := ftsError; + fat32_fileio_async_complete(ctx, eQueueFull); + end; +end; + { Update the SIOC chain-position cache. } procedure updateSIOC( volume : PStorage_Volume; @@ -1084,8 +2009,13 @@ function seekToClusterIndex( ) : boolean; var nextFat : uint32; + maxCluster : uint32; begin seekToClusterIndex := false; + if bootRecord^.sectorSize > 0 then + maxCluster := (bootRecord^.FATSize * bootRecord^.sectorSize) div 4 + else + maxCluster := 0; { Resume from SIOC cache if it covers this file at a useful position } if sioc_valid and (sioc_vol = volume) and (sioc_fileClust = fileStartCluster) @@ -1099,6 +2029,8 @@ begin { Walk forward to targetIdx } while chainPos < targetIdx do begin + if (maxCluster > 0) and (chainPos >= maxCluster) then + exit; nextFat := readFat(volume, curCluster, bootRecord); if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then exit; @@ -1109,6 +2041,50 @@ begin seekToClusterIndex := true; end; +function seekToClusterIndexHinted( + volume : PStorage_Volume; + fileStartCluster : uint32; + targetIdx : uint32; + bootRecord : PBootRecord; + hintValid : boolean; + hintIdx : uint32; + hintCluster : uint32; + var curCluster : uint32; + var chainPos : uint32 +) : boolean; +var + nextFat : uint32; + maxCluster : uint32; +begin + seekToClusterIndexHinted := false; + if bootRecord^.sectorSize > 0 then + maxCluster := (bootRecord^.FATSize * bootRecord^.sectorSize) div 4 + else + maxCluster := 0; + + if hintValid and (hintIdx <= targetIdx) then begin + curCluster := hintCluster; + chainPos := hintIdx; + end else begin + if not seekToClusterIndex(volume, fileStartCluster, targetIdx, bootRecord, curCluster, chainPos) then + exit; + seekToClusterIndexHinted := true; + exit; + end; + + while chainPos < targetIdx do begin + if (maxCluster > 0) and (chainPos >= maxCluster) then + exit; + nextFat := readFat(volume, curCluster, bootRecord); + if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then + exit; + curCluster := nextFat; + chainPos := chainPos + 1; + end; + + seekToClusterIndexHinted := true; +end; + //TODO add optional attributes flag to refine what i return function getDirEntries(volume : PStorage_volume; cluster : uint32; bootRecord : PBootRecord) : PLinkedListBase; var @@ -1310,7 +2286,8 @@ var directories : PLinkedListBase; parentDirectory : PDirectory; parentCluster : uint32; - clusters : PLinkedListBase; + extents : PLinkedListBase; + extent : PClusterExtent; cluster : uint32; bootRecord : PBootRecord; buffer : puint32; @@ -1391,8 +2368,8 @@ begin parentDirectory:= PDirectory(LL_Get(directories, 0)); parentCluster:= dirFirstCluster(parentDirectory); - clusters:= findFreeClusters(volume, 1, bootRecord); - if clusters = nil then begin + extents := findFreeClusterExtents(volume, 1, bootRecord); + if extents = nil then begin { No free clusters available — disk is full } io.syslog.logln('FAT32', 'writeDirectory: disk full'); statusOut^:= ord(eDiskFull); @@ -1401,9 +2378,10 @@ begin LL_Free(directories); exit; end; - cluster:= uint32(LL_Get(clusters, 0)^); + extent := PClusterExtent(LL_Get(extents, 0)); + cluster := extent^.StartCluster; io.syslog.logln('FAT32', 'writeDirectory: allocated cluster'); - LL_Free(clusters); + LL_Free(extents); buffer:= puint32(kalloc(bootRecord^.sectorSize)); if attributes = $10 then begin // if directory @@ -2141,11 +3119,12 @@ begin if config <> nil then spc := config^ else - spc := 1; - if spc = 0 then spc := 1; + spc := fatDefaultSPC(sectors, ctx^.Disk^.sectorSize); + if spc = 0 then + spc := fatDefaultSPC(sectors, ctx^.Disk^.sectorSize); ctx^.SPC := spc; - ctx^.FATSize := ((sectors div spc) * 4) div ctx^.Disk^.sectorSize; + ctx^.FATSize := fatComputeFATSize(sectors, spc, ctx^.Disk^.sectorSize); ctx^.FATStart := start + 1 + 32; { boot sector + 32 reserved sectors } ctx^.DataStart := ctx^.FATStart + ctx^.FATSize; @@ -2169,7 +3148,7 @@ begin bootRecord^.OEMName[6] := 'V'; bootRecord^.OEMName[7] := '1'; bootRecord^.sectorSize := ctx^.Disk^.sectorSize; - bootRecord^.spc := 1; + bootRecord^.spc := uint8(spc); bootRecord^.rsvSectors := 32; bootRecord^.numFats := 1; bootRecord^.mediaDescp := $F8; @@ -2257,8 +3236,9 @@ begin if config <> nil then spc := config^ else - spc := 1; - if spc = 0 then spc := 1; + spc := fatDefaultSPC(sectors, disk^.sectorSize); + if spc = 0 then + spc := fatDefaultSPC(sectors, disk^.sectorSize); //driver.storage.fs.fat32 structure (* BootRecord *) @@ -2271,12 +3251,12 @@ begin bootRecord:= PBootRecord(buffer); - FATSize:= ((sectors div spc) * 4) div disk^.sectorsize; + FATSize:= fatComputeFATSize(sectors, spc, disk^.sectorsize); bootRecord^.jmp2boot := $0; //TODO impliment boot jump bootRecord^.OEMName := asuroArray; bootRecord^.sectorSize := disk^.sectorsize; - bootRecord^.spc := 1; + bootRecord^.spc := uint8(spc); bootRecord^.rsvSectors := 32; //32 is standard bootRecord^.numFats := 1; bootRecord^.mediaDescp := $F8; @@ -2501,8 +3481,13 @@ begin ofi := PFATOpenFile(kalloc(SizeOf(TFATOpenFile))); memset(uint32(ofi), 0, SizeOf(TFATOpenFile)); + ofi^.Volume := volume; ofi^.BootRecord := bootRecord; ofi^.DataStart := fatDataStartLBA(volume, bootRecord); + ofi^.ScratchSize := bootRecord^.sectorSize; + ofi^.ScratchSector := puint32(kalloc(ofi^.ScratchSize)); + if ofi^.ScratchSector <> nil then + memset(uint32(ofi^.ScratchSector), 0, ofi^.ScratchSize); ofi^.Exists := false; { Read directory entries } @@ -2531,17 +3516,22 @@ begin ofi^.ExtPart := stringCopy(extPart); { Search for file in directory entries } - if (dirs <> nil) and (LL_size(dirs) > 0) then begin - for i := 0 to LL_Size(dirs) - 1 do begin - tempdir := PDirectory(LL_get(dirs, i)); - otherCFN := cleanString(tempdir^.filename, status); - if compareByteArray8(cleanFileName, otherCFN) and matchExtension(tempdir^.fileExtension, extPart) then begin - ofi^.FirstCluster := dirFirstCluster(tempdir); - ofi^.ByteSize := tempdir^.byteSize; - ofi^.Exists := true; + if (dirs <> nil) and (LL_size(dirs) > 0) then begin + for i := 0 to LL_Size(dirs) - 1 do begin + tempdir := PDirectory(LL_get(dirs, i)); + otherCFN := cleanString(tempdir^.filename, status); + if compareByteArray8(cleanFileName, otherCFN) and matchExtension(tempdir^.fileExtension, extPart) then begin + ofi^.FirstCluster := dirFirstCluster(tempdir); + ofi^.ByteSize := tempdir^.byteSize; + ofi^.Exists := true; + end; end; end; - end; + + ofi^.AllocClusters := fatClustersForBytes(ofi^.ByteSize, + uint32(bootRecord^.spc) * uint32(bootRecord^.sectorSize)); + if ofi^.Exists and (ofi^.FirstCluster >= 2) and (ofi^.ByteSize > 0) then + ofi^.Extents := fatBuildExtentList(volume, ofi^.FirstCluster, bootRecord); kfree(void(namePart)); kfree(void(extPart)); @@ -2556,12 +3546,38 @@ end; { Called by VFS on CloseFile to free the per-file context. } procedure fat32CloseFile(ctx : pointer); var - ofi : PFATOpenFile; + ofi : PFATOpenFile; + fatCache : PFATCache; + dirBuf : puint32; + rawDir : PDirectory; + loc : TDirEntryLocation; begin if ctx = nil then exit; ofi := PFATOpenFile(ctx); + if ofi^.FatDirty and (ofi^.Volume <> nil) then begin + fatCache := fatCacheGet(ofi^.Volume); + if fatCache <> nil then + fatCacheFlush(fatCache, ofi^.Volume); + end; + if ofi^.MetaDirty and (ofi^.Volume <> nil) and (ofi^.BootRecord <> nil) then begin + dirBuf := puint32(kalloc(ofi^.BootRecord^.sectorSize)); + if (dirBuf <> nil) and locateDirEntry(ofi^.Volume, ofi^.DirCluster, ofi^.BootRecord, + ofi^.CleanName, ofi^.ExtPart, loc, dirBuf) then begin + rawDir := @PDirectory(dirBuf)[loc.EntryIdx]; + rawDir^.byteSize := ofi^.ByteSize; + if ofi^.FirstCluster <> 0 then + setDirFirstCluster(rawDir, ofi^.FirstCluster); + driver.storage.mgr.storage_write(ofi^.Volume^.device, loc.SectorLBA, 1, dirBuf); + end; + if dirBuf <> nil then + kfree(dirBuf); + end; if ofi^.ExtPart <> nil then kfree(void(ofi^.ExtPart)); + if ofi^.Extents <> nil then + LL_Free(ofi^.Extents); + if ofi^.ScratchSector <> nil then + kfree(ofi^.ScratchSector); kfree(puint32(ofi)); end; @@ -2599,11 +3615,12 @@ var newEnd : uint32; oldClusters : uint32; needClusters : uint32; + targetClusters : uint32; extraClusters : uint32; - newClusts : PLinkedListBase; + newExtents : PLinkedListBase; lastCluster : uint32; - nextCluster : uint32; nextFat : uint32; + extent : PClusterExtent; { dir entry update — raw sector scan (Lesson 18) } dirCluster : uint32; dirBuf : puint32; @@ -2620,6 +3637,9 @@ var batchBytes : uint32; sectorLBA : uint32; fatCache : PFATCache; + chainWalkLimit : uint32; + firstNewCluster : uint32; + reserveClusters : uint32; begin push_trace('driver.storage.fs.fat32.writeFileAtOffset.enter'); writeFileAtOffset := 0; @@ -2690,6 +3710,10 @@ begin end; end; fileSize := origByteSize; + if lookup.BootRecord^.sectorSize > 0 then + chainWalkLimit := (lookup.BootRecord^.FATSize * lookup.BootRecord^.sectorSize) div 4 + else + chainWalkLimit := 0; if not lookup.Exists then begin { --- Create a new zero-length file entry on disk --- } @@ -2730,35 +3754,21 @@ begin { ---- Extend file if writing past EOF ---- } if newEnd > fileSize then begin - if fileSize = 0 then - oldClusters := 0 + if ofi <> nil then + oldClusters := ofi^.AllocClusters else - oldClusters := (fileSize + bytesPerCluster - 1) div bytesPerCluster; - needClusters := (newEnd + bytesPerCluster - 1) div bytesPerCluster; - - if needClusters > oldClusters then begin - extraClusters := needClusters - oldClusters; - newClusts := findFreeClusters(volume, extraClusters, lookup.BootRecord); - if newClusts = nil then begin - io.syslog.logln('FAT32', 'writeFileAtOffset: exit-B disk full'); - { Undo just-created dir entry if file was new } - if not lookup.Exists then begin - dirBuf := puint32(kalloc(lookup.BootRecord^.sectorSize)); - driver.storage.mgr.storage_read(volume^.device, loc.SectorLBA, 1, dirBuf); - PDirectory(dirBuf)[loc.EntryIdx].fileName[0] := char($E5); - driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, dirBuf); - kfree(dirBuf); - end; - if ofi = nil then begin - if namePart <> nil then kfree(void(namePart)); - if extPart <> nil then kfree(void(extPart)); - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); - end; - exit; - end; + oldClusters := fatClustersForBytes(fileSize, bytesPerCluster); + needClusters := fatClustersForBytes(newEnd, bytesPerCluster); + targetClusters := needClusters; + if (ofi <> nil) and (needClusters > oldClusters) then begin + reserveClusters := fatPreallocClusterCount(bytesPerCluster); + targetClusters := oldClusters + reserveClusters; + if targetClusters < needClusters then + targetClusters := needClusters; + end; + if targetClusters > oldClusters then begin + extraClusters := targetClusters - oldClusters; if oldClusters > 0 then begin { Walk to last cluster using cache if available } if sioc_valid and (sioc_vol = volume) and (sioc_fileClust = cluster) then @@ -2767,6 +3777,20 @@ begin lastCluster := cluster; nextFat := readFat(volume, lastCluster, lookup.BootRecord); while ((nextFat and $0FFFFFFF) < $0FFFFFF8) and (nextFat <> 0) do begin + if chainWalkLimit = 0 then + break; + chainWalkLimit := chainWalkLimit - 1; + if chainWalkLimit = 0 then begin + io.syslog.logln('FAT32', 'writeFileAtOffset: FAT loop detected while finding tail'); + if ofi = nil then begin + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); + end; + exit; + end; lastCluster := nextFat; nextFat := readFat(volume, lastCluster, lookup.BootRecord); end; @@ -2775,25 +3799,65 @@ begin lastCluster := 0; end; - { Link new clusters } - if extraClusters > 0 then begin - for i := 0 to extraClusters - 1 do begin - nextCluster := puint32(LL_Get(newClusts, i))^; - if lastCluster <> 0 then - writeFat(volume, lastCluster, nextCluster, lookup.BootRecord); - lastCluster := nextCluster; + if fatFindContiguousFreeRange(volume, extraClusters, lookup.BootRecord, firstNewCluster) then begin + fatLinkContiguousRange(volume, lookup.BootRecord, lastCluster, firstNewCluster, extraClusters, lastCluster); + if oldClusters = 0 then + cluster := firstNewCluster; + if ofi <> nil then begin + if ofi^.Extents = nil then + ofi^.Extents := LL_New(sizeof(TClusterExtent)); + if ofi^.Extents <> nil then + fatAppendExtent(ofi^.Extents, firstNewCluster, extraClusters); end; - writeFat(volume, lastCluster, $FFFFFFF8, lookup.BootRecord); + end else begin + newExtents := findFreeClusterExtents(volume, extraClusters, lookup.BootRecord); + if newExtents = nil then begin + io.syslog.logln('FAT32', 'writeFileAtOffset: exit-B disk full'); + { Undo just-created dir entry if file was new } + if not lookup.Exists then begin + dirBuf := puint32(kalloc(lookup.BootRecord^.sectorSize)); + driver.storage.mgr.storage_read(volume^.device, loc.SectorLBA, 1, dirBuf); + PDirectory(dirBuf)[loc.EntryIdx].fileName[0] := char($E5); + driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, dirBuf); + kfree(dirBuf); + end; + if ofi = nil then begin + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); + end; + exit; + end; + + { Link new extents } + if LL_Size(newExtents) > 0 then begin + for i := 0 to LL_Size(newExtents) - 1 do begin + extent := PClusterExtent(LL_Get(newExtents, i)); + fatLinkContiguousRange(volume, lookup.BootRecord, lastCluster, + extent^.StartCluster, extent^.ClusterCount, lastCluster); + if (ofi <> nil) and (extent <> nil) then begin + if ofi^.Extents = nil then + ofi^.Extents := LL_New(sizeof(TClusterExtent)); + if ofi^.Extents <> nil then + fatAppendExtent(ofi^.Extents, extent^.StartCluster, extent^.ClusterCount); + end; + if (oldClusters = 0) and (i = 0) then + cluster := extent^.StartCluster; + end; + end; + LL_Free(newExtents); end; - { If file was zero-length, first new cluster becomes start cluster } - if oldClusters = 0 then - cluster := puint32(LL_Get(newClusts, 0))^; + if ofi <> nil then + ofi^.FatDirty := true; + + if ofi <> nil then + ofi^.AllocClusters := targetClusters; { Update chain cache: last new cluster is the new tail } - updateSIOC(volume, cluster, needClusters - 1, lastCluster); - - LL_Free(newClusts); + updateSIOC(volume, cluster, targetClusters - 1, lastCluster); end; @@ -2818,7 +3882,10 @@ begin spc := uint32(lookup.BootRecord^.spc); secSize := uint32(lookup.BootRecord^.sectorSize); - ioBuf := puint32(kalloc(secSize)); + if (ofi <> nil) and (ofi^.ScratchSector <> nil) and (ofi^.ScratchSize >= secSize) then + ioBuf := ofi^.ScratchSector + else + ioBuf := nil; remaining := byteCount; srcPos := 0; @@ -2845,6 +3912,10 @@ begin { Phase 1: Partial first sector — read-modify-write } if firstSectorOff > 0 then begin + if ioBuf = nil then + ioBuf := puint32(kalloc(secSize)); + if ioBuf = nil then + break; sectorLBA := run.LBA + (skipBytes div secSize); partialBytes := secSize - firstSectorOff; if partialBytes > copyBytes then @@ -2873,6 +3944,10 @@ begin { Phase 3: Partial last sector — read-modify-write } if copyBytes > 0 then begin + if ioBuf = nil then + ioBuf := puint32(kalloc(secSize)); + if ioBuf = nil then + break; sectorLBA := run.LBA + (skipBytes div secSize); driver.storage.mgr.storage_read(volume^.device, sectorLBA, 1, ioBuf); core.util.memcpy(uint32(buffer) + srcPos, uint32(ioBuf), copyBytes); @@ -2902,16 +3977,23 @@ begin end; end; - kfree(ioBuf); + if (ioBuf <> nil) and ((ofi = nil) or (ioBuf <> ofi^.ScratchSector)) then + kfree(ioBuf); - { Flush dirty FAT cache entries to disk } - fatCache := fatCacheGet(volume); - if fatCache <> nil then - fatCacheFlush(fatCache, volume); + { Flush dirty FAT cache entries to disk only for uncached callers. + Open-file callers defer this to fat32CloseFile to avoid flushing on every chunk. } + if ofi = nil then begin + fatCache := fatCacheGet(volume); + if fatCache <> nil then + fatCacheFlush(fatCache, volume); + end; { ---- Update directory entry byteSize if file was extended ---- } { Only commit actual bytes written — never advance size past what was really written (Lesson 22) } if (srcPos > 0) and ((offset + srcPos) > origByteSize) then begin + if ofi <> nil then begin + ofi^.MetaDirty := true; + end else begin { Lesson 18: search raw directory sectors, don't use LL indices. dirCluster, cleanFileName, extPart were set once in the discovery phase. } dirBuf := puint32(kalloc(lookup.BootRecord^.sectorSize)); @@ -2926,6 +4008,7 @@ begin end; kfree(dirBuf); + end; end; { Update cached open-file metadata so the next read/write sees the new state } @@ -2935,6 +4018,7 @@ begin if (ofi^.FirstCluster = 0) and (cluster <> 0) then begin ofi^.FirstCluster := cluster; ofi^.Exists := true; + ofi^.MetaDirty := true; end; end; @@ -3206,6 +4290,283 @@ begin push_trace('driver.storage.fs.fat32.readFileAtOffset.exit'); end; +procedure readFileAtOffsetAsync(volume : PStorage_Volume; directory : pchar; fileName : pchar; + offset : uint32; buffer : puint32; byteCount : uint32; + ctx : pointer; bytesRead : puint32; + callback : TIOCallback; callbackData : pointer); +var + ofi : PFATOpenFile; + asyncCtx : PFATFileIOAsyncCtx; + sectorLBA : uint32; + chunkSectors : uint32; + err : TError; +begin + if bytesRead <> nil then bytesRead^ := 0; + ofi := PFATOpenFile(ctx); + + if (ofi = nil) or (buffer = nil) or (byteCount = 0) then begin + if callback <> nil then callback(eInvalidArgument, callbackData); + exit; + end; + + if not ofi^.Exists then begin + if callback <> nil then callback(eFileDoesNotExist, callbackData); + exit; + end; + + if offset >= ofi^.ByteSize then begin + if callback <> nil then callback(eNone, callbackData); + exit; + end; + + if offset + byteCount > ofi^.ByteSize then + byteCount := ofi^.ByteSize - offset; + + if ((offset mod ofi^.BootRecord^.sectorSize) <> 0) or ((byteCount mod ofi^.BootRecord^.sectorSize) <> 0) then begin + if bytesRead <> nil then + bytesRead^ := readFileAtOffset(volume, directory, fileName, offset, buffer, byteCount, ctx); + if callback <> nil then callback(eNone, callbackData); + exit; + end; + + asyncCtx := fatTransferAlloc(volume); + if asyncCtx = nil then begin + if callback <> nil then callback(eOutOfMemory, callbackData); + exit; + end; + asyncCtx^.Mode := famRead; + asyncCtx^.State := ftsPrepare; + asyncCtx^.Volume := volume; + asyncCtx^.OpenFile := ofi; + asyncCtx^.Buffer := buffer; + asyncCtx^.Offset := offset; + asyncCtx^.ByteCount := byteCount; + asyncCtx^.BytesOut := bytesRead; + asyncCtx^.Callback := callback; + asyncCtx^.CallbackData := callbackData; + asyncCtx^.Cluster := ofi^.FirstCluster; + asyncCtx^.DataStart := ofi^.DataStart; + asyncCtx^.BootRecord := ofi^.BootRecord; + asyncCtx^.LastError := eNone; + asyncCtx^.CursorValid := false; + asyncCtx^.ExtentValid := false; + if fat32_fileio_prepare_single_run(asyncCtx, sectorLBA, chunkSectors) and + (asyncCtx^.ChunkBytes = asyncCtx^.ByteCount) then begin + err := driver.storage.mgr.storage_read_async( + volume^.device, sectorLBA, chunkSectors, buffer, + @fat32_fileio_direct_complete, asyncCtx); + if err <> eNone then begin + fatTransferFree(asyncCtx); + if callback <> nil then callback(err, callbackData); + end; + exit; + end; + fatWorkerEnsure(volume); + if not fatScheduleTransfer(asyncCtx) then begin + fatTransferFree(asyncCtx); + if callback <> nil then callback(eQueueFull, callbackData); + end; +end; + +procedure writeFileAtOffsetAsync(volume : PStorage_Volume; directory : pchar; fileName : pchar; + offset : uint32; buffer : puint32; byteCount : uint32; + ctx : pointer; bytesWritten : puint32; + callback : TIOCallback; callbackData : pointer); +var + ofi : PFATOpenFile; + asyncCtx : PFATFileIOAsyncCtx; + cluster : uint32; + dataStart : uint32; + origByteSize : uint32; + newEnd : uint32; + bytesPerCluster : uint32; + oldClusters : uint32; + needClusters : uint32; + targetClusters : uint32; + extraClusters : uint32; + newExtents : PLinkedListBase; + lastCluster : uint32; + nextFat : uint32; + i : uint32; + extent : PClusterExtent; + dirBuf : puint32; + rawDir : PDirectory; + loc : TDirEntryLocation; + chainWalkLimit : uint32; + firstNewCluster : uint32; + reserveClusters : uint32; + sectorLBA : uint32; + chunkSectors : uint32; + err : TError; +begin + if bytesWritten <> nil then bytesWritten^ := 0; + ofi := PFATOpenFile(ctx); + + if (ofi = nil) or (buffer = nil) or (byteCount = 0) then begin + if callback <> nil then callback(eInvalidArgument, callbackData); + exit; + end; + + if ((offset mod ofi^.BootRecord^.sectorSize) <> 0) or ((byteCount mod ofi^.BootRecord^.sectorSize) <> 0) then begin + if bytesWritten <> nil then + bytesWritten^ := writeFileAtOffset(volume, directory, fileName, offset, buffer, byteCount, ctx); + if callback <> nil then callback(eNone, callbackData); + exit; + end; + + dataStart := ofi^.DataStart; + origByteSize := ofi^.ByteSize; + if ofi^.BootRecord^.sectorSize > 0 then + chainWalkLimit := (ofi^.BootRecord^.FATSize * ofi^.BootRecord^.sectorSize) div 4 + else + chainWalkLimit := 0; + if ofi^.Exists then + cluster := ofi^.FirstCluster + else + cluster := 0; + + if not ofi^.Exists then begin + dirBuf := puint32(kalloc(ofi^.BootRecord^.sectorSize)); + if dirBuf = nil then begin + if callback <> nil then callback(eOutOfMemory, callbackData); + exit; + end; + if not locateFreeDirEntry(volume, ofi^.DirCluster, ofi^.BootRecord, loc, dirBuf) then begin + kfree(dirBuf); + if callback <> nil then callback(eDirectoryFull, callbackData); + exit; + end; + rawDir := @PDirectory(dirBuf)[loc.EntryIdx]; + memset(uint32(rawDir), 0, sizeof(TDirectory)); + rawDir^.fileName := ofi^.CleanName; + rawDir^.attributes := 0; + fillFatExt(rawDir^.fileExtension, ofi^.ExtPart); + rawDir^.clusterLow := 0; + rawDir^.clusterHigh := 0; + rawDir^.byteSize := 0; + driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, dirBuf); + kfree(dirBuf); + ofi^.Exists := true; + sioc_valid := false; + end; + + newEnd := offset + byteCount; + bytesPerCluster := uint32(ofi^.BootRecord^.spc) * uint32(ofi^.BootRecord^.sectorSize); + + if newEnd > origByteSize then begin + oldClusters := ofi^.AllocClusters; + needClusters := fatClustersForBytes(newEnd, bytesPerCluster); + targetClusters := needClusters; + if needClusters > oldClusters then begin + reserveClusters := fatPreallocClusterCount(bytesPerCluster); + targetClusters := oldClusters + reserveClusters; + if targetClusters < needClusters then + targetClusters := needClusters; + end; + + if targetClusters > oldClusters then begin + extraClusters := targetClusters - oldClusters; + if oldClusters > 0 then begin + if sioc_valid and (sioc_vol = volume) and (sioc_fileClust = cluster) then + lastCluster := sioc_chainClust + else + lastCluster := cluster; + nextFat := readFat(volume, lastCluster, ofi^.BootRecord); + while ((nextFat and $0FFFFFFF) < $0FFFFFF8) and (nextFat <> 0) do begin + if chainWalkLimit = 0 then begin + if callback <> nil then callback(eCorruptFilesystem, callbackData); + exit; + end; + chainWalkLimit := chainWalkLimit - 1; + lastCluster := nextFat; + nextFat := readFat(volume, lastCluster, ofi^.BootRecord); + end; + end else + lastCluster := 0; + + if fatFindContiguousFreeRange(volume, extraClusters, ofi^.BootRecord, firstNewCluster) then begin + fatLinkContiguousRange(volume, ofi^.BootRecord, lastCluster, firstNewCluster, extraClusters, lastCluster); + if oldClusters = 0 then + cluster := firstNewCluster; + if ofi^.Extents = nil then + ofi^.Extents := LL_New(sizeof(TClusterExtent)); + if ofi^.Extents <> nil then + fatAppendExtent(ofi^.Extents, firstNewCluster, extraClusters); + end else begin + newExtents := findFreeClusterExtents(volume, extraClusters, ofi^.BootRecord); + if newExtents = nil then begin + if callback <> nil then callback(eDiskFull, callbackData); + exit; + end; + + if LL_Size(newExtents) > 0 then begin + for i := 0 to LL_Size(newExtents) - 1 do begin + extent := PClusterExtent(LL_Get(newExtents, i)); + fatLinkContiguousRange(volume, ofi^.BootRecord, lastCluster, + extent^.StartCluster, extent^.ClusterCount, lastCluster); + if extent <> nil then begin + if ofi^.Extents = nil then + ofi^.Extents := LL_New(sizeof(TClusterExtent)); + if ofi^.Extents <> nil then + fatAppendExtent(ofi^.Extents, extent^.StartCluster, extent^.ClusterCount); + end; + if (oldClusters = 0) and (i = 0) then + cluster := extent^.StartCluster; + end; + end; + LL_Free(newExtents); + end; + + ofi^.FatDirty := true; + ofi^.AllocClusters := targetClusters; + updateSIOC(volume, cluster, targetClusters - 1, lastCluster); + end; + end; + + asyncCtx := fatTransferAlloc(volume); + if asyncCtx = nil then begin + if callback <> nil then callback(eOutOfMemory, callbackData); + exit; + end; + asyncCtx^.Mode := famWrite; + asyncCtx^.State := ftsPrepare; + asyncCtx^.Volume := volume; + asyncCtx^.OpenFile := ofi; + asyncCtx^.Buffer := buffer; + asyncCtx^.Offset := offset; + asyncCtx^.ByteCount := byteCount; + asyncCtx^.BytesOut := bytesWritten; + asyncCtx^.Callback := callback; + asyncCtx^.CallbackData := callbackData; + asyncCtx^.Cluster := cluster; + asyncCtx^.OrigByteSize := origByteSize; + asyncCtx^.DataStart := dataStart; + asyncCtx^.BootRecord := ofi^.BootRecord; + + if (cluster <> 0) and ((offset + byteCount) > ofi^.ByteSize) then + ofi^.MetaDirty := true; + + asyncCtx^.LastError := eNone; + asyncCtx^.CursorValid := false; + asyncCtx^.ExtentValid := false; + if fat32_fileio_prepare_single_run(asyncCtx, sectorLBA, chunkSectors) and + (asyncCtx^.ChunkBytes = asyncCtx^.ByteCount) then begin + err := driver.storage.mgr.storage_write_async( + volume^.device, sectorLBA, chunkSectors, buffer, + @fat32_fileio_direct_complete, asyncCtx); + if err <> eNone then begin + fatTransferFree(asyncCtx); + if callback <> nil then callback(err, callbackData); + end; + exit; + end; + fatWorkerEnsure(volume); + if not fatScheduleTransfer(asyncCtx) then begin + fatTransferFree(asyncCtx); + if callback <> nil then callback(eQueueFull, callbackData); + end; +end; + procedure init(); begin push_trace('driver.storage.fs.fat32.init()'); @@ -3219,6 +4580,8 @@ begin filesystem.detectcallback:= @detect_volumes; filesystem.readOffsetCallback := @readFileAtOffset; filesystem.writeOffsetCallback := @writeFileAtOffset; + filesystem.readOffsetAsyncCallback := @readFileAtOffsetAsync; + filesystem.writeOffsetAsyncCallback := @writeFileAtOffsetAsync; filesystem.fileSizeCallback := @getFileSize; filesystem.identifyCallback := @identify_volume; filesystem.deleteFileCallback := @deleteFile; @@ -3231,8 +4594,6 @@ begin parked cleanly (CPU-free) while the driver.storage.ctl.ahci ISR completes the request. } filesystem.createDirAsyncCallback := nil; filesystem.readDirAsyncCallback := nil; - filesystem.deleteFileAsyncCallback := nil; - filesystem.deleteDirAsyncCallback := nil; driver.storage.fs.mgr.register_filesystem(@filesystem); io.syslog.logln('FAT32', 'init: done'); diff --git a/src/driver/storage/fs/driver.storage.fs.flatfs.pas b/src/driver/storage/fs/driver.storage.fs.flatfs.pas index bf1f7fec..d679c5ba 100644 --- a/src/driver/storage/fs/driver.storage.fs.flatfs.pas +++ b/src/driver/storage/fs/driver.storage.fs.flatfs.pas @@ -869,7 +869,8 @@ end; function readFileAtOffset(volume : PStorage_Volume; directory : pchar; fileName : pchar; offset : uint32; - buffer : puint32; byteCount : uint32) : uint32; + buffer : puint32; byteCount : uint32; + ctx : pointer) : uint32; var fileEntries : PFile_Entry; fileCount : uint32; diff --git a/src/driver/storage/fs/driver.storage.fs.iso9660.pas b/src/driver/storage/fs/driver.storage.fs.iso9660.pas index d96e5e4c..c3f7d767 100644 --- a/src/driver/storage/fs/driver.storage.fs.iso9660.pas +++ b/src/driver/storage/fs/driver.storage.fs.iso9660.pas @@ -649,7 +649,8 @@ end; function readFileAtOffset(volume : PStorage_Volume; directory : pchar; fileName : pchar; offset : uint32; - buffer : puint32; byteCount : uint32) : uint32; + buffer : puint32; byteCount : uint32; + ctx : pointer) : uint32; var buf : puint32; pvd : PPVD; diff --git a/src/driver/storage/vol/driver.storage.vol.mgr.pas b/src/driver/storage/vol/driver.storage.vol.mgr.pas index 26471497..6e5c69da 100644 --- a/src/driver/storage/vol/driver.storage.vol.mgr.pas +++ b/src/driver/storage/vol/driver.storage.vol.mgr.pas @@ -530,8 +530,8 @@ begin end; end; - { Write cached mbr to disk — buffer is the persistent cache } - driver.storage.mgr.storage_write_async(device, 0, 1, puint32(mbr), callback, callbackData); + { Write cached mbr to disk through the storage manager's staged MBR path. } + driver.storage.mgr.write_mbr_async(device, mbr, callback, callbackData); end; procedure remove_partition_async(device : PStorage_Device; slot : uint32; @@ -563,8 +563,8 @@ begin if (oldPart.LBA_start <> 0) and (oldPart.sector_count <> 0) then remove_volume_by_start(device, oldPart.LBA_start); - { Write cached mbr to disk } - driver.storage.mgr.storage_write_async(device, 0, 1, puint32(mbr), callback, callbackData); + { Write cached mbr to disk through the storage manager's staged MBR path. } + driver.storage.mgr.write_mbr_async(device, mbr, callback, callbackData); end; procedure init_disk_async(device : PStorage_Device; @@ -594,8 +594,8 @@ begin PMaster_Boot_Record(cache)^.boot_sector := $AA55; device^.cachedMBR := pointer(cache); - { Write to disk } - driver.storage.mgr.storage_write_async(device, 0, 1, cache, callback, callbackData); + { Write to disk through the storage manager's staged MBR path. } + driver.storage.mgr.write_mbr_async(device, PMaster_Boot_Record(cache), callback, callbackData); end; { ===================== Filesystem operations ===================== } diff --git a/src/proc/proc.types.pas b/src/proc/proc.types.pas index c5a7bded..dc4848ea 100644 --- a/src/proc/proc.types.pas +++ b/src/proc/proc.types.pas @@ -19,7 +19,7 @@ const PROCESS_STACK_SIZE = 8192; { Base quantum multiplier: quantum = Priority * BASE_QUANTUM ticks } - BASE_QUANTUM = 5; + BASE_QUANTUM = 1; type { Forward declaration } -- 2.54.0 From aadc59f83156e181c729dfa643f400efcf6b85cc Mon Sep 17 00:00:00 2001 From: Aaron Date: Mon, 16 Mar 2026 21:18:44 +0000 Subject: [PATCH 05/21] fat32 rewrite and storage stability fixes --- plan.md | 500 ++ src/app/app.diskutil.pas | 133 +- src/app/app.filebrowser.pas | 76 +- src/app/app.iotest.pas | 510 +- src/driver/storage/driver.storage.mgr.pas | 12 +- src/driver/storage/driver.storage.vfs.pas | 67 +- .../fs/driver.storage.fs.fat32.old.pas | 4650 +++++++++++++++++ .../storage/fs/driver.storage.fs.fat32.pas | 4617 +--------------- .../storage/fs/driver.storage.fs.flatfs.pas | 31 +- .../fs/fat32/driver.storage.fs.fat32.core.pas | 2403 +++++++++ .../driver.storage.fs.fat32.transfer.pas | 410 ++ .../fat32/driver.storage.fs.fat32.types.pas | 222 + .../storage/vol/driver.storage.vol.mgr.pas | 238 +- 13 files changed, 9163 insertions(+), 4706 deletions(-) create mode 100644 plan.md create mode 100644 src/driver/storage/fs/driver.storage.fs.fat32.old.pas create mode 100644 src/driver/storage/fs/fat32/driver.storage.fs.fat32.core.pas create mode 100644 src/driver/storage/fs/fat32/driver.storage.fs.fat32.transfer.pas create mode 100644 src/driver/storage/fs/fat32/driver.storage.fs.fat32.types.pas diff --git a/plan.md b/plan.md new file mode 100644 index 00000000..b4c20cb5 --- /dev/null +++ b/plan.md @@ -0,0 +1,500 @@ +# FAT32 Rewrite Plan + +Implement this entire plan without stopping. + +## Goal + +Replace the current FAT32 runtime I/O architecture with a simpler, faster design where open-time metadata work builds a cached runtime view, and steady-state file reads and writes consume that cached view with direct storage I/O. + +The rewrite should make the common path thin enough that contiguous, preopened reads are near raw block I/O performance, while preserving correctness for fragmented files, append/growth, and metadata updates. + +## Immediate Direction + +1. Rename the current FAT32 implementation file to `driver.storage.fs.fat32.old.pas`. +2. Create a new `driver.storage.fs.fat32.pas` as the public registration/entry unit. +3. Move the new implementation into a dedicated `fat32/` folder with multiple focused units instead of another monolithic source file. +4. Rebuild the internals from a simpler architecture instead of continuing to patch the old design. + +The old file remains as a reference during migration and can be deleted after parity is reached. + +## Core Problems To Fix + +1. The current runtime path is overcomplicated. +2. Ordinary file I/O pays too much control-plane overhead. +3. Fragmented files are treated as a special slow path instead of a normal case. +4. The async API still hides synchronous behavior in some cases. +5. FAT and directory metadata work leaks into the steady-state transfer loop. +6. The current source layout is too large to maintain comfortably as one unit. + +## Design Principles + +1. Fragmentation is normal, not a special case. +2. File I/O should be run-map driven. +3. FAT is metadata, not the runtime transfer engine. +4. The common path must avoid `kalloc`/`kfree`. +5. Async APIs must actually be async. +6. The same architecture must handle 1 run or 100 runs. +7. Metadata work should be deferred or isolated from the hot path. +8. Ownership and lifetime rules must be explicit at the file, volume, and transfer levels. + +## Architectural Layers + +The new design should keep three layers separate so FAT policy does not leak back into the transfer engine. + +### A. FAT Metadata Layer + +Responsible for: + +- boot record and FAT geometry interpretation +- FAT entry reads/writes +- FAT chain traversal +- directory entry lookup +- raw directory entry locator resolution +- directory entry update/commit +- cluster allocation/free +- run-map build/rebuild +- corruption detection and validation + +### B. Open-File State Layer + +Responsible for: + +- pointer/reference to the mounted volume +- pointer/reference to shared immutable `TFATVolumeInfo` +- first cluster +- committed byte size +- allocated cluster count +- cached run map +- logical dirty state +- reusable scratch sector buffer +- sequential cursor hints +- cached raw directory entry location +- open-handle lifetime and teardown bookkeeping + +### C. Transfer Layer + +Responsible for: + +- offset/length to run slicing +- direct storage I/O submission +- partial-sector read-modify-write handling +- async continuation/chaining +- completed-byte accounting +- EOF/capacity bounds enforcement + +The transfer layer must not perform FAT scans, directory searches, or cluster allocation from IRQ/completion context. + +## Runtime View: Runs, Not FAT Walks + +Define the runtime view explicitly: + +- run = contiguous range of file clusters on disk +- run map = ordered in-memory array of all file runs for one open handle + +The run map is the authoritative transfer view for that handle. FAT is consulted only during open, growth, explicit rebuild/validation, or metadata commit. + +Fragmentation only changes how many runs are walked. It must not select a different architecture. + +## Volume Geometry And Shared State + +Do not copy boot/FAT geometry into every file handle. + +Use a shared per-volume immutable record such as `TFATVolumeInfo` containing: + +- sector size +- sectors per cluster +- FAT start +- FAT size +- data start +- root cluster +- max cluster +- other immutable geometry/cache references needed for fast translation + +Each open file should hold: + +- `Volume` +- pointer/reference to `TFATVolumeInfo` + +## Open File State + +Each open file should own a compact cached state record such as `TFATOpenFile` containing: + +- `Volume` +- `VolumeInfo` +- `FirstCluster` +- `ByteSize` +- `AllocClusters` +- run map +- sequential cursor hint +- reusable scratch sector buffer +- dirty allocation / dirty size / dirty dir-entry flags +- raw directory entry locator + +This state replaces repeated rediscovery and repeated FAT walking during normal I/O. + +### Raw Directory Entry Locator + +Every open file must cache enough information to update the on-disk directory entry without rescanning the parent directory. The cached form should include either: + +- parent directory first cluster plus raw entry location data + +or a fully resolved raw location such as: + +- raw directory entry cluster +- raw sector LBA +- entry index within sector + +This is required to avoid regressions in first-cluster and size updates. + +### File Size And Allocation State + +The plan must keep these rules explicit: + +- `ByteSize` = committed logical EOF +- `AllocClusters` = current allocated chain length +- allocated capacity = `AllocClusters * ClusterSize` +- writes may use existing allocated capacity without allocation growth +- `ByteSize` advances only from actual completed writes, never intended request length + +This directly protects against partial-growth and short-write corruption. + +### Empty File Creation Model + +For a newly created empty file: + +- `FirstCluster` may be `0` +- run map is initially empty +- `ByteSize = 0` +- `AllocClusters = 0` +- raw directory entry locator is known immediately +- first allocation happens only when a write requires storage + +## Run-Map Lifetime Rules + +This needs to be defined up front: + +1. Each open file owns its own run map. +2. The run map is built once at open and then trusted for that handle unless the same handle grows or explicitly rebuilds it. +3. Writes through that handle update that handle's run map incrementally in memory. +4. Other open handles are not kept coherent in the first rewrite. Their run maps and sizes are treated as open-time snapshots until reopen. +5. Concurrent write opens for the same file should be disallowed for the first rewrite, or explicitly treated as unsupported/undefined. Prefer rejecting the second writable open. +6. Flush/close commits directory entry and FAT changes from the handle's current state; optional full rebuild-and-validate before commit is a later hardening step, not part of the hot path. + +## Unified Transfer Engine + +Implement one transfer engine for both reads and writes. + +It should: + +- accept open-file state, buffer, offset, length, mode, callback +- translate byte ranges into run slices +- issue direct storage I/O chunk by chunk +- handle completion by advancing precomputed transfer state +- use scratch-sector read-modify-write only for unaligned edges +- complete without separate aligned/fragmented architectures + +The old special-case direct path should become the normal path. + +## Async Execution Model + +For the first rewrite, define one concrete model: + +- open/build-run-map may remain synchronous +- read/write entrypoints submit async storage I/O and return immediately +- transfer progression uses a small pooled `TFATTransferCtx` +- IRQ/completion callbacks only advance precomputed transfer state and schedule the next I/O or final completion +- IRQ/completion callbacks must not call helpers that can perform blocking FAT or directory I/O +- close/flush may remain synchronous initially if that keeps the contract honest and simple + +This keeps async behavior explicit and avoids hidden blocking inside async hooks. + +## Rare Heavy Work Only + +Only these operations should do heavier FAT or metadata work: + +- opening a file +- building or rebuilding a run map +- allocating/extending clusters +- updating directory entries +- flushing FAT or metadata +- partial-sector read-modify-write preparation +- corruption validation/failure handling + +These must not dominate steady-state transfer throughput. + +## Corruption And Validation Rules + +When FAT traversal or run-map build finds: + +- out-of-range cluster +- cycle +- bad-cluster marker +- premature EOC relative to stored size + +the first rewrite should fail the open or mark the file invalid for transfer. It should not attempt automatic repair. + +All corruption-handling loops must be bounded by hard limits derived from FAT size / max cluster count. + +## Required Runtime Invariants + +Invariant 1 + +For any open file: + +`sum(run.length_clusters) = AllocClusters` + +Invariant 2 + +Committed logical size must satisfy: + +`ByteSize <= AllocClusters * ClusterSize` + +Invariant 3 + +On successful flush/close: + +- directory entry first cluster matches open state +- directory entry size matches committed bytes +- FAT chain matches the run map if dirty allocation occurred + +Invariant 4 + +No async transfer may outlive: + +- file handle +- volume object +- transfer context backing storage + +Invariant 5 + +The run map is the authoritative transfer view for the handle; FAT is not re-walked in the steady-state transfer path. + +## What To Remove From The New Design + +The new implementation should avoid carrying forward these ideas: + +- worker-centric per-chunk file transfer orchestration +- separate fast path and general path for aligned vs fragmented I/O +- synchronous fallbacks hidden inside async hooks +- repeated FAT-chain walking during ordinary sequential transfer +- hot-path temporary allocations +- parent-directory rescans just to commit file size or first cluster + +## Planned Source Layout + +The rewrite should move FAT32 internals into a dedicated folder so the code remains readable and reviewable. + +Planned shape: + +- `src/driver/storage/fs/driver.storage.fs.fat32.pas` for public registration and top-level hook wiring +- `src/driver/storage/fs/driver.storage.fs.fat32.old.pas` for the temporary legacy reference +- `src/driver/storage/fs/fat32/` for focused internal units + +Likely internal unit split: + +- volume / geometry +- FAT metadata helpers +- directory lookup and raw entry location +- open-file state and handle lifetime +- run-map construction +- transfer engine +- allocation/growth +- flush/sync + +Exact file names can be finalized during Phase 0, but the rewrite should not collapse back into one large source file. + +## Migration Plan + +### Phase 0: File Split And Scaffolding + +1. Rename existing FAT32 unit source to `driver.storage.fs.fat32.old.pas`. +2. Create a new `driver.storage.fs.fat32.pas` that preserves the public filesystem registration contract. +3. Create the `src/driver/storage/fs/fat32/` folder and split new internals into focused units from the start. + +### Phase 1A: Volume Geometry And FAT Primitives + +Build and validate: + +- `TFATVolumeInfo` +- shared geometry decode +- FAT entry read helper +- bounded FAT chain traversal helper +- run-map builder +- raw directory entry locator helper + +At the end of this phase, the core metadata primitives are usable without the old runtime architecture. + +### Phase 1B: Minimal Read-Only Mount/Open + +Build the new unit with only: + +- type definitions needed by the filesystem manager +- boot record and directory entry structures +- volume identify/detect hooks +- basic open/close hooks +- read-only run-map construction + +At the end of this phase, the new unit should compile, mount a FAT32 volume, open files, and build read-only run maps. + +### Phase 2: Read Path First + +Implement a unified run-driven read engine: + +- build run map at open +- read from runs directly +- support multi-run files without architectural fallback +- use a pooled transfer context +- use a reusable scratch sector buffer only for unaligned reads + +Acceptance target: + +- aligned sequential reads on contiguous preopened files should be near raw `STORBENCH` read speed +- fragmented reads should scale mainly with run count, not collapse architecturally + +### Phase 3: Writes Within Existing Allocation + +Implement overwrite and in-capacity write support on the same transfer model: + +- direct writes over existing runs +- partial-sector handling through reusable scratch buffer +- metadata dirtied in memory, not rewritten every chunk +- logical EOF advanced only from actual completed bytes + +Acceptance target: + +- aligned writes over preallocated files use the same thin transfer engine as reads +- no per-chunk metadata rewrite + +### Phase 4: Allocation And Growth + +Implement append and extension logic: + +- allocate clusters when writes exceed current capacity +- extend the open-file run map incrementally +- prefer contiguous growth when possible +- keep allocation outside the steady-state transfer loop +- handle first-cluster update for empty files + +Acceptance target: + +- append-heavy writes remain much closer to preallocated write performance than today +- fragmented growth still remains run-driven + +### Phase 5: Metadata Commit Rules + +Implement clean flush/close behavior: + +- FAT dirty tracking +- directory entry update on close or explicit sync +- first-cluster update for newly created files +- byte-size commit from actual completed bytes only +- handle/volume teardown rules for in-flight transfers + +Acceptance target: + +- no regression in crash-safety compared with current design assumptions +- no hot-path metadata thrash + +### Phase 6: Async Contract Cleanup + +Make the async API honest and documented: + +- async hooks always return immediately +- no synchronous disk I/O hidden inside async entrypoints +- callback context is consistent and documented +- FD lifetime and teardown rules are enforced +- close/unmount/process-exit races are explicitly handled + +Acceptance target: + +- `ReadFileAsync` and `WriteFileAsync` are genuinely async for all supported cases +- no blocking FAT work is re-entered from IRQ/completion context + +## Required Supporting Structures + +The new implementation should include: + +- `TFATVolumeInfo` for shared immutable geometry +- `TFATOpenFile` with run map, logical state, raw directory entry locator, and reusable scratch buffer +- pooled `TFATTransferCtx` +- compact run representation +- read/write helpers that work from run maps, not repeated FAT scans +- explicit flush/sync helpers + +It should not require a per-volume worker just to progress ordinary file transfers. + +## Compatibility Requirements + +The new implementation should preserve compatibility with: + +- `driver.storage.fs.mgr` +- `driver.storage.vfs` +- current filesystem hook signatures in `driver.storage.types` +- current FAT32 on-disk format + +The rewrite should change the internal architecture, not the external filesystem contract, unless a later cleanup intentionally revises that contract. + +## Performance Targets + +Track both open/close cost and steady-state transfer throughput. + +Initial realistic targets: + +- reads on contiguous preopened files should be near raw block I/O throughput +- writes on preallocated files should be much closer to raw than today +- append/growth will remain measurably slower due to FAT and directory updates +- the remaining gap to raw should be mostly metadata/allocation cost, not control-plane overhead + +Longer-term target: + +- once FAT32 overhead is reduced, remaining limits should mainly be in storage/AHCI + +## Correctness Requirements + +Must explicitly preserve: + +- correct handling of fragmented files +- correct file size updates +- correct first-cluster updates +- correct directory entry location handling +- no shared DMA-buffer hazards +- no blocking FAT work from IRQ callbacks +- no use-after-free during close/unmount/process teardown +- bounded handling of bad or cyclic FAT chains +- correct behavior for empty files with `FirstCluster = 0` + +## Validation Plan + +1. Compile after each phase. +2. Smoke-test mount/open/read on a known FAT32 volume. +3. Measure open/close cost separately from transfer throughput. +4. Run `IOTEST` on a fresh 4 KB cluster FAT32 volume. +5. Run `STORBENCH` on the same disk and compare. +6. Test fragmented-file read/write behavior. +7. Test create, append, close, reopen, and verify. +8. Test writable-open rejection or unsupported-path behavior for concurrent writers. +9. Test close-during-async and volume invalidation edge cases. +10. Test corruption detection on invalid or cyclic cluster chains. + +## Non-Goals For The First Rewrite + +These are explicitly not required for the first clean rewrite: + +- perfect crash-consistency semantics +- journaled metadata behavior +- aggressive readahead/writeback cache +- cross-handle cache coherency for concurrent file writers +- automatic FAT repair + +Those can come after the architecture is clean. + +## Definition Of Success + +The rewrite is successful when: + +1. FAT32 ordinary file I/O is run-driven by default. +2. The old direct path no longer exists as a separate idea because it became the main path. +3. Async hooks are genuinely async. +4. Hot-path allocator churn is removed. +5. Directory entry updates no longer require parent-directory rescans during ordinary file writes. +6. FAT32 throughput is no longer dominated by internal orchestration overhead. diff --git a/src/app/app.diskutil.pas b/src/app/app.diskutil.pas index fe62b997..35abade3 100644 --- a/src/app/app.diskutil.pas +++ b/src/app/app.diskutil.pas @@ -32,7 +32,8 @@ uses debug.tracer, driver.video, driver.storage.vol.mgr, - driver.video.windows; + driver.video.windows, + core.util, arch.x86.util; const WIN_W = 740; @@ -62,6 +63,18 @@ var add_err_lbl : Plv_obj; { Sidebar item tracking for highlight } active_sidebar_btn : Plv_obj; + fmt_poll_timer : Plv_timer; + fmt_done_flag : uint32; + fmt_done_error : TError; + fmt_done_vol_idx : uint32; + ui_session_id : uint32; + +type + PFmtUICallbackCtx = ^TFmtUICallbackCtx; + TFmtUICallbackCtx = record + SessionID : uint32; + VolumeIdx : uint32; + end; { ============================================================ Forward declarations @@ -73,6 +86,7 @@ procedure refreshSidebar; forward; procedure showDeviceDetail(devIdx: uint32); forward; procedure showVolumeDetail(volIdx: uint32); forward; procedure clearDetail; forward; +procedure fmt_poll_cb(tmr: Plv_timer); cdecl; forward; { ============================================================ Helpers — size formatting @@ -125,6 +139,53 @@ begin else boolStr := 'No'; end; +function formatErrorName(err : TError) : pchar; +begin + case err of + eNone: formatErrorName := 'eNone'; + eUnknown: formatErrorName := 'eUnknown'; + eNotSupported: formatErrorName := 'eNotSupported'; + eOutOfMemory: formatErrorName := 'eOutOfMemory'; + eInvalidArgument: formatErrorName := 'eInvalidArgument'; + eFileInUse: formatErrorName := 'eFileInUse'; + eFileDoesNotExist: formatErrorName := 'eFileDoesNotExist'; + eInvalidFileName: formatErrorName := 'eInvalidFileName'; + eInvalidFileExtension: formatErrorName := 'eInvalidFileExtension'; + eFilenameTooLong: formatErrorName := 'eFilenameTooLong'; + eDirectoryDoesNotExist: formatErrorName := 'eDirectoryDoesNotExist'; + eDirectoryAlreadyExists: formatErrorName := 'eDirectoryAlreadyExists'; + eDirectoryNotEmpty: formatErrorName := 'eDirectoryNotEmpty'; + eDirectoryFull: formatErrorName := 'eDirectoryFull'; + eNotADirectory: formatErrorName := 'eNotADirectory'; + eWriteOnly: formatErrorName := 'eWriteOnly'; + eReadOnly: formatErrorName := 'eReadOnly'; + ePermissionDenied: formatErrorName := 'ePermissionDenied'; + eInvalidPath: formatErrorName := 'eInvalidPath'; + eTooManyOpenFiles: formatErrorName := 'eTooManyOpenFiles'; + eInvalidHandle: formatErrorName := 'eInvalidHandle'; + eFileNotLoaded: formatErrorName := 'eFileNotLoaded'; + eAlreadyExists: formatErrorName := 'eAlreadyExists'; + eDiskFull: formatErrorName := 'eDiskFull'; + eIOError: formatErrorName := 'eIOError'; + eIOTimeout: formatErrorName := 'eIOTimeout'; + eIOCancelled: formatErrorName := 'eIOCancelled'; + eDeviceNotReady: formatErrorName := 'eDeviceNotReady'; + eCorruptFilesystem: formatErrorName := 'eCorruptFilesystem'; + eBadSector: formatErrorName := 'eBadSector'; + eAlreadyMounted: formatErrorName := 'eAlreadyMounted'; + eNotMounted: formatErrorName := 'eNotMounted'; + eUnsupportedFilesystem: formatErrorName := 'eUnsupportedFilesystem'; + eDeviceNotFound: formatErrorName := 'eDeviceNotFound'; + eDeviceRemoved: formatErrorName := 'eDeviceRemoved'; + eQueueFull: formatErrorName := 'eQueueFull'; + eNoFreeSlot: formatErrorName := 'eNoFreeSlot'; + eInvalidPartitionTable: formatErrorName := 'eInvalidPartitionTable'; + eVolumeNotFound: formatErrorName := 'eVolumeNotFound'; + else + formatErrorName := 'eUnknown'; + end; +end; + { Check if a pchar string contains only digits } function isNumericStr(s: pchar): boolean; var @@ -453,6 +514,7 @@ begin end; end; + memset(uint32(@part), 0, sizeof(TPartition_table)); driver.storage.vol.mbr.setup_partition(@part, lba, sectors); driver.storage.vol.mgr.add_partition_async(device, uint32(slot), part, nil, nil); @@ -552,12 +614,41 @@ end; ============================================================ } { Async completion callback — fired from ISR context when format finishes. - Safe to touch LVGL here because LVGL and driver.storage.ctl.ahci ISRs are naturally - serialised (both run with IF=0). } + Only sets completion flags; LVGL work happens later on the UI poll timer. } procedure fmt_done_cb(error : TError; userdata : pointer); +var + ctx : PFmtUICallbackCtx; begin + ctx := PFmtUICallbackCtx(userdata); + if ctx <> nil then begin + if ctx^.SessionID = ui_session_id then begin + fmt_done_error := error; + fmt_done_vol_idx := ctx^.VolumeIdx; + puint32(@fmt_done_flag)^ := 1; + end; + kfree(void(ctx)); + end; +end; + +procedure fmt_poll_cb(tmr: Plv_timer); cdecl; +var + mbox : Plv_obj; +begin + if puint32(@fmt_done_flag)^ <> 1 then exit; + puint32(@fmt_done_flag)^ := 0; + refreshSidebar; - showVolumeDetail(sel_vol_idx); + showVolumeDetail(fmt_done_vol_idx); + + if fmt_done_error <> eNone then begin + closeMsgBox; + mbox := lv_msgbox_create(lv_layer_top); + active_mbox := mbox; + lv_msgbox_add_title(mbox, 'Format Failed'); + lv_msgbox_add_text(mbox, 'The volume was formatted but could not be verified.'); + lv_msgbox_add_text(mbox, formatErrorName(fmt_done_error)); + lv_msgbox_add_footer_button(mbox, 'OK'); + end; end; procedure fmt_ok_cb(e: Plv_event); cdecl; @@ -566,6 +657,7 @@ var selIdx : uint32; fs : PFilesystem; vol : PStorage_Volume; + cbCtx : PFmtUICallbackCtx; begin code := lv_event_get_code(e); if code <> LV_EVENT_CLICKED then exit; @@ -579,9 +671,21 @@ begin closeMsgBox; - { Submit async format — returns immediately, fmt_done_cb fires on completion } - driver.storage.vol.mgr.format_volume_async(vol^.device, sel_vol_idx, fs^.sName, nil, - @fmt_done_cb, nil); + cbCtx := PFmtUICallbackCtx(kalloc(sizeof(TFmtUICallbackCtx))); + if cbCtx = nil then exit; + cbCtx^.SessionID := ui_session_id; + cbCtx^.VolumeIdx := sel_vol_idx; + + { Submit async format — returns immediately, fmt_done_cb only flips a flag. } + if not driver.storage.vol.mgr.format_volume_async(vol^.device, sel_vol_idx, fs^.sName, nil, + @fmt_done_cb, cbCtx) then begin + kfree(void(cbCtx)); + closeMsgBox; + active_mbox := lv_msgbox_create(lv_layer_top); + lv_msgbox_add_title(active_mbox, 'Format Failed'); + lv_msgbox_add_text(active_mbox, 'Could not start the format operation.'); + lv_msgbox_add_footer_button(active_mbox, 'OK'); + end; end; procedure fmt_cancel_cb(e: Plv_event); cdecl; @@ -1292,6 +1396,12 @@ procedure onClose(wid: uint32); begin debug.tracer.push_trace('diskutil.onClose'); closeMsgBox; + ui_session_id := ui_session_id + 1; + puint32(@fmt_done_flag)^ := 0; + if fmt_poll_timer <> nil then begin + lv_timer_delete(fmt_poll_timer); + fmt_poll_timer := nil; + end; if proc_pid <> 0 then begin proc.mgr.kill(proc_pid); proc_pid := 0; @@ -1399,6 +1509,10 @@ begin add_textarea := nil; add_err_lbl := nil; active_sidebar_btn := nil; + fmt_done_flag := 0; + fmt_done_error := eNone; + fmt_done_vol_idx := 0; + fmt_poll_timer := lv_timer_create(@fmt_poll_cb, 50, nil); refreshSidebar; @@ -1431,6 +1545,11 @@ begin add_textarea := nil; add_err_lbl := nil; active_sidebar_btn := nil; + fmt_poll_timer := nil; + fmt_done_flag := 0; + fmt_done_error := eNone; + fmt_done_vol_idx := 0; + ui_session_id := 1; driver.video.desktop.registerProgram('Disk Utility', @launch); debug.tracer.pop_trace; end; diff --git a/src/app/app.filebrowser.pas b/src/app/app.filebrowser.pas index 0305e63c..32a01157 100644 --- a/src/app/app.filebrowser.pas +++ b/src/app/app.filebrowser.pas @@ -195,6 +195,7 @@ procedure do_refresh_load(state: PFileBrowserState); forward; procedure do_refresh_build(state: PFileBrowserState); forward; procedure schedule_refresh(state: PFileBrowserState); forward; procedure fb_refresh_poll_cb(tmr: Plv_timer); cdecl; forward; +function fb_error_name(err : TError) : pchar; forward; procedure fb_collect_cb(key: pchar; data: void; ud: void); forward; procedure do_refresh(state: PFileBrowserState); forward; procedure navigate_to(state: PFileBrowserState; newDir: pchar); forward; @@ -582,6 +583,51 @@ begin puint32(@state^.refresh_pending)^ := 1; end; +function fb_error_name(err : TError) : pchar; +begin + case err of + eNone: fb_error_name := 'eNone'; + eUnknown: fb_error_name := 'eUnknown'; + eNotSupported: fb_error_name := 'eNotSupported'; + eOutOfMemory: fb_error_name := 'eOutOfMemory'; + eInvalidArgument: fb_error_name := 'eInvalidArgument'; + eFileInUse: fb_error_name := 'eFileInUse'; + eFileDoesNotExist: fb_error_name := 'eFileDoesNotExist'; + eInvalidFileName: fb_error_name := 'eInvalidFileName'; + eInvalidFileExtension: fb_error_name := 'eInvalidFileExtension'; + eFilenameTooLong: fb_error_name := 'eFilenameTooLong'; + eDirectoryDoesNotExist: fb_error_name := 'eDirectoryDoesNotExist'; + eDirectoryAlreadyExists: fb_error_name := 'eDirectoryAlreadyExists'; + eDirectoryNotEmpty: fb_error_name := 'eDirectoryNotEmpty'; + eDirectoryFull: fb_error_name := 'eDirectoryFull'; + eNotADirectory: fb_error_name := 'eNotADirectory'; + eWriteOnly: fb_error_name := 'eWriteOnly'; + eReadOnly: fb_error_name := 'eReadOnly'; + ePermissionDenied: fb_error_name := 'ePermissionDenied'; + eInvalidPath: fb_error_name := 'eInvalidPath'; + eTooManyOpenFiles: fb_error_name := 'eTooManyOpenFiles'; + eInvalidHandle: fb_error_name := 'eInvalidHandle'; + eFileNotLoaded: fb_error_name := 'eFileNotLoaded'; + eAlreadyExists: fb_error_name := 'eAlreadyExists'; + eDiskFull: fb_error_name := 'eDiskFull'; + eIOError: fb_error_name := 'eIOError'; + eIOTimeout: fb_error_name := 'eIOTimeout'; + eIOCancelled: fb_error_name := 'eIOCancelled'; + eDeviceNotReady: fb_error_name := 'eDeviceNotReady'; + eDeviceRemoved: fb_error_name := 'eDeviceRemoved'; + eDeviceNotFound: fb_error_name := 'eDeviceNotFound'; + eQueueFull: fb_error_name := 'eQueueFull'; + eNoFreeSlot: fb_error_name := 'eNoFreeSlot'; + eCorruptFilesystem: fb_error_name := 'eCorruptFilesystem'; + eBadSector: fb_error_name := 'eBadSector'; + eAlreadyMounted: fb_error_name := 'eAlreadyMounted'; + eNotMounted: fb_error_name := 'eNotMounted'; + eUnsupportedFilesystem: fb_error_name := 'eUnsupportedFilesystem'; + eInvalidPartitionTable: fb_error_name := 'eInvalidPartitionTable'; + eVolumeNotFound: fb_error_name := 'eVolumeNotFound'; + end; +end; + { fb_refresh_poll_cb — 50ms LVGL poll timer; when rd_ready=1, build UI } procedure fb_refresh_poll_cb(tmr: Plv_timer); cdecl; var @@ -604,9 +650,12 @@ begin case state^.wop_kind of 1: lv_msgbox_add_text(mbox, 'Could not create folder.'); 2: lv_msgbox_add_text(mbox, 'Could not rename item.'); + 3: lv_msgbox_add_text(mbox, 'Could not create file.'); + 4: lv_msgbox_add_text(mbox, 'Could not delete item.'); else lv_msgbox_add_text(mbox, 'Operation failed.'); end; + lv_msgbox_add_text(mbox, fb_error_name(state^.wop_result)); ok_b := lv_msgbox_add_footer_button(mbox, 'OK'); lv_obj_add_event_cb(ok_b, @fb_mbox_close_cb, LV_EVENT_CLICKED, mbox); end; @@ -619,8 +668,8 @@ begin kfree(void(state^.wop_path2)); state^.wop_path2 := nil; end; - { For rename, clear selection } - if state^.wop_kind = 2 then + { For rename/delete, clear selection } + if (state^.wop_kind = 2) or (state^.wop_kind = 4) then clear_selection(state); schedule_refresh(state); end; @@ -1591,15 +1640,16 @@ begin @fb_delete_done, state); end; -{ Async completion callback — runs in ISR/worker context, defer to LVGL } +{ Async completion callback — runs outside the LVGL thread, so only set flags. } procedure fb_delete_done(error: TError; userdata: pointer); var state: PFileBrowserState; begin state := PFileBrowserState(userdata); if state = nil then exit; - state^.op_err := error; - lv_timer_create(@fb_delete_done_timer, 1, state); + state^.wop_kind := 4; + state^.wop_result := error; + puint32(@state^.wop_done)^ := 1; end; { Deferred UI update after delete } @@ -1864,11 +1914,16 @@ begin end else begin if nfctx^.handle <> 0 then driver.storage.vfs.CloseFile(nfctx^.handle); - lv_timer_create(@fb_newfile_done_timer, 1, nfctx); + if nfctx^.state <> nil then begin + nfctx^.state^.wop_kind := 3; + nfctx^.state^.wop_result := nfctx^.err; + puint32(@nfctx^.state^.wop_done)^ := 1; + end; + kfree(void(nfctx)); end; end; -{ Async step 2: write complete — close handle and schedule UI refresh } +{ Async step 2: write complete — close handle and defer UI work via poll flags } procedure fb_newfile_written(error: TError; userdata: pointer); var nfctx : PNewFileCtx; @@ -1878,7 +1933,12 @@ begin if error <> eNone then nfctx^.err := error; if nfctx^.handle <> 0 then driver.storage.vfs.CloseFile(nfctx^.handle); - lv_timer_create(@fb_newfile_done_timer, 1, nfctx); + if nfctx^.state <> nil then begin + nfctx^.state^.wop_kind := 3; + nfctx^.state^.wop_result := nfctx^.err; + puint32(@nfctx^.state^.wop_done)^ := 1; + end; + kfree(void(nfctx)); end; procedure fb_newfile_done_timer(tmr: Plv_timer); cdecl; diff --git a/src/app/app.iotest.pas b/src/app/app.iotest.pas index ad9b1bd9..0ead3324 100644 --- a/src/app/app.iotest.pas +++ b/src/app/app.iotest.pas @@ -1,12 +1,13 @@ { - App->IOTest - Write and read-back a 10 MB file to exercise offset-based I/O. + App->IOTest - Write and read-back a file to exercise offset-based I/O. - Usage: IOTEST [path] + Usage: IOTEST [size] [path] + Default size: 25 MB Default path: /disk/vol0/IOTEST.BIN - Writes 10 MB in 4 KB chunks with a rotating byte pattern, then reads - the file back and verifies every byte. Reports throughput (KB/s) - for both the write and the read phase. + Writes the requested size in chunks with a rotating byte pattern, then + reads the file back and verifies every byte. Reports pure I/O throughput + separately from buffer fill and verification overhead. @author(Aaron Hance ) } @@ -24,21 +25,133 @@ implementation uses driver.storage.vfs, driver.storage.types, memory.heap, core.util, arch.x86.util, - core.strings, arch.x86.bda; + core.strings, arch.x86.bda, + proc.mgr, proc.types; const - FILE_SIZE = 25 * 1024 * 1024; { 25 MB } + DEFAULT_FILE_SIZE = 25 * 1024 * 1024; { 25 MB } CHUNK_SIZE = 4096*128; { MB per is 4k*128 = 512 KB, so 50 chunks total } TICK_HZ = 1024; { timer ISR frequency } + IO_WAIT_TIMEOUT_TICKS = 10 * TICK_HZ; + DELETE_TIMEOUT_TICKS = 5 * TICK_HZ; DEFAULT_PATH : pchar = '/disk/vol0/IOTEST.BIN'; -{ Fill buf[0..CHUNK_SIZE-1] with a deterministic pattern seeded by chunkIdx } -procedure fillPattern(buf : puint8; chunkIdx : uint32); +type + TIOTSyncWaitCtx = record + Done : uint32; + Error : TError; + BytesDone : uint32; + Process : proc.types.PProcessContext; + end; + PIOTSyncWaitCtx = ^TIOTSyncWaitCtx; + +function isNumericStr(s : pchar) : boolean; var i : uint32; begin - if CHUNK_SIZE > 0 then - for i := 0 to CHUNK_SIZE - 1 do + isNumericStr := false; + if (s = nil) or (s[0] = char(0)) then exit; + i := 0; + while s[i] <> char(0) do begin + if (s[i] < '0') or (s[i] > '9') then exit; + i := i + 1; + end; + isNumericStr := true; +end; + +function parseSizeBytes(s : pchar) : uint32; +var + len : uint32; + idx : uint32; + digitsEnd: uint32; + value : uint32; + unit1 : char; + unit2 : char; +begin + parseSizeBytes := 0; + if s = nil then exit; + len := stringSize(s); + if len = 0 then exit; + + idx := 0; + value := 0; + while (idx < len) and (s[idx] >= '0') and (s[idx] <= '9') do begin + value := (value * 10) + uint32(uint8(s[idx]) - uint8('0')); + idx := idx + 1; + end; + if (idx = 0) or (value = 0) then exit; + + digitsEnd := idx; + unit1 := #0; + unit2 := #0; + if idx < len then begin + unit1 := s[idx]; + idx := idx + 1; + end; + if idx < len then begin + unit2 := s[idx]; + idx := idx + 1; + end; + if idx <> len then exit; + + if unit1 = #0 then begin + parseSizeBytes := value; + exit; + end; + + if (unit2 <> #0) and (unit2 <> 'B') and (unit2 <> 'b') then + exit; + + if (digitsEnd + 2 = len) and ((unit1 = 'K') or (unit1 = 'k')) then + parseSizeBytes := value * 1024 + else if (digitsEnd + 2 = len) and ((unit1 = 'M') or (unit1 = 'm')) then + parseSizeBytes := value * 1048576 + else if (digitsEnd + 2 = len) and ((unit1 = 'G') or (unit1 = 'g')) then + parseSizeBytes := value * 1073741824 + else if (digitsEnd + 1 = len) and ((unit1 = 'B') or (unit1 = 'b')) then + parseSizeBytes := value + else begin + if unit2 <> #0 then + exit; + if ((unit1 = 'K') or (unit1 = 'k')) then + parseSizeBytes := value * 1024 + else if ((unit1 = 'M') or (unit1 = 'm')) then + parseSizeBytes := value * 1048576 + else if ((unit1 = 'G') or (unit1 = 'g')) then + parseSizeBytes := value * 1073741824; + end; +end; + +procedure writeSize(buf : POutBuf; bytes : uint32); +begin + if bytes >= 1073741824 then begin + io.stdio.bufWriteInt(buf, bytes div 1073741824); + io.stdio.bufWriteStr(buf, '.'); + io.stdio.bufWriteInt(buf, ((bytes mod 1073741824) * 10) div 1073741824); + io.stdio.bufWriteStr(buf, ' GB'); + end else if bytes >= 1048576 then begin + io.stdio.bufWriteInt(buf, bytes div 1048576); + io.stdio.bufWriteStr(buf, '.'); + io.stdio.bufWriteInt(buf, ((bytes mod 1048576) * 10) div 1048576); + io.stdio.bufWriteStr(buf, ' MB'); + end else if bytes >= 1024 then begin + io.stdio.bufWriteInt(buf, bytes div 1024); + io.stdio.bufWriteStr(buf, '.'); + io.stdio.bufWriteInt(buf, ((bytes mod 1024) * 10) div 1024); + io.stdio.bufWriteStr(buf, ' KB'); + end else begin + io.stdio.bufWriteInt(buf, bytes); + io.stdio.bufWriteStr(buf, ' B'); + end; +end; + +{ Fill buf[0..byteCount-1] with a deterministic pattern seeded by chunkIdx } +procedure fillPattern(buf : puint8; chunkIdx : uint32; byteCount : uint32); +var + i : uint32; +begin + if byteCount > 0 then + for i := 0 to byteCount - 1 do buf[i] := uint8((chunkIdx + i) and $FF); end; @@ -56,45 +169,224 @@ begin end; end; +procedure iot_sync_complete(error : TError; userdata : pointer); +var + ctx : PIOTSyncWaitCtx; +begin + ctx := PIOTSyncWaitCtx(userdata); + if ctx = nil then exit; + if ctx^.Process <> nil then + ctx^.Process^.State := psReady; + ctx^.Error := error; + puint32(@ctx^.Done)^ := 1; +end; + +procedure writeErrorName(buf : POutBuf; err : TError); +begin + case err of + eNone: io.stdio.bufWriteStr(buf, 'eNone'); + eUnknown: io.stdio.bufWriteStr(buf, 'eUnknown'); + eNotSupported: io.stdio.bufWriteStr(buf, 'eNotSupported'); + eOutOfMemory: io.stdio.bufWriteStr(buf, 'eOutOfMemory'); + eInvalidArgument: io.stdio.bufWriteStr(buf, 'eInvalidArgument'); + eFileInUse: io.stdio.bufWriteStr(buf, 'eFileInUse'); + eFileDoesNotExist: io.stdio.bufWriteStr(buf, 'eFileDoesNotExist'); + eInvalidFileName: io.stdio.bufWriteStr(buf, 'eInvalidFileName'); + eInvalidFileExtension: io.stdio.bufWriteStr(buf, 'eInvalidFileExtension'); + eFilenameTooLong: io.stdio.bufWriteStr(buf, 'eFilenameTooLong'); + eDirectoryDoesNotExist: io.stdio.bufWriteStr(buf, 'eDirectoryDoesNotExist'); + eDirectoryAlreadyExists: io.stdio.bufWriteStr(buf, 'eDirectoryAlreadyExists'); + eDirectoryNotEmpty: io.stdio.bufWriteStr(buf, 'eDirectoryNotEmpty'); + eDirectoryFull: io.stdio.bufWriteStr(buf, 'eDirectoryFull'); + eNotADirectory: io.stdio.bufWriteStr(buf, 'eNotADirectory'); + eWriteOnly: io.stdio.bufWriteStr(buf, 'eWriteOnly'); + eReadOnly: io.stdio.bufWriteStr(buf, 'eReadOnly'); + ePermissionDenied: io.stdio.bufWriteStr(buf, 'ePermissionDenied'); + eInvalidPath: io.stdio.bufWriteStr(buf, 'eInvalidPath'); + eTooManyOpenFiles: io.stdio.bufWriteStr(buf, 'eTooManyOpenFiles'); + eInvalidHandle: io.stdio.bufWriteStr(buf, 'eInvalidHandle'); + eFileNotLoaded: io.stdio.bufWriteStr(buf, 'eFileNotLoaded'); + eAlreadyExists: io.stdio.bufWriteStr(buf, 'eAlreadyExists'); + eDiskFull: io.stdio.bufWriteStr(buf, 'eDiskFull'); + eIOError: io.stdio.bufWriteStr(buf, 'eIOError'); + eIOTimeout: io.stdio.bufWriteStr(buf, 'eIOTimeout'); + eIOCancelled: io.stdio.bufWriteStr(buf, 'eIOCancelled'); + eDeviceNotReady: io.stdio.bufWriteStr(buf, 'eDeviceNotReady'); + eDeviceRemoved: io.stdio.bufWriteStr(buf, 'eDeviceRemoved'); + eDeviceNotFound: io.stdio.bufWriteStr(buf, 'eDeviceNotFound'); + eQueueFull: io.stdio.bufWriteStr(buf, 'eQueueFull'); + eNoFreeSlot: io.stdio.bufWriteStr(buf, 'eNoFreeSlot'); + eCorruptFilesystem: io.stdio.bufWriteStr(buf, 'eCorruptFilesystem'); + eBadSector: io.stdio.bufWriteStr(buf, 'eBadSector'); + eAlreadyMounted: io.stdio.bufWriteStr(buf, 'eAlreadyMounted'); + eNotMounted: io.stdio.bufWriteStr(buf, 'eNotMounted'); + eUnsupportedFilesystem: io.stdio.bufWriteStr(buf, 'eUnsupportedFilesystem'); + eInvalidPartitionTable: io.stdio.bufWriteStr(buf, 'eInvalidPartitionTable'); + eVolumeNotFound: io.stdio.bufWriteStr(buf, 'eVolumeNotFound'); + end; +end; + +function currentTicks : uint32; +begin + currentTicks := puint32(@arch.x86.bda.Counters.c32)^; +end; + +function waitForIOTask(var wait : TIOTSyncWaitCtx; timeoutTicks : uint32; var err : TError) : boolean; +var + startT : uint32; +begin + waitForIOTask := false; + startT := currentTicks; + + asm pushf; cli end; + if puint32(@wait.Done)^ = 0 then begin + if proc.mgr.CurrentProcess <> nil then + proc.mgr.CurrentProcess^.State := psAwaiting; + end; + asm popf end; + + while puint32(@wait.Done)^ = 0 do begin + if (timeoutTicks > 0) and ((currentTicks - startT) >= timeoutTicks) then begin + err := eIOTimeout; + if proc.mgr.CurrentProcess <> nil then + proc.mgr.CurrentProcess^.State := psReady; + exit; + end; + asm hlt end; + end; + + err := wait.Error; + waitForIOTask := wait.Error = eNone; +end; + +function writeFileWithError(fileHandle : TFileHandle; position : uint32; buffer : puint8; + length : uint32; var err : TError) : uint32; +var + wait : TIOTSyncWaitCtx; +begin + writeFileWithError := 0; + memset(uint32(@wait), 0, sizeof(TIOTSyncWaitCtx)); + wait.Error := eUnknown; + wait.Process := proc.mgr.CurrentProcess; + + driver.storage.vfs.WriteFileAsync(fileHandle, position, buffer, length, + @wait.BytesDone, @iot_sync_complete, @wait); + if waitForIOTask(wait, IO_WAIT_TIMEOUT_TICKS, err) then + writeFileWithError := wait.BytesDone; +end; + +function readFileWithError(fileHandle : TFileHandle; position : uint32; buffer : puint8; + length : uint32; var err : TError) : uint32; +var + wait : TIOTSyncWaitCtx; +begin + readFileWithError := 0; + memset(uint32(@wait), 0, sizeof(TIOTSyncWaitCtx)); + wait.Error := eUnknown; + wait.Process := proc.mgr.CurrentProcess; + + driver.storage.vfs.ReadFileAsync(fileHandle, position, buffer, length, + @wait.BytesDone, @iot_sync_complete, @wait); + if waitForIOTask(wait, IO_WAIT_TIMEOUT_TICKS, err) then + readFileWithError := wait.BytesDone; +end; + +function deleteFileWithTimeout(path : pchar; var err : TError; timeoutTicks : uint32) : boolean; +var + wait : TIOTSyncWaitCtx; + startT : uint32; +begin + deleteFileWithTimeout := false; + memset(uint32(@wait), 0, sizeof(TIOTSyncWaitCtx)); + wait.Error := eUnknown; + wait.Process := proc.mgr.CurrentProcess; + startT := currentTicks; + + driver.storage.vfs.DeleteFileAsync(path, @err, @iot_sync_complete, @wait); + + while puint32(@wait.Done)^ = 0 do begin + if (currentTicks - startT) >= timeoutTicks then begin + err := eIOTimeout; + if proc.mgr.CurrentProcess <> nil then + proc.mgr.CurrentProcess^.State := psReady; + exit; + end; + asm hlt end; + end; + + err := wait.Error; + deleteFileWithTimeout := wait.Error = eNone; +end; + procedure run(Params : PParamList; stdin_buf, stdout_buf, stderr_buf : POutBuf); var path : pchar; + sizeParam : pchar; + pathParam : pchar; fh : TFileHandle; fErr : TError; buf : puint8; verBuf : puint8; offset : uint32; chunkIdx : uint32; + chunkBytes : uint32; written : uint32; readBack : uint32; - totalChunks: uint32; + fileSize : uint32; ok : boolean; i : uint32; t0, t1 : uint32; elapsed : uint32; kbps : uint32; - tmp : pchar; tLast : uint32; { tick of last per-second report } bytesInSec : uint32; { bytes transferred since last report } + ioTicks : uint32; + prepTicks : uint32; + verifyTicks: uint32; + ioTicksInSec : uint32; + opStart : uint32; + opEnd : uint32; + parsedSize : uint32; + ioErr : TError; begin - { Determine target path } - path := getParam(0, Params); - if path = nil then - path := DEFAULT_PATH; + fileSize := DEFAULT_FILE_SIZE; + path := DEFAULT_PATH; + sizeParam := getParam(0, Params); + pathParam := getParam(1, Params); - totalChunks := FILE_SIZE div CHUNK_SIZE; + if sizeParam <> nil then begin + parsedSize := parseSizeBytes(sizeParam); + if parsedSize > 0 then begin + fileSize := parsedSize; + if pathParam <> nil then + path := pathParam; + end else begin + { Backward-compatible path-only form: IOTEST [path] } + path := sizeParam; + end; + end; - { Allocate two 4 KB heap buffers } + { Allocate two heap buffers } buf := puint8(kalloc(CHUNK_SIZE)); verBuf := puint8(kalloc(CHUNK_SIZE)); + if (buf = nil) or (verBuf = nil) then begin + io.stdio.bufWriteStrLn(stderr_buf, 'ERROR: Out of memory.'); + if buf <> nil then kfree(puint32(buf)); + if verBuf <> nil then kfree(puint32(verBuf)); + exit; + end; { ==================== WRITE PHASE ==================== } - io.stdio.bufWriteStr(stdout_buf, 'Writing 25 MB to '); + io.stdio.bufWriteStr(stdout_buf, 'Writing '); + writeSize(stdout_buf, fileSize); + io.stdio.bufWriteStr(stdout_buf, ' to '); io.stdio.bufWriteStrLn(stdout_buf, path); fh := OpenFile(path, omCreate, @fErr); if (fh = 0) or (fErr <> eNone) then begin - io.stdio.bufWriteStrLn(stderr_buf, 'ERROR: Could not create file.'); + io.stdio.bufWriteStr(stderr_buf, 'ERROR: Could not create file: '); + writeErrorName(stderr_buf, fErr); + io.stdio.bufWriteNewLine(stderr_buf); kfree(puint32(buf)); kfree(puint32(verBuf)); exit; @@ -103,49 +395,76 @@ begin t0 := arch.x86.bda.Counters.c32; tLast := t0; bytesInSec := 0; + ioTicks := 0; + prepTicks := 0; + ioTicksInSec := 0; offset := 0; ok := true; - if totalChunks > 0 then begin - for chunkIdx := 0 to totalChunks - 1 do begin - fillPattern(buf, chunkIdx); - written := WriteFile(fh, offset, buf, CHUNK_SIZE); - if written <> CHUNK_SIZE then begin + chunkIdx := 0; + while offset < fileSize do begin + chunkBytes := fileSize - offset; + if chunkBytes > CHUNK_SIZE then + chunkBytes := CHUNK_SIZE; + opStart := arch.x86.bda.Counters.c32; + fillPattern(buf, chunkIdx, chunkBytes); + opEnd := arch.x86.bda.Counters.c32; + prepTicks := prepTicks + (opEnd - opStart); + opStart := arch.x86.bda.Counters.c32; + written := writeFileWithError(fh, offset, buf, chunkBytes, ioErr); + opEnd := arch.x86.bda.Counters.c32; + ioTicks := ioTicks + (opEnd - opStart); + ioTicksInSec := ioTicksInSec + (opEnd - opStart); + if written <> chunkBytes then begin io.stdio.bufWriteStr(stderr_buf, 'ERROR: Short write at offset '); - io.stdio.bufWriteIntLn(stderr_buf, offset); + io.stdio.bufWriteInt(stderr_buf, offset); + io.stdio.bufWriteStr(stderr_buf, ' (got '); + io.stdio.bufWriteInt(stderr_buf, written); + io.stdio.bufWriteStr(stderr_buf, ', err '); + writeErrorName(stderr_buf, ioErr); + io.stdio.bufWriteStrLn(stderr_buf, ')'); ok := false; break; - end; - offset := offset + CHUNK_SIZE; - bytesInSec := bytesInSec + CHUNK_SIZE; - t1 := arch.x86.bda.Counters.c32; - if (t1 - tLast) >= TICK_HZ then begin - kbps := (bytesInSec div 1024) * TICK_HZ div (t1 - tLast); - io.stdio.bufWriteStr(stdout_buf, ' W '); - io.stdio.bufWriteInt(stdout_buf, offset div 1024); - io.stdio.bufWriteStr(stdout_buf, ' KB '); - writeThroughput(stdout_buf, kbps); - io.stdio.bufWriteNewLine(stdout_buf); - tLast := t1; - bytesInSec := 0; - end; end; + offset := offset + chunkBytes; + bytesInSec := bytesInSec + chunkBytes; + if ioTicksInSec >= TICK_HZ then begin + kbps := (bytesInSec div 1024) * TICK_HZ div ioTicksInSec; + io.stdio.bufWriteStr(stdout_buf, ' W '); + io.stdio.bufWriteInt(stdout_buf, offset div 1024); + io.stdio.bufWriteStr(stdout_buf, ' KB '); + writeThroughput(stdout_buf, kbps); + io.stdio.bufWriteNewLine(stdout_buf); + tLast := arch.x86.bda.Counters.c32; + bytesInSec := 0; + ioTicksInSec := 0; + end; + chunkIdx := chunkIdx + 1; end; - t1 := arch.x86.bda.Counters.c32; + opStart := arch.x86.bda.Counters.c32; io.stdio.bufWriteStrLn(stdout_buf, 'Closing write handle...'); CloseFile(fh); + opEnd := arch.x86.bda.Counters.c32; + ioTicks := ioTicks + (opEnd - opStart); + t1 := opEnd; io.stdio.bufWriteStrLn(stdout_buf, 'Write handle closed.'); if ok then begin - elapsed := t1 - t0; - io.stdio.bufWriteStr(stdout_buf, 'Write complete: '); + elapsed := ioTicks; + io.stdio.bufWriteStr(stdout_buf, 'Write I/O complete: '); if elapsed > 0 then begin - kbps := ((FILE_SIZE div 1024) * TICK_HZ) div elapsed; + kbps := ((fileSize div 1024) * TICK_HZ) div elapsed; writeThroughput(stdout_buf, kbps); io.stdio.bufWriteStr(stdout_buf, ' ('); io.stdio.bufWriteInt(stdout_buf, (elapsed * 1000) div TICK_HZ); io.stdio.bufWriteStrLn(stdout_buf, ' ms)'); end else io.stdio.bufWriteStrLn(stdout_buf, '< 1 tick'); + io.stdio.bufWriteStr(stdout_buf, 'Write buffer fill: '); + if prepTicks > 0 then begin + io.stdio.bufWriteInt(stdout_buf, (prepTicks * 1000) div TICK_HZ); + io.stdio.bufWriteStrLn(stdout_buf, ' ms'); + end else + io.stdio.bufWriteStrLn(stdout_buf, '< 1 tick'); end; if not ok then begin @@ -159,7 +478,9 @@ begin fh := OpenFile(path, omRead, @fErr); if (fh = 0) or (fErr <> eNone) then begin - io.stdio.bufWriteStrLn(stderr_buf, 'ERROR: Could not open file for reading.'); + io.stdio.bufWriteStr(stderr_buf, 'ERROR: Could not open file for reading: '); + writeErrorName(stderr_buf, fErr); + io.stdio.bufWriteNewLine(stderr_buf); kfree(puint32(buf)); kfree(puint32(verBuf)); exit; @@ -168,25 +489,39 @@ begin t0 := arch.x86.bda.Counters.c32; tLast := t0; bytesInSec := 0; + ioTicks := 0; + verifyTicks := 0; + ioTicksInSec := 0; offset := 0; ok := true; - if totalChunks > 0 then begin - for chunkIdx := 0 to totalChunks - 1 do begin - core.util.memset(uint32(verBuf), 0, CHUNK_SIZE); - readBack := ReadFile(fh, offset, verBuf, CHUNK_SIZE); - if readBack <> CHUNK_SIZE then begin + chunkIdx := 0; + while offset < fileSize do begin + chunkBytes := fileSize - offset; + if chunkBytes > CHUNK_SIZE then + chunkBytes := CHUNK_SIZE; + core.util.memset(uint32(verBuf), 0, chunkBytes); + opStart := arch.x86.bda.Counters.c32; + readBack := readFileWithError(fh, offset, verBuf, chunkBytes, ioErr); + opEnd := arch.x86.bda.Counters.c32; + ioTicks := ioTicks + (opEnd - opStart); + ioTicksInSec := ioTicksInSec + (opEnd - opStart); + if readBack <> chunkBytes then begin io.stdio.bufWriteStr(stderr_buf, 'ERROR: Short read at offset '); io.stdio.bufWriteInt(stderr_buf, offset); io.stdio.bufWriteStr(stderr_buf, ' (got '); io.stdio.bufWriteInt(stderr_buf, readBack); + io.stdio.bufWriteStr(stderr_buf, ', err '); + writeErrorName(stderr_buf, ioErr); io.stdio.bufWriteStrLn(stderr_buf, ')'); ok := false; break; - end; + end; - { Verify pattern } - fillPattern(buf, chunkIdx); - for i := 0 to CHUNK_SIZE - 1 do begin + { Verify pattern } + opStart := arch.x86.bda.Counters.c32; + fillPattern(buf, chunkIdx, chunkBytes); + if chunkBytes > 0 then begin + for i := 0 to chunkBytes - 1 do begin if verBuf[i] <> buf[i] then begin io.stdio.bufWriteStr(stderr_buf, 'ERROR: Mismatch at offset '); io.stdio.bufWriteInt(stderr_buf, offset + i); @@ -199,46 +534,67 @@ begin break; end; end; - if not ok then break; - - offset := offset + CHUNK_SIZE; - bytesInSec := bytesInSec + CHUNK_SIZE; - t1 := arch.x86.bda.Counters.c32; - if (t1 - tLast) >= TICK_HZ then begin - kbps := (bytesInSec div 1024) * TICK_HZ div (t1 - tLast); - io.stdio.bufWriteStr(stdout_buf, ' R '); - io.stdio.bufWriteInt(stdout_buf, offset div 1024); - io.stdio.bufWriteStr(stdout_buf, ' KB '); - writeThroughput(stdout_buf, kbps); - io.stdio.bufWriteNewLine(stdout_buf); - tLast := t1; - bytesInSec := 0; - end; end; + opEnd := arch.x86.bda.Counters.c32; + verifyTicks := verifyTicks + (opEnd - opStart); + if not ok then break; + + offset := offset + chunkBytes; + bytesInSec := bytesInSec + chunkBytes; + if ioTicksInSec >= TICK_HZ then begin + kbps := (bytesInSec div 1024) * TICK_HZ div ioTicksInSec; + io.stdio.bufWriteStr(stdout_buf, ' R '); + io.stdio.bufWriteInt(stdout_buf, offset div 1024); + io.stdio.bufWriteStr(stdout_buf, ' KB '); + writeThroughput(stdout_buf, kbps); + io.stdio.bufWriteNewLine(stdout_buf); + tLast := arch.x86.bda.Counters.c32; + bytesInSec := 0; + ioTicksInSec := 0; + end; + chunkIdx := chunkIdx + 1; end; - t1 := arch.x86.bda.Counters.c32; + opStart := arch.x86.bda.Counters.c32; io.stdio.bufWriteStrLn(stdout_buf, 'Closing read handle...'); CloseFile(fh); + opEnd := arch.x86.bda.Counters.c32; + ioTicks := ioTicks + (opEnd - opStart); + t1 := opEnd; io.stdio.bufWriteStrLn(stdout_buf, 'Read handle closed.'); if ok then begin - elapsed := t1 - t0; - io.stdio.bufWriteStr(stdout_buf, 'Read+verify complete: '); + elapsed := ioTicks; + io.stdio.bufWriteStr(stdout_buf, 'Read I/O complete: '); if elapsed > 0 then begin - kbps := ((FILE_SIZE div 1024) * TICK_HZ) div elapsed; + kbps := ((fileSize div 1024) * TICK_HZ) div elapsed; writeThroughput(stdout_buf, kbps); io.stdio.bufWriteStr(stdout_buf, ' ('); io.stdio.bufWriteInt(stdout_buf, (elapsed * 1000) div TICK_HZ); io.stdio.bufWriteStrLn(stdout_buf, ' ms)'); end else io.stdio.bufWriteStrLn(stdout_buf, '< 1 tick'); - io.stdio.bufWriteStrLn(stdout_buf, 'PASS: All 10 MB verified OK.'); + io.stdio.bufWriteStr(stdout_buf, 'Verification pass: '); + if verifyTicks > 0 then begin + io.stdio.bufWriteInt(stdout_buf, (verifyTicks * 1000) div TICK_HZ); + io.stdio.bufWriteStrLn(stdout_buf, ' ms'); + end else + io.stdio.bufWriteStrLn(stdout_buf, '< 1 tick'); + io.stdio.bufWriteStr(stdout_buf, 'PASS: All '); + writeSize(stdout_buf, fileSize); + io.stdio.bufWriteStrLn(stdout_buf, ' verified OK.'); end else begin io.stdio.bufWriteStrLn(stderr_buf, 'FAIL: Verification failed.'); end; { ==================== CLEANUP ==================== } - DeleteFile(path, @fErr); + io.stdio.bufWriteStrLn(stdout_buf, 'Deleting test file...'); + if deleteFileWithTimeout(path, fErr, DELETE_TIMEOUT_TICKS) then + io.stdio.bufWriteStrLn(stdout_buf, 'Test file deleted.') + else begin + io.stdio.bufWriteStr(stdout_buf, 'Cleanup skipped: '); + writeErrorName(stdout_buf, fErr); + io.stdio.bufWriteNewLine(stdout_buf); + end; kfree(puint32(buf)); kfree(puint32(verBuf)); @@ -247,7 +603,7 @@ end; procedure init(); begin debug.tracer.push_trace('iotest.init'); - io.stdio.registerCommand('IOTEST', @run, 'Write and read-verify a 10 MB file.'); + io.stdio.registerCommand('IOTEST', @run, 'Write and read-verify a file. Usage: IOTEST [size] [path]'); end; end. diff --git a/src/driver/storage/driver.storage.mgr.pas b/src/driver/storage/driver.storage.mgr.pas index 7d141a85..22bf178e 100644 --- a/src/driver/storage/driver.storage.mgr.pas +++ b/src/driver/storage/driver.storage.mgr.pas @@ -336,18 +336,20 @@ end; procedure write_mbr(device : PStorage_Device; mbr : PMaster_Boot_Record); var + oldCache : pointer; cache : puint32; buf : puint32; begin push_trace('driver.storage.mgr.write_mbr'); if (device = nil) or (mbr = nil) or (not device^.writable) then exit; { Update cache } - if device^.cachedMBR <> nil then - kfree(device^.cachedMBR); + oldCache := device^.cachedMBR; cache := puint32(kalloc(sizeof(TMaster_Boot_Record))); if cache <> nil then memcpy(uint32(mbr), uint32(cache), sizeof(TMaster_Boot_Record)); device^.cachedMBR := pointer(cache); + if oldCache <> nil then + kfree(oldCache); buf := alloc_mbr_io_buffer(device, mbr); if buf = nil then exit; @@ -364,6 +366,7 @@ end; procedure write_mbr_async(device : PStorage_Device; mbr : PMaster_Boot_Record; callback : TIOCallback; callbackData : pointer); var + oldCache : pointer; cache : puint32; buf : puint32; ctx : PMBRWriteAsyncData; @@ -379,12 +382,13 @@ begin exit; end; { Update cache } - if device^.cachedMBR <> nil then - kfree(device^.cachedMBR); + oldCache := device^.cachedMBR; cache := puint32(kalloc(sizeof(TMaster_Boot_Record))); if cache <> nil then memcpy(uint32(mbr), uint32(cache), sizeof(TMaster_Boot_Record)); device^.cachedMBR := pointer(cache); + if oldCache <> nil then + kfree(oldCache); buf := alloc_mbr_io_buffer(device, mbr); if buf = nil then begin diff --git a/src/driver/storage/driver.storage.vfs.pas b/src/driver/storage/driver.storage.vfs.pas index 2222a753..22c505b4 100644 --- a/src/driver/storage/driver.storage.vfs.pas +++ b/src/driver/storage/driver.storage.vfs.pas @@ -134,7 +134,7 @@ procedure UnwatchDirectory(WatchID : uint32); { Volume invalidation — must be called before freeing a volume. Closes all open FDs referencing the volume across all processes, evicts directory cache entries, and removes VFS mount points. } -procedure InvalidateVolume(vol : PStorage_Volume); +function InvalidateVolume(vol : PStorage_Volume) : boolean; { VFS Functions } function newVirtualDirectory(Path : pchar) : TError; @@ -149,6 +149,7 @@ implementation uses driver.storage.fs.mgr, + driver.storage.fs.fat32, proc.mgr, proc.types, io.stdio, @@ -508,7 +509,36 @@ end; function GetObjectFromPath(path : pchar) : PVFSObject; forward; function GetObjectFromPathEx(path : pchar; var volRelPath : pchar) : PVFSObject; forward; -procedure InvalidateVolume(vol : PStorage_Volume); +function volumeHasAsyncHandles(vol : PStorage_Volume) : boolean; +var + pCount : uint32; + pi : uint32; + fi : uint32; + pCtx : proc.types.PProcessContext; + tbl : PFDTable; +begin + volumeHasAsyncHandles := false; + if vol = nil then exit; + + pCount := proc.mgr.processCount(); + if pCount = 0 then exit; + + for pi := 0 to pCount - 1 do begin + pCtx := proc.mgr.getProcessByIndex(pi); + if pCtx = nil then continue; + tbl := PFDTable(pCtx^.FDTable); + if tbl = nil then continue; + for fi := 0 to MAX_FDS - 1 do begin + if tbl^.Entries[fi].InUse and (tbl^.Entries[fi].Volume = vol) + and (tbl^.Entries[fi].AsyncRefs > 0) then begin + volumeHasAsyncHandles := true; + exit; + end; + end; + end; +end; + +function InvalidateVolume(vol : PStorage_Volume) : boolean; var pCount : uint32; pi : uint32; @@ -522,7 +552,12 @@ var child : PVFSObject; begin debug.tracer.push_trace('driver.storage.vfs.InvalidateVolume.enter'); + InvalidateVolume := false; if vol = nil then exit; + if volumeHasAsyncHandles(vol) then begin + debug.tracer.push_trace('driver.storage.vfs.InvalidateVolume.busy'); + exit; + end; { 1. Evict all directory cache entries for this volume } DirCache_Invalidate(vol, nil); @@ -575,6 +610,10 @@ begin kfree(void(bootObj)); end; + if (vol^.filesystem <> nil) and (stringCompare(vol^.filesystem^.sName, 'FAT32') = 0) then + FAT32ReleaseVolumeState(vol); + + InvalidateVolume := true; debug.tracer.push_trace('driver.storage.vfs.InvalidateVolume.exit'); end; @@ -1313,6 +1352,17 @@ begin { Cache per-file metadata and populate DataSize via FS open hook } if (vol^.filesystem <> nil) and (vol^.filesystem^.openFileCallback <> nil) then begin fd^.FSPrivate := vol^.filesystem^.openFileCallback(vol, dir, fname, fd^.DataSize); + if fd^.FSPrivate = nil then begin + if Error <> nil then begin + if (OpenMode = omCreate) or (OpenMode = omWrite) then + Error^ := eInvalidFileName + else + Error^ := eFileDoesNotExist; + end; + fd_close(tbl, slot); + debug.tracer.push_trace('driver.storage.vfs.OpenFile.exit'); + exit; + end; if vol^.filesystem^.closeFileCallback <> nil then fd^.FSCloseHook := pointer(vol^.filesystem^.closeFileCallback); if Error <> nil then Error^ := eNone; @@ -1599,6 +1649,19 @@ begin if (vol^.filesystem <> nil) and (vol^.filesystem^.openFileCallback <> nil) then begin fd^.FSPrivate := vol^.filesystem^.openFileCallback(vol, dir, fname, fd^.DataSize); + if fd^.FSPrivate = nil then begin + if Error <> nil then begin + if (OpenMode = omCreate) or (OpenMode = omWrite) then + Error^ := eInvalidFileName + else + Error^ := eFileDoesNotExist; + end; + fd_close(tbl, slot); + OutHandle := 0; + if Callback <> nil then Callback(Error^, CallbackData); + debug.tracer.push_trace('driver.storage.vfs.OpenFileAsync.exit'); + exit; + end; if vol^.filesystem^.closeFileCallback <> nil then fd^.FSCloseHook := pointer(vol^.filesystem^.closeFileCallback); if Error <> nil then Error^ := eNone; diff --git a/src/driver/storage/fs/driver.storage.fs.fat32.old.pas b/src/driver/storage/fs/driver.storage.fs.fat32.old.pas new file mode 100644 index 00000000..4698394f --- /dev/null +++ b/src/driver/storage/fs/driver.storage.fs.fat32.old.pas @@ -0,0 +1,4650 @@ +// Copyright 2021 Kieron Morris +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ + Driver->storage->driver.storage.fs.fat32 - driver.storage.fs.fat32 filesystem driver + + @author(Aaron Hance ) +} + +unit driver.storage.fs.fat32.old; + +interface + +uses + boot.mgr, + driver.storage.fs.mgr, + core.ds.lists, + memory.heap, + proc.mgr, + proc.types, + driver.timer.rtc, + io.stdio, + driver.storage.mgr, + driver.storage.types, + core.strings, + io.syslog, + debug.tracer, + core.util, arch.x86.util, + driver.storage.vol.mgr; + +procedure FAT32LegacyReleaseVolumeCtx(volume : PStorage_Volume); + +type + + TBootRecord = bitpacked record + jmp2boot : ubit24; + OEMName : array[0..7] of char; + sectorSize : uint16; + spc : uint8; + rsvSectors : uint16; + numFats : uint8; + numDirEnt : uint16; + numSectors : uint16; + mediaDescp : uint8; + sectorsPerFat : uint16; + sectorsPerTrack : uint16; + heads : uint16; + hiddenSectors : uint32; + manySectors : uint32; + FATSize : uint32; + flags : uint16; + FATVersion : uint16; + rootCluster : uint32; + FSInfoCluster : uint16; + backupCluster : uint16; + reserved0 : array[0..11] of uint8; + driveNumber : uint8; + reserved1 : uint8; + bsignature : uint8;// = $28; + volumeID : uint32; + volumeLabel : array[0..10] of uint8; + identString : array[0..7] of char;// = 'FAT32 '; + end; + PBootRecord = ^TBootRecord; + + TFATExtArray = array[0..2] of char; + + TDirectory = packed record + fileName : array[0..7] of char; + fileExtension : TFATExtArray; + attributes : uint8; + reserved0 : uint8; + timeFine : uint8; + time : uint16; + date : uint16; + accessTime : uint16; + clusterHigh : uint16; + modifiedTime : uint16; + modifiedDate : uint16; + clusterLow : uint16; + byteSize : uint32; + end; + PDirectory = ^TDirectory; + + TFilesystemInfo = record + leadSignature : uint32; + reserved0 : array[0..479] of uint8; + structSignature : uint32; + freeSectors : uint32; + nextFreeSector : uint32; + reserved1 : array[0..11] of uint8; + trailSignature : uint32; + end; + + TFatVolumeInfo = record + sectorsPerCluster : uint8; // must be power of 2 and mult by sectorsize to max 32k + end; + PFatVolumeInfo = ^TFatVolumeInfo; + + TClusterExtent = record + StartCluster : uint32; + ClusterCount : uint32; + end; + PClusterExtent = ^TClusterExtent; + +const + { FAT sector cache — 2-way set-associative, 256 sets, LRU replacement. + This keeps small-cluster sequential files from churning the FAT cache + once they grow beyond a few megabytes. } + FAT_CACHE_SETS = 256; + FAT_CACHE_WAYS = 2; + FAT_CACHE_LINES = 512; { SETS * WAYS } + FAT_TRANSFER_POOL_CAPACITY = 64; + FAT_WORK_QUEUE_CAPACITY = FAT_TRANSFER_POOL_CAPACITY; + FAT_WRITE_PREALLOC_BYTES = 8 * 1024 * 1024; + FAT_WRITE_PREALLOC_MAX_CLUSTERS = 2048; + FAT_WRITE_PREALLOC_MIN_CLUSTERS = 64; + +type + TFATCacheLine = record + SectorIdx : uint32; { which FAT sector is stored here } + Dirty : boolean; { true if written but not flushed to disk } + Valid : boolean; { true if line contains valid data } + LRU : uint8; { 0 = MRU, 1 = LRU, 2 = LRU (for 3-way), 3 = LRU (for 4-way) } + end; + + PFATCache = ^TFATCache; + TFATCache = record + Lines : array[0..FAT_CACHE_LINES - 1] of TFATCacheLine; + Data : puint32; { kalloc'd FAT_CACHE_LINES * 512 bytes of FAT sector data } + EvictBuf : puint32; { kalloc'd 512-byte scratch for eviction writes } + Busy : boolean; { spinlock: serializes miss-path eviction+load } + FatStart : uint32; { LBA of first FAT sector on disk } + Device : PStorage_Device; { device pointer for I/O } + BootRecord : PBootRecord; { cached boot record (read once) } + end; + + TFATAsyncMode = (famRead, famWrite); + + TFATTransferState = ( + ftsIdle, + ftsPrepare, + ftsIssueIO, + ftsAwaitIO, + ftsAdvance, + ftsComplete, + ftsError + ); + + { Per-file cached metadata created by fat32OpenFile and stored in the + file descriptor's FSPrivate pointer. Avoids re-reading boot record, + directory listing, and searching for the file on every read/write. } + PFATOpenFile = ^TFATOpenFile; + PFATVolumeCtx = ^TFATVolumeCtx; + PFATFileIOAsyncCtx = ^TFATFileIOAsyncCtx; + + TFATOpenFile = record + Volume : PStorage_Volume; + BootRecord : PBootRecord; { borrows from TFATCache — do NOT kfree } + DataStart : uint32; { fatDataStartLBA result } + FirstCluster : uint32; { file's start cluster (0 if new/empty) } + ByteSize : uint32; { current file size in bytes } + AllocClusters: uint32; { current reserved cluster count for appends } + Extents : PLinkedListBase;{ cached cluster extents for fast sequential I/O } + DirCluster : uint32; { parent directory's start cluster } + CleanName : byteArray8; { cleaned 8.3 filename } + ExtPart : pchar; { heap-allocated extension string — owned } + ScratchSector: puint32; { per-open-file sector scratch buffer } + ScratchSize : uint32; { size of ScratchSector in bytes } + Exists : boolean; { true if file existed at open time } + FatDirty : boolean; { true if FAT cache must be flushed on close } + MetaDirty : boolean; { true if dir entry size/cluster needs sync on close } + end; + + TFATVolumeCtx = record + Cache : TFATCache; + TransferPoolBuf : pointer; + TransferFreeList : pointer; + Worker : proc.types.PProcessContext; + WorkHead : uint32; + WorkTail : uint32; + WorkCount : uint32; + WorkQueue : array[0..FAT_WORK_QUEUE_CAPACITY - 1] of PFATFileIOAsyncCtx; + end; + + TFATFileIOAsyncCtx = record + Next : PFATFileIOAsyncCtx; + VolumeCtx : PFATVolumeCtx; + State : TFATTransferState; + Mode : TFATAsyncMode; + Volume : PStorage_Volume; + OpenFile : PFATOpenFile; + Buffer : puint32; + Offset : uint32; + ByteCount : uint32; + BytesDone : uint32; + BytesOut : puint32; + Callback : TIOCallback; + CallbackData : pointer; + Cluster : uint32; + OrigByteSize : uint32; + DataStart : uint32; + BootRecord : PBootRecord; + ChunkBytes : uint32; + LastError : TError; + CursorValid : boolean; + CursorIndex : uint32; + CursorCluster: uint32; + ExtentValid : boolean; + ExtentIdx : uint32; + ExtentStartIdx : uint32; + end; + + { Async format state machine steps } + TFmtStep = ( + fmtBootSector, + fmtZeroFAT, + fmtFATEntries, + fmtRootDir, + fmtSystemDir, + fmtDone + ); + + PFmtContext = ^TFmtContext; + TFmtContext = record + Step : TFmtStep; + Volume : PStorage_Volume; + Disk : PStorage_Device; + SectorStart : uint32; + FATStart : uint32; + DataStart : uint32; + FATSize : uint32; + BatchPos : uint32; + BatchSize : uint32; + RootCluster : uint32; + SPC : uint32; + Buffer : puint32; + ZeroBuffer : puint32; + Callback : TIOCallback; + CallbackData : pointer; + end; + +var + filesystem : TFilesystem; + +procedure init; +procedure create_volume(volume : PStorage_Volume; sectors : uint32; start : uint32; config : puint32); +procedure create_volume_async(volume : PStorage_Volume; sectors : uint32; start : uint32; config : puint32; callback : TIOCallback; callbackData : pointer); +procedure detect_volumes(disk : PStorage_Device); +function identify_volume(volume : PStorage_Volume) : boolean; +function readDirectoryGen(volume : PStorage_volume; directory : pchar; status : puint32) : PLinkedListBase; +procedure writeDirectoryGen(volume : PStorage_volume; directory : pchar; dirName : pchar; attributes : uint32; statusOut : puint32); +procedure deleteFile(volume : PStorage_Volume; filePath : pchar; statusOut : puint32); +procedure deleteDir(volume : PStorage_Volume; path : pchar; statusOut : puint32); +procedure renameFile(volume : PStorage_Volume; filePath : pchar; newName : pchar; statusOut : puint32); +//function writeDirectory(volume : PStorage_volume; directory : pchar; attributes : uint32) : uint8; // need to handle parent table cluster overflow, need to take attributes +//function readDirectory(volume : PStorage_volume; directory : pchar; listPtr : PLinkedListBase) : uint8; //returns: 0 = success, 1 = dir not exsist, 2 = not directory, 3 = error + + +implementation + +{ ---- Module-level I/O caches ---- } +var + + { Sequential I/O chain-position cache: stores the last visited + (cluster-index, cluster-number) pair so the next call can resume + instead of walking the entire chain from cluster 0. } + sioc_vol : PStorage_Volume = nil; + sioc_fileClust : uint32 = 0; + sioc_chainIdx : uint32 = 0; + sioc_chainClust : uint32 = 0; + sioc_valid : boolean = false; + + { Free-cluster allocation hint: next findFreeClusters scan starts here + instead of cluster 2, making sequential allocations O(1). } + sioc_allocHint : uint32 = 2; + sioc_allocVol : PStorage_Volume = nil; + +{ Validate a FAT32 8.3 filename. + fullName should be in "NAME.EXT" form. + Returns true if valid: name part 1-8 chars, extension 0-3 chars, + no illegal characters. } +function isValidFAT32Name(fullName : pchar) : boolean; +var + i, fnLen, dotPos, nameLen, extLen : uint32; + c : char; +begin + isValidFAT32Name := false; + if fullName = nil then exit; + fnLen := stringSize(fullName); + if fnLen = 0 then exit; + + { Find last dot position } + dotPos := fnLen; + for i := 0 to fnLen - 1 do begin + if fullName[i] = '.' then + dotPos := i; + end; + + nameLen := dotPos; + if dotPos < fnLen then + extLen := fnLen - dotPos - 1 + else + extLen := 0; + + { Name part must be 1-8 characters } + if (nameLen < 1) or (nameLen > 8) then exit; + { Extension part must be 0-3 characters } + if extLen > 3 then exit; + + { Check all characters for validity } + for i := 0 to fnLen - 1 do begin + c := fullName[i]; + if i = dotPos then continue; { skip the dot separator } + { Reject control characters (< 0x20) except we don't need to check 0 since stringSize stops there } + if uint8(c) < $20 then exit; + { Reject illegal FAT32 characters: " * / : < > ? \ | } + case c of + '"', '*', '/', ':', '<', '>', '?', '\', '|': exit; + end; + end; + + isValidFAT32Name := true; +end; + +function load(ptr : void) : boolean; +begin + io.syslog.logln('FAT32', 'LOADED.') +end; + +function matchExtension(dirExt : TFATExtArray; ext : pchar) : boolean; +var + j : uint32; + c : char; + extDone : boolean; +begin + matchExtension:= true; + extDone := false; + for j:=0 to 2 do begin + if (not extDone) and (ext <> nil) and (ext[j] <> char(0)) then begin + c:= ext[j]; + { Uppercase a-z to A-Z for case-insensitive FAT32 matching } + if (c >= 'a') and (c <= 'z') then + c := char(uint8(c) - 32); + end else begin + c:= ' '; + extDone := true; + end; + if dirExt[j] <> c then begin + matchExtension:= false; + break; + end; + end; +end; + +function cleanString(str : pchar; status : puint32) : byteArray8; +var + i : uint32; + ii: uint32; +begin + push_trace('cleanstring()'); + if str = nil then begin + for i:=0 to 7 do cleanString[i]:= ' '; + exit; + end; + for i:=0 to 7 do begin + if str[i] = char(0) then begin + for ii:=i to 7 do begin + cleanString[ii]:= ' '; + end; + break; + end else begin + { Uppercase a-z to A-Z for FAT32 } + if (str[i] >= 'a') and (str[i] <= 'z') then + cleanString[i]:= char(uint8(str[i]) - 32) + else + cleanString[i]:= str[i]; + end; + end; + +end; + +{ Split a full filename like 'README.TXT' into name and extension parts. + Caller must free nameOut and extOut. } +procedure splitFileNameParts(fullName : pchar; var nameOut : pchar; var extOut : pchar); +var + fnLen, dotPos, i : uint32; +begin + push_trace('driver.storage.fs.fat32.splitFileNameParts'); + nameOut := nil; + extOut := nil; + if fullName = nil then begin + nameOut := stringNew(0); + extOut := stringNew(0); + exit; + end; + fnLen := stringSize(fullName); + dotPos := fnLen; + for i := 0 to fnLen - 1 do begin + if fullName[i] = '.' then + dotPos := i; + end; + nameOut := stringTrim(fullName, dotPos); + if dotPos < fnLen then + extOut := stringCopy(pchar(@fullName[dotPos + 1])) + else + extOut := stringNew(0); +end; + +function cleanStringCha(str : pchar) : pchar; +var + i : uint32; + ii: uint32; +begin + cleanStringCha:= pchar(kalloc(10)); + memset(uint32(cleanstringcha), 0, 10); + push_trace('cleanstringcha'); + if str = nil then begin + for i:=0 to 7 do cleanStringCha[i]:= ' '; + exit; + end; + for i:=0 to 7 do begin + if str[i] = char(0) then begin + for ii:=i to 7 do begin + cleanStringCha[ii]:= ' '; + end; + break; + end else begin + push_trace('cleanstringcha1.2'); + cleanstringcha[i]:= str[i]; + + end; + end; + +end; + +function readBootRecord(volume : PStorage_volume) : PBootRecord; +var + buffer : puint32; + bufSize : uint32; +begin + bufSize := volume^.device^.sectorSize; + if bufSize < 512 then bufSize := 512; + buffer:= puint32(kalloc(bufSize)); + memset(uint32(buffer), 0, bufSize); + driver.storage.mgr.storage_read(volume^.device, volume^.sectorStart, 1, buffer); + readBootRecord:= PBootRecord(buffer); +end; + +function bootSectorHasSignature(buffer : puint32) : boolean; +begin + bootSectorHasSignature := (puint8(uint32(buffer) + 510)^ = $55) and + (puint8(uint32(buffer) + 511)^ = $AA); +end; + +{ ---- FAT sector cache ---- } + +{ Get or lazily create the per-volume FAT32 context. + Stores the context pointer in volume^.fsPrivate. } +function fatVolumeCtxGet(volume : PStorage_Volume) : PFATVolumeCtx; +var + ctx : PFATVolumeCtx; + br : PBootRecord; + i : uint32; + base : uint32; + tx : PFATFileIOAsyncCtx; +begin + fatVolumeCtxGet := nil; + if volume = nil then exit; + + if volume^.fsPrivate <> nil then begin + fatVolumeCtxGet := PFATVolumeCtx(volume^.fsPrivate); + exit; + end; + + ctx := PFATVolumeCtx(kalloc(sizeof(TFATVolumeCtx))); + if ctx = nil then exit; + memset(uint32(ctx), 0, sizeof(TFATVolumeCtx)); + + ctx^.Cache.Data := puint32(kalloc(FAT_CACHE_LINES * 512)); + if ctx^.Cache.Data = nil then begin + kfree(puint32(ctx)); + exit; + end; + memset(uint32(ctx^.Cache.Data), 0, FAT_CACHE_LINES * 512); + + ctx^.Cache.EvictBuf := puint32(kalloc(512)); + if ctx^.Cache.EvictBuf = nil then begin + kfree(ctx^.Cache.Data); + kfree(puint32(ctx)); + exit; + end; + memset(uint32(ctx^.Cache.EvictBuf), 0, 512); + + ctx^.Cache.Busy := false; + for i := 0 to FAT_CACHE_LINES - 1 do begin + ctx^.Cache.Lines[i].Valid := false; + ctx^.Cache.Lines[i].Dirty := false; + ctx^.Cache.Lines[i].LRU := uint8(i mod FAT_CACHE_WAYS); + end; + + br := readBootRecord(volume); + ctx^.Cache.BootRecord := br; + ctx^.Cache.FatStart := volume^.sectorStart + br^.rsvSectors; + ctx^.Cache.Device := volume^.device; + + ctx^.TransferPoolBuf := kalloc(FAT_TRANSFER_POOL_CAPACITY * sizeof(TFATFileIOAsyncCtx)); + if ctx^.TransferPoolBuf <> nil then begin + base := uint32(ctx^.TransferPoolBuf); + if FAT_TRANSFER_POOL_CAPACITY > 0 then + for i := 0 to FAT_TRANSFER_POOL_CAPACITY - 1 do begin + tx := PFATFileIOAsyncCtx(base + (i * sizeof(TFATFileIOAsyncCtx))); + tx^.Next := PFATFileIOAsyncCtx(ctx^.TransferFreeList); + tx^.VolumeCtx := ctx; + tx^.State := ftsIdle; + ctx^.TransferFreeList := pointer(tx); + end; + end; + + volume^.fsPrivate := pointer(ctx); + fatVolumeCtxGet := ctx; +end; + +procedure fatCacheFlush(cache : PFATCache; volume : PStorage_Volume); forward; + +procedure FAT32LegacyReleaseVolumeCtx(volume : PStorage_Volume); +var + ctx : PFATVolumeCtx; +begin + if volume = nil then exit; + ctx := PFATVolumeCtx(volume^.fsPrivate); + if ctx = nil then exit; + + volume^.fsPrivate := nil; + fatCacheFlush(@ctx^.Cache, volume); + + if ctx^.Worker <> nil then begin + proc.mgr.terminate(ctx^.Worker^.ProcessID); + exit; + end; + + if ctx^.Cache.BootRecord <> nil then + kfree(puint32(ctx^.Cache.BootRecord)); + if ctx^.Cache.Data <> nil then + kfree(ctx^.Cache.Data); + if ctx^.Cache.EvictBuf <> nil then + kfree(ctx^.Cache.EvictBuf); + if ctx^.TransferPoolBuf <> nil then + kfree(void(ctx^.TransferPoolBuf)); + kfree(puint32(ctx)); +end; + +{ Get or lazily create the per-volume FAT cache. } +function fatCacheGet(volume : PStorage_Volume) : PFATCache; +var + ctx : PFATVolumeCtx; +begin + ctx := fatVolumeCtxGet(volume); + if ctx = nil then + fatCacheGet := nil + else + fatCacheGet := @ctx^.Cache; +end; + +function fatTransferAlloc(volume : PStorage_Volume) : PFATFileIOAsyncCtx; +var + ctx : PFATVolumeCtx; + transfer : PFATFileIOAsyncCtx; +begin + fatTransferAlloc := nil; + ctx := fatVolumeCtxGet(volume); + if ctx = nil then exit; + + asm pushf; cli end; + transfer := PFATFileIOAsyncCtx(ctx^.TransferFreeList); + if transfer <> nil then + ctx^.TransferFreeList := pointer(transfer^.Next); + asm popf end; + + if transfer = nil then exit; + + memset(uint32(transfer), 0, sizeof(TFATFileIOAsyncCtx)); + transfer^.VolumeCtx := ctx; + transfer^.State := ftsIdle; + fatTransferAlloc := transfer; +end; + +procedure fatTransferFree(ctx : PFATFileIOAsyncCtx); +var + volCtx : PFATVolumeCtx; +begin + if ctx = nil then exit; + volCtx := ctx^.VolumeCtx; + if volCtx = nil then exit; + + asm pushf; cli end; + ctx^.Next := PFATFileIOAsyncCtx(volCtx^.TransferFreeList); + volCtx^.TransferFreeList := pointer(ctx); + asm popf end; +end; + +function fatWorkerQueuePush(volCtx : PFATVolumeCtx; transfer : PFATFileIOAsyncCtx) : boolean; +begin + fatWorkerQueuePush := false; + if (volCtx = nil) or (transfer = nil) then exit; + + asm pushf; cli end; + if volCtx^.WorkCount < FAT_WORK_QUEUE_CAPACITY then begin + volCtx^.WorkQueue[volCtx^.WorkTail] := transfer; + volCtx^.WorkTail := (volCtx^.WorkTail + 1) mod FAT_WORK_QUEUE_CAPACITY; + volCtx^.WorkCount := volCtx^.WorkCount + 1; + fatWorkerQueuePush := true; + end; + asm popf end; +end; + +function fatWorkerQueuePop(volCtx : PFATVolumeCtx; var transfer : PFATFileIOAsyncCtx) : boolean; +begin + fatWorkerQueuePop := false; + transfer := nil; + if volCtx = nil then exit; + + asm pushf; cli end; + if volCtx^.WorkCount > 0 then begin + transfer := volCtx^.WorkQueue[volCtx^.WorkHead]; + volCtx^.WorkQueue[volCtx^.WorkHead] := nil; + volCtx^.WorkHead := (volCtx^.WorkHead + 1) mod FAT_WORK_QUEUE_CAPACITY; + volCtx^.WorkCount := volCtx^.WorkCount - 1; + fatWorkerQueuePop := true; + end; + asm popf end; +end; + +procedure fatWorkerWake(volCtx : PFATVolumeCtx); +begin + if (volCtx = nil) or (volCtx^.Worker = nil) then exit; + + asm pushf; cli end; + volCtx^.Worker^.PendingMsg := smCustom; + volCtx^.Worker^.MsgData := nil; + if volCtx^.Worker^.State = psAwaiting then + volCtx^.Worker^.State := psReady; + asm popf end; +end; + +procedure fat32_fileio_async_run_next(ctx : PFATFileIOAsyncCtx); forward; + +procedure fat32_transfer_worker(pctx : proc.types.PProcessContext); +var + volCtx : PFATVolumeCtx; + transfer : PFATFileIOAsyncCtx; +begin + volCtx := PFATVolumeCtx(pctx^.Local); + if volCtx = nil then begin + proc.mgr.proc_exit(1); + exit; + end; + + while true do begin + if pctx^.PendingMsg = smTerminate then begin + pctx^.PendingMsg := smNone; + proc.mgr.proc_exit(0); + exit; + end; + + if fatWorkerQueuePop(volCtx, transfer) then begin + fat32_fileio_async_run_next(transfer); + continue; + end; + + asm pushf; cli end; + if volCtx^.WorkCount = 0 then begin + pctx^.PendingMsg := smNone; + pctx^.State := psAwaiting; + end; + asm popf end; + + if pctx^.State = psAwaiting then + proc.mgr.proc_await; + end; +end; + +procedure fatWorkerEnsure(volume : PStorage_Volume); +var + volCtx : PFATVolumeCtx; +begin + volCtx := fatVolumeCtxGet(volume); + if volCtx = nil then exit; + if volCtx^.Worker <> nil then exit; + volCtx^.Worker := proc.mgr.create('fat32.xfer', @fat32_transfer_worker, void(volCtx), 1); +end; + +function fatScheduleTransfer(ctx : PFATFileIOAsyncCtx) : boolean; +var + volCtx : PFATVolumeCtx; +begin + fatScheduleTransfer := false; + if ctx = nil then exit; + + volCtx := ctx^.VolumeCtx; + if volCtx = nil then exit; + if volCtx^.Worker = nil then exit; + + if fatWorkerQueuePush(volCtx, ctx) then begin + fatWorkerWake(volCtx); + fatScheduleTransfer := true; + end; +end; + +{ Flush all dirty cache lines to disk. + CLI/STI protects cache metadata; disk I/O runs with interrupts enabled. } +procedure fatCacheFlush(cache : PFATCache; volume : PStorage_Volume); +var + i : uint32; + lba : uint32; + needFlush : boolean; +begin + { Acquire Busy lock — protects EvictBuf and prevents concurrent evictions } + while true do begin + asm pushf; cli end; + if not cache^.Busy then begin + cache^.Busy := true; + asm popf end; + break; + end; + asm popf end; + asm hlt end; + end; + + for i := 0 to FAT_CACHE_LINES - 1 do begin + asm pushf; cli end; + needFlush := cache^.Lines[i].Valid and cache^.Lines[i].Dirty; + if needFlush then begin + lba := cache^.FatStart + cache^.Lines[i].SectorIdx; + cache^.Lines[i].Dirty := false; + core.util.memcpy(uint32(cache^.Data) + (i * 512), + uint32(cache^.EvictBuf), 512); + end; + asm popf end; + + if needFlush then + driver.storage.mgr.storage_write( + cache^.Device, lba, 1, cache^.EvictBuf); + end; + + { Release lock } + asm pushf; cli end; + cache^.Busy := false; + asm popf end; +end; + +{ Invalidate all cache lines (e.g. on unmount). } +procedure fatCacheInvalidate(cache : PFATCache); +var + i : uint32; +begin + for i := 0 to FAT_CACHE_LINES - 1 do begin + cache^.Lines[i].Valid := false; + cache^.Lines[i].Dirty := false; + cache^.Lines[i].LRU := uint8(i mod FAT_CACHE_WAYS); + end; +end; + +{ Return the per-volume cached boot record without any allocation. + The FAT cache lazily reads and keeps a single copy; callers MUST NOT kfree it. } +function getCachedBootRecord(volume : PStorage_Volume) : PBootRecord; +begin + getCachedBootRecord := fatCacheGet(volume)^.BootRecord; +end; + +{ Read a single FAT entry using the sector cache. + 128 FAT entries per 512-byte sector. 2-way set-associative: set = fatSector mod FAT_CACHE_SETS. + CLI/STI protects cache metadata; disk I/O runs with interrupts enabled. } +function readFat(volume : PStorage_volume; cluster : uint32; bootRecord : PBootRecord) : uint32; +var + cache : PFATCache; + fatSecIdx : uint32; { which FAT sector this cluster lives in } + setIdx : uint32; { cache set index } + baseSlot : uint32; { first line index of this set } + entryOff : uint32; { offset within that sector (0..127) } + dataPtr : puint32; { pointer into cache data for the hit/victim slot } + lba : uint32; + doEvict : boolean; + evictLBA : uint32; + w : uint32; { way iterator } + victim : uint32; { chosen victim slot index } +begin + cache := fatCacheGet(volume); + fatSecIdx := cluster div 128; + setIdx := fatSecIdx mod FAT_CACHE_SETS; + baseSlot := setIdx * FAT_CACHE_WAYS; + entryOff := cluster mod 128; + + { Fast path: cache hit under CLI — check both ways } + asm pushf; cli end; + for w := 0 to FAT_CACHE_WAYS - 1 do begin + if cache^.Lines[baseSlot + w].Valid and (cache^.Lines[baseSlot + w].SectorIdx = fatSecIdx) then begin + dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); + readFat := dataPtr[entryOff] and $0FFFFFFF; + { Update LRU: this way is MRU } + cache^.Lines[baseSlot + w].LRU := 0; + cache^.Lines[baseSlot + (1 - w)].LRU := 1; + asm popf end; + exit; + end; + end; + asm popf end; + + { Miss path: acquire Busy spinlock to serialize eviction+load } + while true do begin + asm pushf; cli end; + if not cache^.Busy then begin + { Re-check for hit — another process may have loaded this sector } + for w := 0 to FAT_CACHE_WAYS - 1 do begin + if cache^.Lines[baseSlot + w].Valid and (cache^.Lines[baseSlot + w].SectorIdx = fatSecIdx) then begin + dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); + readFat := dataPtr[entryOff] and $0FFFFFFF; + cache^.Lines[baseSlot + w].LRU := 0; + cache^.Lines[baseSlot + (1 - w)].LRU := 1; + asm popf end; + exit; + end; + end; + cache^.Busy := true; + { Pick victim: prefer invalid way, else LRU way } + victim := baseSlot; + if (not cache^.Lines[baseSlot].Valid) then + victim := baseSlot + else if (not cache^.Lines[baseSlot + 1].Valid) then + victim := baseSlot + 1 + else if cache^.Lines[baseSlot + 1].LRU >= cache^.Lines[baseSlot].LRU then + victim := baseSlot + 1 + else + victim := baseSlot; + dataPtr := puint32(uint32(cache^.Data) + (victim * 512)); + doEvict := cache^.Lines[victim].Valid and cache^.Lines[victim].Dirty; + if doEvict then begin + evictLBA := cache^.FatStart + cache^.Lines[victim].SectorIdx; + core.util.memcpy(uint32(dataPtr), uint32(cache^.EvictBuf), 512); + end; + cache^.Lines[victim].Valid := false; + asm popf end; + break; + end; + asm popf end; + asm hlt end; + end; + + { Disk I/O with interrupts enabled — exclusive via Busy flag } + if doEvict then + driver.storage.mgr.storage_write(cache^.Device, evictLBA, 1, cache^.EvictBuf); + lba := cache^.FatStart + fatSecIdx; + driver.storage.mgr.storage_read(cache^.Device, lba, 1, dataPtr); + + { Update cache metadata, set LRU, release lock under CLI } + asm pushf; cli end; + cache^.Lines[victim].SectorIdx := fatSecIdx; + cache^.Lines[victim].Valid := true; + cache^.Lines[victim].Dirty := false; + { New entry is MRU, the other way is LRU } + cache^.Lines[victim].LRU := 0; + if victim = baseSlot then + cache^.Lines[baseSlot + 1].LRU := 1 + else + cache^.Lines[baseSlot].LRU := 1; + readFat := dataPtr[entryOff] and $0FFFFFFF; + cache^.Busy := false; + asm popf end; +end; + +{ Write a single FAT entry into cache. No immediate disk I/O — call fatCacheFlush later. + 2-way set-associative: set = fatSector mod FAT_CACHE_SETS. + CLI/STI protects cache metadata; disk I/O runs with interrupts enabled. } +procedure writeFat(volume : PStorage_volume; cluster : uint32; value : uint32; bootRecord : PBootRecord); +var + cache : PFATCache; + fatSecIdx : uint32; + setIdx : uint32; + baseSlot : uint32; + entryOff : uint32; + dataPtr : puint32; + lba : uint32; + doEvict : boolean; + evictLBA : uint32; + w : uint32; + victim : uint32; +begin + cache := fatCacheGet(volume); + fatSecIdx := cluster div 128; + setIdx := fatSecIdx mod FAT_CACHE_SETS; + baseSlot := setIdx * FAT_CACHE_WAYS; + entryOff := cluster mod 128; + + { Fast path: cache hit — check both ways } + asm pushf; cli end; + for w := 0 to FAT_CACHE_WAYS - 1 do begin + if cache^.Lines[baseSlot + w].Valid and (cache^.Lines[baseSlot + w].SectorIdx = fatSecIdx) then begin + dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); + if value = 0 then + dataPtr[entryOff] := 0 + else + dataPtr[entryOff] := (dataPtr[entryOff] and $F0000000) or (value and $0FFFFFFF); + cache^.Lines[baseSlot + w].Dirty := true; + { Update LRU: this way is MRU } + cache^.Lines[baseSlot + w].LRU := 0; + cache^.Lines[baseSlot + (1 - w)].LRU := 1; + asm popf end; + exit; + end; + end; + asm popf end; + + { Miss path: acquire Busy spinlock } + while true do begin + asm pushf; cli end; + if not cache^.Busy then begin + { Re-check for hit after acquiring lock } + for w := 0 to FAT_CACHE_WAYS - 1 do begin + if cache^.Lines[baseSlot + w].Valid and (cache^.Lines[baseSlot + w].SectorIdx = fatSecIdx) then begin + dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); + if value = 0 then + dataPtr[entryOff] := 0 + else + dataPtr[entryOff] := (dataPtr[entryOff] and $F0000000) or (value and $0FFFFFFF); + cache^.Lines[baseSlot + w].Dirty := true; + cache^.Lines[baseSlot + w].LRU := 0; + cache^.Lines[baseSlot + (1 - w)].LRU := 1; + asm popf end; + exit; + end; + end; + cache^.Busy := true; + { Pick victim: prefer invalid way, else LRU way } + victim := baseSlot; + if (not cache^.Lines[baseSlot].Valid) then + victim := baseSlot + else if (not cache^.Lines[baseSlot + 1].Valid) then + victim := baseSlot + 1 + else if cache^.Lines[baseSlot + 1].LRU >= cache^.Lines[baseSlot].LRU then + victim := baseSlot + 1 + else + victim := baseSlot; + dataPtr := puint32(uint32(cache^.Data) + (victim * 512)); + doEvict := cache^.Lines[victim].Valid and cache^.Lines[victim].Dirty; + if doEvict then begin + evictLBA := cache^.FatStart + cache^.Lines[victim].SectorIdx; + core.util.memcpy(uint32(dataPtr), uint32(cache^.EvictBuf), 512); + end; + cache^.Lines[victim].Valid := false; + asm popf end; + break; + end; + asm popf end; + asm hlt end; + end; + + { Disk I/O with interrupts enabled — exclusive via Busy flag } + if doEvict then + driver.storage.mgr.storage_write(cache^.Device, evictLBA, 1, cache^.EvictBuf); + lba := cache^.FatStart + fatSecIdx; + driver.storage.mgr.storage_read(cache^.Device, lba, 1, dataPtr); + + { Update cache, write value, set LRU, release lock under CLI } + asm pushf; cli end; + cache^.Lines[victim].SectorIdx := fatSecIdx; + cache^.Lines[victim].Valid := true; + cache^.Lines[victim].Dirty := false; + if value = 0 then + dataPtr[entryOff] := 0 + else + dataPtr[entryOff] := (dataPtr[entryOff] and $F0000000) or (value and $0FFFFFFF); + cache^.Lines[victim].Dirty := true; + cache^.Lines[victim].LRU := 0; + if victim = baseSlot then + cache^.Lines[baseSlot + 1].LRU := 1 + else + cache^.Lines[baseSlot].LRU := 1; + cache^.Busy := false; + asm popf end; +end; + +function getFatChain(volume : PStorage_volume; cluster : uint32; bootRecord : PBootRecord) : PLinkedListBase; +var + currentCluster : uint32; + currentClusterValue : uint32; + clusters : PLinkedListBase; + dirElm : puint32; + maxCluster : uint32; + iterCount : uint32; +begin + clusters:= LL_New(sizeof(uint32)); + currentCluster:= cluster; + currentClusterValue:= cluster; + if bootRecord^.sectorSize > 0 then + maxCluster := (bootRecord^.FATSize * bootRecord^.sectorSize) div 4 + else + maxCluster := 0; + iterCount := 0; + + while true do begin + if (maxCluster > 0) and (iterCount >= maxCluster) then + break; + currentClusterValue:= readFat(volume, currentClusterValue, bootRecord); + + if (currentClusterValue and $0FFFFFFF) = $0FFFFFF7 then begin + break; + end else if (currentClusterValue and $0FFFFFFF) >= $0FFFFFF8 then begin + dirElm:= LL_add(clusters); + dirElm^:= currentCluster; + break; + end else if currentClusterValue = 0 then begin + break; + end else begin + dirElm:= LL_add(clusters); + dirElm^:= currentCluster; + end; + + currentCluster := currentClusterValue; + iterCount := iterCount + 1; + end; + + getFatChain:= clusters; +end; + +function fatMaxClusterCount(bootRecord : PBootRecord) : uint32; +begin + if (bootRecord <> nil) and (bootRecord^.sectorSize > 0) then + fatMaxClusterCount := (bootRecord^.FATSize * bootRecord^.sectorSize) div 4 + else + fatMaxClusterCount := 0; +end; + +function fatDefaultSPC(sectors : uint32; sectorSize : uint32) : uint32; +begin + if sectorSize = 0 then begin + fatDefaultSPC := 1; + exit; + end; + + { Default to 4 KiB clusters for sane FAT32 throughput without excessive + slack on small files. Callers can still override via config. } + fatDefaultSPC := 4096 div sectorSize; + if fatDefaultSPC = 0 then + fatDefaultSPC := 1; + if fatDefaultSPC > 64 then + fatDefaultSPC := 64; +end; + +function fatComputeFATSize(sectors : uint32; spc : uint32; sectorSize : uint32) : uint32; +var + clusterCount : uint32; + fatBytes : uint32; +begin + if spc = 0 then + spc := 1; + if sectorSize = 0 then begin + fatComputeFATSize := 0; + exit; + end; + + clusterCount := (sectors + spc - 1) div spc; + fatBytes := clusterCount * 4; + fatComputeFATSize := (fatBytes + sectorSize - 1) div sectorSize; + if fatComputeFATSize = 0 then + fatComputeFATSize := 1; +end; + +function fatClustersForBytes(byteCount : uint32; bytesPerCluster : uint32) : uint32; +begin + if (byteCount = 0) or (bytesPerCluster = 0) then begin + fatClustersForBytes := 0; + exit; + end; + + fatClustersForBytes := (byteCount + bytesPerCluster - 1) div bytesPerCluster; +end; + +function fatPreallocClusterCount(bytesPerCluster : uint32) : uint32; +begin + if bytesPerCluster = 0 then begin + fatPreallocClusterCount := 1; + exit; + end; + + fatPreallocClusterCount := FAT_WRITE_PREALLOC_BYTES div bytesPerCluster; + if fatPreallocClusterCount = 0 then + fatPreallocClusterCount := 1; + if fatPreallocClusterCount < FAT_WRITE_PREALLOC_MIN_CLUSTERS then + fatPreallocClusterCount := FAT_WRITE_PREALLOC_MIN_CLUSTERS; + if fatPreallocClusterCount > FAT_WRITE_PREALLOC_MAX_CLUSTERS then + fatPreallocClusterCount := FAT_WRITE_PREALLOC_MAX_CLUSTERS; +end; + +procedure fatUpdateAllocHint(volume : PStorage_volume; bootRecord : PBootRecord; nextCluster : uint32); +var + maxCluster : uint32; +begin + maxCluster := fatMaxClusterCount(bootRecord); + if maxCluster <= 2 then begin + sioc_allocHint := 2; + sioc_allocVol := volume; + exit; + end; + + if nextCluster < 2 then + nextCluster := 2; + if nextCluster >= maxCluster then + nextCluster := 2; + + sioc_allocHint := nextCluster; + sioc_allocVol := volume; +end; + +function fatTryContiguousRange(volume : PStorage_volume; bootRecord : PBootRecord; + rangeStart : uint32; rangeEnd : uint32; needed : uint32; + var foundStart : uint32) : boolean; +var + scanPos : uint32; + runLen : uint32; + runPos : uint32; + fatVal : uint32; +begin + fatTryContiguousRange := false; + if (needed = 0) or (bootRecord = nil) or (rangeStart >= rangeEnd) then + exit; + + scanPos := rangeStart; + runLen := 0; + runPos := 0; + + while scanPos < rangeEnd do begin + fatVal := readFat(volume, scanPos, bootRecord); + if fatVal = 0 then begin + if runLen = 0 then + runPos := scanPos; + runLen := runLen + 1; + if runLen >= needed then begin + foundStart := runPos; + fatTryContiguousRange := true; + exit; + end; + end else + runLen := 0; + scanPos := scanPos + 1; + end; +end; + +function fatFindContiguousFreeRange(volume : PStorage_volume; amount : uint32; + bootRecord : PBootRecord; var runStart : uint32) : boolean; +var + scanStart : uint32; + maxCluster : uint32; +begin + fatFindContiguousFreeRange := false; + if amount = 0 then exit; + + maxCluster := fatMaxClusterCount(bootRecord); + if maxCluster <= 2 then exit; + + if (sioc_allocVol = volume) and (sioc_allocHint >= 2) and (sioc_allocHint < maxCluster) then + scanStart := sioc_allocHint + else + scanStart := 2; + + if fatTryContiguousRange(volume, bootRecord, scanStart, maxCluster, amount, runStart) or + ((scanStart > 2) and fatTryContiguousRange(volume, bootRecord, 2, scanStart, amount, runStart)) then + fatFindContiguousFreeRange := true; +end; + +procedure fatLinkContiguousRange(volume : PStorage_volume; bootRecord : PBootRecord; + previousTail : uint32; firstCluster : uint32; clusterCount : uint32; + var lastCluster : uint32); +var + cur : uint32; + i : uint32; +begin + lastCluster := previousTail; + if clusterCount = 0 then exit; + + if previousTail <> 0 then + writeFat(volume, previousTail, firstCluster, bootRecord); + + cur := firstCluster; + i := 1; + while i < clusterCount do begin + writeFat(volume, cur, cur + 1, bootRecord); + cur := cur + 1; + i := i + 1; + end; + + writeFat(volume, cur, $FFFFFFF8, bootRecord); + lastCluster := cur; + fatUpdateAllocHint(volume, bootRecord, lastCluster + 1); +end; + +function fatAppendExtent(extents : PLinkedListBase; firstCluster : uint32; + clusterCount : uint32) : boolean; +var + ext : PClusterExtent; + lastExt : PClusterExtent; + extCount : uint32; +begin + fatAppendExtent := false; + if (extents = nil) or (clusterCount = 0) then + exit; + + extCount := LL_Size(extents); + if extCount > 0 then begin + lastExt := PClusterExtent(LL_Get(extents, extCount - 1)); + if (lastExt <> nil) and ((lastExt^.StartCluster + lastExt^.ClusterCount) = firstCluster) then begin + lastExt^.ClusterCount := lastExt^.ClusterCount + clusterCount; + fatAppendExtent := true; + exit; + end; + end; + + ext := PClusterExtent(LL_Add(extents)); + if ext = nil then + exit; + ext^.StartCluster := firstCluster; + ext^.ClusterCount := clusterCount; + fatAppendExtent := true; +end; + +function fatBuildExtentList(volume : PStorage_volume; cluster : uint32; + bootRecord : PBootRecord) : PLinkedListBase; +var + extents : PLinkedListBase; + current : uint32; + nextCluster : uint32; + runStart : uint32; + runCount : uint32; + maxCluster : uint32; + iterCount : uint32; +begin + fatBuildExtentList := nil; + if (bootRecord = nil) or (cluster < 2) then + exit; + + extents := LL_New(sizeof(TClusterExtent)); + if extents = nil then + exit; + + if bootRecord^.sectorSize > 0 then + maxCluster := (bootRecord^.FATSize * bootRecord^.sectorSize) div 4 + else + maxCluster := 0; + + current := cluster; + runStart := cluster; + runCount := 1; + iterCount := 0; + + while true do begin + if (maxCluster > 0) and (iterCount >= maxCluster) then + break; + + nextCluster := readFat(volume, current, bootRecord); + if (nextCluster = 0) or ((nextCluster and $0FFFFFFF) = $0FFFFFF7) then + break; + + if (nextCluster and $0FFFFFFF) >= $0FFFFFF8 then begin + if not fatAppendExtent(extents, runStart, runCount) then begin + LL_Free(extents); + exit; + end; + fatBuildExtentList := extents; + exit; + end; + + if nextCluster = (current + 1) then + runCount := runCount + 1 + else begin + if not fatAppendExtent(extents, runStart, runCount) then begin + LL_Free(extents); + exit; + end; + runStart := nextCluster; + runCount := 1; + end; + + current := nextCluster; + iterCount := iterCount + 1; + end; + + if runCount > 0 then begin + if not fatAppendExtent(extents, runStart, runCount) then begin + LL_Free(extents); + exit; + end; + end; + + fatBuildExtentList := extents; +end; + +function fatLocateExtentRun(extents : PLinkedListBase; targetIdx : uint32; + hintValid : boolean; hintExtentIdx : uint32; + hintStartIdx : uint32; var runStartCluster : uint32; + var runClusterCount : uint32; var outExtentIdx : uint32; + var outExtentStartIdx : uint32) : boolean; +var + i : uint32; + clusterIx : uint32; + extCount : uint32; + ext : PClusterExtent; +begin + fatLocateExtentRun := false; + if extents = nil then + exit; + + extCount := LL_Size(extents); + if extCount = 0 then + exit; + + if hintValid and (hintExtentIdx < extCount) and (hintStartIdx <= targetIdx) then begin + i := hintExtentIdx; + clusterIx := hintStartIdx; + end else begin + i := 0; + clusterIx := 0; + end; + + while i < extCount do begin + ext := PClusterExtent(LL_Get(extents, i)); + if ext = nil then + exit; + + if targetIdx < (clusterIx + ext^.ClusterCount) then begin + runStartCluster := ext^.StartCluster + (targetIdx - clusterIx); + runClusterCount := ext^.ClusterCount - (targetIdx - clusterIx); + outExtentIdx := i; + outExtentStartIdx := clusterIx; + fatLocateExtentRun := true; + exit; + end; + + clusterIx := clusterIx + ext^.ClusterCount; + i := i + 1; + end; +end; + +function findFreeClusterExtents(volume : PStorage_volume; amount : uint32; + bootRecord : PBootRecord) : PLinkedListBase; +var + maxCluster : uint32; + scanStart : uint32; + currentAmount : uint32; + lastAllocated : uint32; + extents : PLinkedListBase; + + procedure appendExtent(firstCluster : uint32; clusterCount : uint32); + var + ext : PClusterExtent; + begin + ext := PClusterExtent(LL_Add(extents)); + ext^.StartCluster := firstCluster; + ext^.ClusterCount := clusterCount; + currentAmount := currentAmount + clusterCount; + lastAllocated := firstCluster + clusterCount - 1; + end; + + procedure scanRange(rangeStart : uint32; rangeEnd : uint32); + var + scanPos : uint32; + runStart : uint32; + runLen : uint32; + takeCount : uint32; + fatVal : uint32; + begin + scanPos := rangeStart; + while (scanPos < rangeEnd) and (currentAmount < amount) do begin + fatVal := readFat(volume, scanPos, bootRecord); + if fatVal <> 0 then begin + scanPos := scanPos + 1; + continue; + end; + + runStart := scanPos; + runLen := 0; + while scanPos < rangeEnd do begin + fatVal := readFat(volume, scanPos, bootRecord); + if fatVal <> 0 then + break; + runLen := runLen + 1; + scanPos := scanPos + 1; + end; + + takeCount := amount - currentAmount; + if runLen < takeCount then + takeCount := runLen; + appendExtent(runStart, takeCount); + end; + end; +begin + extents := LL_New(sizeof(TClusterExtent)); + currentAmount := 0; + lastAllocated := 0; + + if amount = 0 then begin + findFreeClusterExtents := extents; + exit; + end; + + maxCluster := fatMaxClusterCount(bootRecord); + if maxCluster <= 2 then begin + LL_Free(extents); + findFreeClusterExtents := nil; + exit; + end; + + if (sioc_allocVol = volume) and (sioc_allocHint >= 2) and (sioc_allocHint < maxCluster) then + scanStart := sioc_allocHint + else + scanStart := 2; + + scanRange(scanStart, maxCluster); + if (currentAmount < amount) and (scanStart > 2) then + scanRange(2, scanStart); + + if currentAmount < amount then begin + LL_Free(extents); + findFreeClusterExtents := nil; + exit; + end; + + fatUpdateAllocHint(volume, bootRecord, lastAllocated + 1); + findFreeClusterExtents := extents; +end; + +function compareByteArray8(str1 : byteArray8; str2 : byteArray8) : boolean; +var + i : uint32; +begin + push_trace('driver.storage.fs.fat32.compareArray'); + compareByteArray8:= true; + for i:=0 to 7 do begin + if str1[i] <> str2[i] then begin + compareByteArray8:= false; + break; + end; + end; +end; + +{ Extract the starting cluster number from a directory entry. } +function dirFirstCluster(dir : PDirectory) : uint32; +begin + dirFirstCluster := uint32(dir^.clusterLow) or uint32(dir^.clusterHigh shl 16); +end; + +{ Set the starting cluster number in a directory entry. } +procedure setDirFirstCluster(dir : PDirectory; cluster : uint32); +begin + dir^.clusterLow := uint16(cluster and $FFFF); + dir^.clusterHigh := uint16((cluster shr 16) and $FFFF); +end; + +{ Calculate the LBA of the first data sector on a FAT32 volume. } +function fatDataStartLBA(volume : PStorage_Volume; bootRecord : PBootRecord) : uint32; +begin + fatDataStartLBA := volume^.sectorStart + bootRecord^.rsvSectors + bootRecord^.FATSize; +end; + +{ Convert a cluster number to its starting LBA. } +function clusterToLBA(dataStart : uint32; spc : uint32; cluster : uint32) : uint32; +begin + if cluster < 2 then + clusterToLBA := dataStart + else + clusterToLBA := dataStart + ((cluster - 2) * spc); +end; + +{ Write an 8.3 extension into a TFATExtArray: uppercase, pad with spaces. } +procedure fillFatExt(var dest : TFATExtArray; extPart : pchar); +var + j : uint32; +begin + dest[0] := ' '; + dest[1] := ' '; + dest[2] := ' '; + if (extPart <> nil) and (extPart[0] <> char(0)) then begin + for j := 0 to 2 do begin + if extPart[j] = char(0) then break; + if (extPart[j] >= 'a') and (extPart[j] <= 'z') then + dest[j] := char(uint8(extPart[j]) - 32) + else + dest[j] := extPart[j]; + end; + end; +end; + +{ Search a single sector buffer for a directory entry matching name+ext. + Returns the index (0..entriesPerSec-1), or -1 if not found. + Sets hitEnd to true if a char(0) sentinel was reached (end of directory). } +function findEntryInSector(buffer : puint32; entriesPerSec : uint32; + name : byteArray8; ext : pchar; var hitEnd : boolean) : integer; +var + idx : uint32; + dir : PDirectory; +begin + findEntryInSector := -1; + hitEnd := false; + if entriesPerSec > 0 then + for idx := 0 to entriesPerSec - 1 do begin + dir := @PDirectory(buffer)[idx]; + if dir^.fileName[0] = char(0) then begin + hitEnd := true; + exit; + end; + if dir^.fileName[0] = char($E5) then continue; + if compareByteArray8(dir^.fileName, name) + and matchExtension(dir^.fileExtension, ext) then begin + findEntryInSector := integer(idx); + exit; + end; + end; +end; + +{ Search a single sector buffer for a free directory entry (char(0) or $E5). + Returns the index (0..entriesPerSec-1), or -1 if sector is full. } +function findFreeEntryInSector(buffer : puint32; entriesPerSec : uint32) : integer; +var + idx : uint32; + dir : PDirectory; +begin + findFreeEntryInSector := -1; + if entriesPerSec > 0 then + for idx := 0 to entriesPerSec - 1 do begin + dir := @PDirectory(buffer)[idx]; + if (dir^.fileName[0] = char(0)) or (dir^.fileName[0] = char($E5)) then begin + findFreeEntryInSector := integer(idx); + exit; + end; + end; +end; + +{ --- Higher-level helpers --- } + +type + TDirEntryLocation = record + Found : boolean; + SectorLBA : uint32; + EntryIdx : uint32; + end; + + TFileLookup = record + BootRecord : PBootRecord; + Dirs : PLinkedListBase; + Dir : PDirectory; + Exists : boolean; + Status : puint32; + end; + + TRunInfo = record + StartCluster : uint32; + LastCluster : uint32; + RunLen : uint32; + RunSectors : uint32; + RunBytes : uint32; + LBA : uint32; + end; + +{ Locate a named entry in the parent directory's raw sectors. + Walks the full FAT chain of parentCluster, scanning each sector + with findEntryInSector. On match, fills loc and leaves buffer + containing the matching sector (caller must kfree buffer). + Returns true if found. } +function locateDirEntry( + volume : PStorage_Volume; + parentCluster : uint32; + bootRecord : PBootRecord; + cleanName : byteArray8; + ext : pchar; + var loc : TDirEntryLocation; + buffer : puint32 +) : boolean; +var + dataStart : uint32; + spc : uint32; + entriesPerSector : uint32; + dirChain : PLinkedListBase; + dc : uint32; + ds : uint32; + curCluster : uint32; + clusterLBA : uint32; + hitEnd : boolean; + entryIdx : integer; + done : boolean; +begin + locateDirEntry := false; + loc.Found := false; + + dataStart := fatDataStartLBA(volume, bootRecord); + spc := uint32(bootRecord^.spc); + entriesPerSector := uint32(bootRecord^.sectorSize) div uint32(sizeof(TDirectory)); + dirChain := getFatChain(volume, parentCluster, bootRecord); + done := false; + + dc := 0; + while (dc < LL_size(dirChain)) and (not done) do begin + curCluster := puint32(LL_Get(dirChain, dc))^; + clusterLBA := clusterToLBA(dataStart, spc, curCluster); + ds := 0; + while (ds < spc) and (not done) do begin + driver.storage.mgr.storage_read(volume^.device, clusterLBA + ds, 1, buffer); + entryIdx := findEntryInSector(buffer, entriesPerSector, cleanName, ext, hitEnd); + if hitEnd then + done := true + else if entryIdx >= 0 then begin + loc.Found := true; + loc.SectorLBA := clusterLBA + ds; + loc.EntryIdx := uint32(entryIdx); + done := true; + end; + ds := ds + 1; + end; + dc := dc + 1; + end; + + LL_Free(dirChain); + locateDirEntry := loc.Found; +end; + +{ Locate an unused (free) directory entry slot in the parent directory. + Returns true if a free slot was found, filling loc and leaving buffer + containing the sector. } +function locateFreeDirEntry( + volume : PStorage_Volume; + parentCluster : uint32; + bootRecord : PBootRecord; + var loc : TDirEntryLocation; + buffer : puint32 +) : boolean; +var + dataStart : uint32; + spc : uint32; + entriesPerSector : uint32; + dirChain : PLinkedListBase; + dc : uint32; + ds : uint32; + curCluster : uint32; + clusterLBA : uint32; + entryIdx : integer; + found : boolean; +begin + locateFreeDirEntry := false; + loc.Found := false; + + dataStart := fatDataStartLBA(volume, bootRecord); + spc := uint32(bootRecord^.spc); + entriesPerSector := uint32(bootRecord^.sectorSize) div uint32(sizeof(TDirectory)); + dirChain := getFatChain(volume, parentCluster, bootRecord); + found := false; + + if LL_size(dirChain) > 0 then begin + for dc := 0 to LL_size(dirChain) - 1 do begin + curCluster := puint32(LL_Get(dirChain, dc))^; + clusterLBA := clusterToLBA(dataStart, spc, curCluster); + for ds := 0 to spc - 1 do begin + driver.storage.mgr.storage_read(volume^.device, clusterLBA + ds, 1, buffer); + entryIdx := findFreeEntryInSector(buffer, entriesPerSector); + if entryIdx >= 0 then begin + loc.Found := true; + loc.SectorLBA := clusterLBA + ds; + loc.EntryIdx := uint32(entryIdx); + found := true; + end; + if found then break; + end; + if found then break; + end; + end; + + LL_Free(dirChain); + locateFreeDirEntry := loc.Found; +end; + +{ Build a contiguous cluster run starting at startCluster. + Scans ahead via readFat, accumulating consecutive clusters up to + maxRunSectors worth of sectors. + Returns the number of clusters in the run (runLen). + Sets lastCluster to the last cluster in the run (= startCluster + runLen - 1). } +function buildContiguousRun( + volume : PStorage_Volume; + startCluster : uint32; + spc : uint32; + maxRunSectors : uint32; + bootRecord : PBootRecord; + var runLen : uint32; + var lastCluster : uint32 +) : uint32; +var + nextFat : uint32; +begin + runLen := 1; + lastCluster := startCluster; + + while (runLen * spc) < maxRunSectors do begin + nextFat := readFat(volume, lastCluster, bootRecord); + if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then + break; + if nextFat <> lastCluster + 1 then + break; + lastCluster := nextFat; + runLen := runLen + 1; + end; + + buildContiguousRun := runLen; +end; + +procedure updateSIOC( + volume : PStorage_Volume; + fileStartCluster : uint32; + chainIdx : uint32; + chainCluster : uint32 +); forward; + +function seekToClusterIndex( + volume : PStorage_Volume; + fileStartCluster : uint32; + targetIdx : uint32; + bootRecord : PBootRecord; + var curCluster : uint32; + var chainPos : uint32 +) : boolean; forward; + +function seekToClusterIndexHinted( + volume : PStorage_Volume; + fileStartCluster : uint32; + targetIdx : uint32; + bootRecord : PBootRecord; + hintValid : boolean; + hintIdx : uint32; + hintCluster : uint32; + var curCluster : uint32; + var chainPos : uint32 +) : boolean; forward; + +procedure fat32_fileio_async_complete(ctx : PFATFileIOAsyncCtx; error : TError); +begin + if ctx = nil then exit; + if ctx^.BytesOut <> nil then + ctx^.BytesOut^ := ctx^.BytesDone; + if ctx^.Callback <> nil then + ctx^.Callback(error, ctx^.CallbackData); + fatTransferFree(ctx); +end; + +function fat32_fileio_prepare_single_run(ctx : PFATFileIOAsyncCtx; + var sectorLBA : uint32; var chunkSectors : uint32) : boolean; +const + MAX_RUN_SECTORS = 2048; +var + run : TRunInfo; + curCluster : uint32; + chainPos : uint32; + currentOffset : uint32; + bytesPerCluster : uint32; + startClusterIdx : uint32; + inClusterOffset : uint32; + remaining : uint32; + skipBytes : uint32; + copyBytes : uint32; + secSize : uint32; + spc : uint32; + lastBytePos : uint32; + lastClusterUsed : uint32; + lastClusterIdx : uint32; + maxRunClusters : uint32; + extentIdx : uint32; + extentStartIdx : uint32; +begin + fat32_fileio_prepare_single_run := false; + if ctx = nil then + exit; + + currentOffset := ctx^.Offset + ctx^.BytesDone; + remaining := ctx^.ByteCount - ctx^.BytesDone; + spc := uint32(ctx^.BootRecord^.spc); + secSize := uint32(ctx^.BootRecord^.sectorSize); + bytesPerCluster := spc * secSize; + startClusterIdx := currentOffset div bytesPerCluster; + inClusterOffset := currentOffset mod bytesPerCluster; + + maxRunClusters := MAX_RUN_SECTORS div spc; + if maxRunClusters = 0 then + maxRunClusters := 1; + + if (ctx^.OpenFile <> nil) and (ctx^.OpenFile^.Extents <> nil) and + fatLocateExtentRun(ctx^.OpenFile^.Extents, startClusterIdx, + ctx^.ExtentValid, ctx^.ExtentIdx, ctx^.ExtentStartIdx, + run.StartCluster, run.RunLen, extentIdx, extentStartIdx) then begin + if run.RunLen > maxRunClusters then + run.RunLen := maxRunClusters; + run.LastCluster := run.StartCluster + run.RunLen - 1; + ctx^.ExtentValid := true; + ctx^.ExtentIdx := extentIdx; + ctx^.ExtentStartIdx := extentStartIdx; + curCluster := run.LastCluster; + chainPos := startClusterIdx + run.RunLen - 1; + end else begin + if not seekToClusterIndexHinted( + ctx^.Volume, ctx^.Cluster, startClusterIdx, ctx^.BootRecord, + ctx^.CursorValid, ctx^.CursorIndex, ctx^.CursorCluster, + curCluster, chainPos) then + exit; + + run.StartCluster := curCluster; + buildContiguousRun(ctx^.Volume, curCluster, spc, MAX_RUN_SECTORS, ctx^.BootRecord, run.RunLen, run.LastCluster); + end; + + run.RunSectors := run.RunLen * spc; + run.RunBytes := run.RunSectors * secSize; + run.LBA := clusterToLBA(ctx^.DataStart, spc, run.StartCluster); + + skipBytes := inClusterOffset; + copyBytes := run.RunBytes - skipBytes; + if copyBytes > remaining then + copyBytes := remaining; + + if (copyBytes = 0) or ((copyBytes mod secSize) <> 0) or ((skipBytes mod secSize) <> 0) then + exit; + + sectorLBA := run.LBA + (skipBytes div secSize); + chunkSectors := copyBytes div secSize; + ctx^.ChunkBytes := copyBytes; + + lastBytePos := skipBytes + copyBytes - 1; + lastClusterIdx := startClusterIdx + (lastBytePos div bytesPerCluster); + lastClusterUsed := run.StartCluster + (lastBytePos div bytesPerCluster); + updateSIOC(ctx^.Volume, ctx^.Cluster, lastClusterIdx, lastClusterUsed); + ctx^.CursorValid := true; + ctx^.CursorIndex := lastClusterIdx; + ctx^.CursorCluster := lastClusterUsed; + + fat32_fileio_prepare_single_run := true; +end; + +procedure fat32_fileio_direct_complete(error : TError; userdata : pointer); +var + ctx : PFATFileIOAsyncCtx; +begin + ctx := PFATFileIOAsyncCtx(userdata); + if ctx = nil then exit; + + if error = eNone then begin + ctx^.BytesDone := ctx^.BytesDone + ctx^.ChunkBytes; + if (ctx^.Mode = famWrite) and (ctx^.OpenFile <> nil) then begin + if (ctx^.BytesDone > 0) and ((ctx^.Offset + ctx^.BytesDone) > ctx^.OpenFile^.ByteSize) then begin + ctx^.OpenFile^.ByteSize := ctx^.Offset + ctx^.BytesDone; + ctx^.OpenFile^.MetaDirty := true; + end; + if (ctx^.OpenFile^.FirstCluster = 0) and (ctx^.Cluster <> 0) then begin + ctx^.OpenFile^.FirstCluster := ctx^.Cluster; + ctx^.OpenFile^.Exists := true; + ctx^.OpenFile^.MetaDirty := true; + end; + end; + end; + + fat32_fileio_async_complete(ctx, error); +end; + +procedure fat32_fileio_async_step(error : TError; userdata : pointer); forward; + +procedure fat32_fileio_async_run_next(ctx : PFATFileIOAsyncCtx); +const + MAX_RUN_SECTORS = 2048; +var + run : TRunInfo; + curCluster : uint32; + chainPos : uint32; + currentOffset : uint32; + bytesPerCluster : uint32; + startClusterIdx : uint32; + inClusterOffset : uint32; + remaining : uint32; + skipBytes : uint32; + copyBytes : uint32; + secSize : uint32; + spc : uint32; + sectorLBA : uint32; + chunkSectors : uint32; + lastBytePos : uint32; + lastClusterUsed : uint32; + lastClusterIdx : uint32; + maxRunClusters : uint32; + extentIdx : uint32; + extentStartIdx : uint32; + err : TError; +begin + if ctx = nil then exit; + + if ctx^.State = ftsAwaitIO then begin + if ctx^.LastError <> eNone then begin + ctx^.State := ftsError; + fat32_fileio_async_complete(ctx, ctx^.LastError); + exit; + end; + + ctx^.State := ftsAdvance; + ctx^.BytesDone := ctx^.BytesDone + ctx^.ChunkBytes; + + if (ctx^.Mode = famWrite) and (ctx^.OpenFile <> nil) then begin + if (ctx^.BytesDone > 0) and ((ctx^.Offset + ctx^.BytesDone) > ctx^.OpenFile^.ByteSize) then begin + ctx^.OpenFile^.ByteSize := ctx^.Offset + ctx^.BytesDone; + ctx^.OpenFile^.MetaDirty := true; + end; + if (ctx^.OpenFile^.FirstCluster = 0) and (ctx^.Cluster <> 0) then begin + ctx^.OpenFile^.FirstCluster := ctx^.Cluster; + ctx^.OpenFile^.Exists := true; + ctx^.OpenFile^.MetaDirty := true; + end; + end; + end; + + if ctx^.BytesDone >= ctx^.ByteCount then begin + ctx^.State := ftsComplete; + fat32_fileio_async_complete(ctx, eNone); + exit; + end; + + ctx^.State := ftsPrepare; + + currentOffset := ctx^.Offset + ctx^.BytesDone; + remaining := ctx^.ByteCount - ctx^.BytesDone; + spc := uint32(ctx^.BootRecord^.spc); + secSize := uint32(ctx^.BootRecord^.sectorSize); + bytesPerCluster := spc * secSize; + startClusterIdx := currentOffset div bytesPerCluster; + inClusterOffset := currentOffset mod bytesPerCluster; + + maxRunClusters := MAX_RUN_SECTORS div spc; + if maxRunClusters = 0 then + maxRunClusters := 1; + + if (ctx^.OpenFile <> nil) and (ctx^.OpenFile^.Extents <> nil) and + fatLocateExtentRun(ctx^.OpenFile^.Extents, startClusterIdx, + ctx^.ExtentValid, ctx^.ExtentIdx, ctx^.ExtentStartIdx, + run.StartCluster, run.RunLen, extentIdx, extentStartIdx) then begin + if run.RunLen > maxRunClusters then + run.RunLen := maxRunClusters; + run.LastCluster := run.StartCluster + run.RunLen - 1; + ctx^.ExtentValid := true; + ctx^.ExtentIdx := extentIdx; + ctx^.ExtentStartIdx := extentStartIdx; + curCluster := run.LastCluster; + chainPos := startClusterIdx + run.RunLen - 1; + end else begin + if not seekToClusterIndexHinted( + ctx^.Volume, ctx^.Cluster, startClusterIdx, ctx^.BootRecord, + ctx^.CursorValid, ctx^.CursorIndex, ctx^.CursorCluster, + curCluster, chainPos) then begin + ctx^.State := ftsError; + fat32_fileio_async_complete(ctx, eCorruptFilesystem); + exit; + end; + + run.StartCluster := curCluster; + buildContiguousRun(ctx^.Volume, curCluster, spc, MAX_RUN_SECTORS, ctx^.BootRecord, run.RunLen, run.LastCluster); + end; + run.RunSectors := run.RunLen * spc; + run.RunBytes := run.RunSectors * secSize; + run.LBA := clusterToLBA(ctx^.DataStart, spc, run.StartCluster); + + skipBytes := inClusterOffset; + copyBytes := run.RunBytes - skipBytes; + if copyBytes > remaining then + copyBytes := remaining; + + if (copyBytes = 0) or ((copyBytes mod secSize) <> 0) or ((skipBytes mod secSize) <> 0) then begin + ctx^.State := ftsError; + fat32_fileio_async_complete(ctx, eInvalidArgument); + exit; + end; + + sectorLBA := run.LBA + (skipBytes div secSize); + chunkSectors := copyBytes div secSize; + ctx^.ChunkBytes := copyBytes; + + lastBytePos := skipBytes + copyBytes - 1; + lastClusterIdx := startClusterIdx + (lastBytePos div bytesPerCluster); + lastClusterUsed := run.StartCluster + (lastBytePos div bytesPerCluster); + updateSIOC(ctx^.Volume, ctx^.Cluster, lastClusterIdx, lastClusterUsed); + ctx^.CursorValid := true; + ctx^.CursorIndex := lastClusterIdx; + ctx^.CursorCluster := lastClusterUsed; + + ctx^.State := ftsIssueIO; + + if ctx^.Mode = famRead then + err := driver.storage.mgr.storage_read_async( + ctx^.Volume^.device, sectorLBA, chunkSectors, + puint32(uint32(ctx^.Buffer) + ctx^.BytesDone), @fat32_fileio_async_step, ctx) + else + err := driver.storage.mgr.storage_write_async( + ctx^.Volume^.device, sectorLBA, chunkSectors, + puint32(uint32(ctx^.Buffer) + ctx^.BytesDone), @fat32_fileio_async_step, ctx); + + if err = eNone then + ctx^.State := ftsAwaitIO + else + ctx^.State := ftsError; + if err <> eNone then + fat32_fileio_async_complete(ctx, err); +end; + +procedure fat32_fileio_async_step(error : TError; userdata : pointer); +var + ctx : PFATFileIOAsyncCtx; +begin + ctx := PFATFileIOAsyncCtx(userdata); + if ctx = nil then exit; + ctx^.LastError := error; + if not fatScheduleTransfer(ctx) then begin + ctx^.State := ftsError; + fat32_fileio_async_complete(ctx, eQueueFull); + end; +end; + +{ Update the SIOC chain-position cache. } +procedure updateSIOC( + volume : PStorage_Volume; + fileStartCluster : uint32; + chainIdx : uint32; + chainCluster : uint32 +); +begin + sioc_vol := volume; + sioc_fileClust := fileStartCluster; + sioc_chainIdx := chainIdx; + sioc_chainClust := chainCluster; + sioc_valid := true; +end; + +{ Seek to a specific cluster index in a file's FAT chain. + Uses the SIOC cache to resume from a previously cached position + when possible, otherwise walks from the file's start cluster. + Returns true on success, false if the chain ended prematurely. } +function seekToClusterIndex( + volume : PStorage_Volume; + fileStartCluster : uint32; + targetIdx : uint32; + bootRecord : PBootRecord; + var curCluster : uint32; + var chainPos : uint32 +) : boolean; +var + nextFat : uint32; + maxCluster : uint32; +begin + seekToClusterIndex := false; + if bootRecord^.sectorSize > 0 then + maxCluster := (bootRecord^.FATSize * bootRecord^.sectorSize) div 4 + else + maxCluster := 0; + + { Resume from SIOC cache if it covers this file at a useful position } + if sioc_valid and (sioc_vol = volume) and (sioc_fileClust = fileStartCluster) + and (sioc_chainIdx <= targetIdx) then begin + curCluster := sioc_chainClust; + chainPos := sioc_chainIdx; + end else begin + curCluster := fileStartCluster; + chainPos := 0; + end; + + { Walk forward to targetIdx } + while chainPos < targetIdx do begin + if (maxCluster > 0) and (chainPos >= maxCluster) then + exit; + nextFat := readFat(volume, curCluster, bootRecord); + if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then + exit; + curCluster := nextFat; + chainPos := chainPos + 1; + end; + + seekToClusterIndex := true; +end; + +function seekToClusterIndexHinted( + volume : PStorage_Volume; + fileStartCluster : uint32; + targetIdx : uint32; + bootRecord : PBootRecord; + hintValid : boolean; + hintIdx : uint32; + hintCluster : uint32; + var curCluster : uint32; + var chainPos : uint32 +) : boolean; +var + nextFat : uint32; + maxCluster : uint32; +begin + seekToClusterIndexHinted := false; + if bootRecord^.sectorSize > 0 then + maxCluster := (bootRecord^.FATSize * bootRecord^.sectorSize) div 4 + else + maxCluster := 0; + + if hintValid and (hintIdx <= targetIdx) then begin + curCluster := hintCluster; + chainPos := hintIdx; + end else begin + if not seekToClusterIndex(volume, fileStartCluster, targetIdx, bootRecord, curCluster, chainPos) then + exit; + seekToClusterIndexHinted := true; + exit; + end; + + while chainPos < targetIdx do begin + if (maxCluster > 0) and (chainPos >= maxCluster) then + exit; + nextFat := readFat(volume, curCluster, bootRecord); + if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then + exit; + curCluster := nextFat; + chainPos := chainPos + 1; + end; + + seekToClusterIndexHinted := true; +end; + +//TODO add optional attributes flag to refine what i return +function getDirEntries(volume : PStorage_volume; cluster : uint32; bootRecord : PBootRecord) : PLinkedListBase; +var + buffer : puint32; + bufferI : puint32; + clusters : PLinkedListBase; + directories : PLinkedListBase; + i : uint32 = 0; + maxEntries : uint32; + datastart : uint32; + sectorLocation : uint32; + dirElm : puint32; +begin + push_trace('driver.storage.fs.fat32.getDirEntries.enter'); + directories:= LL_New(sizeof(TDirectory)); + + clusters:= PLinkedListBase(getFatChain(volume, cluster, bootRecord)); + if (clusters = nil) or (LL_size(clusters) = 0) then begin + getDirEntries := directories; + if clusters <> nil then LL_Free(clusters); + push_trace('driver.storage.fs.fat32.getDirEntries.exit'); + exit; + end; + push_trace('driver.storage.fs.fat32.getDirEntries.allocBuffer'); + buffer:= puint32(kalloc( (bootRecord^.sectorSize * bootRecord^.spc) * LL_size(clusters) )); + memset(uint32(buffer), 0, (bootRecord^.sectorSize * bootRecord^.spc) * LL_size(clusters) ); + + dataStart:= fatDataStartLBA(volume, bootRecord); + + push_trace('driver.storage.fs.fat32.getDirEntries.readSectors'); + for i:=0 to LL_size(clusters) - 1 do begin + sectorLocation:= clusterToLBA(dataStart, bootRecord^.spc, puint32(LL_Get(clusters, i))^); + bufferI:= puint32(uint32(buffer) + uint32(i * bootRecord^.spc * bootRecord^.sectorSize)); + driver.storage.mgr.storage_read(volume^.device, sectorLocation, bootRecord^.spc, bufferI); + end; + + i:=0; + maxEntries:= ((bootRecord^.sectorSize * bootRecord^.spc) * LL_size(clusters)) div sizeof(TDirectory); + while i < maxEntries do begin + if PDirectory(buffer)[i].fileName[0] = char(0) then break; + + { Skip deleted entries ($E5 = FAT32 deleted marker) } + if PDirectory(buffer)[i].fileName[0] = char($E5) then begin + i+=1; + continue; + end; + + dirElm:= LL_Add(directories); + PDirectory(dirElm)^:= PDirectory(buffer)[i]; + i+=1; + end; + + getDirEntries:= directories; //get last . + LL_Free(clusters); + kfree(buffer); + push_trace('driver.storage.fs.fat32.getDirEntries.exit'); +end; + +function fat2GenericEntries(list : PLinkedListBase) : PLinkedListBase; +var + i : uint32; + entry : PDirectory_Entry; + dir : PDirectory; + dirElm : puint32; + nameBuf : pchar; + nameLen : uint32; + extLen : uint32; +begin + push_trace('driver.storage.fs.fat32.fat2GenericEntries.enter'); + puint32(entry) := kalloc(sizeof(TDirectory_Entry)); + fat2GenericEntries:= LL_New(sizeof(TDirectory_Entry)); + + if LL_size(list) > 0 then begin + + for i:= 0 to LL_Size(list) - 1 do begin + dir := PDirectory(LL_get(list, i)); + + { Skip deleted entries (should already be filtered by getDirEntries, but guard here too) } + if dir^.fileName[0] = char($E5) then continue; + + { Trim trailing spaces from fileName (8 bytes) } + nameLen := 8; + while (nameLen > 0) and (dir^.fileName[nameLen - 1] = ' ') do + nameLen := nameLen - 1; + + { Trim trailing spaces from extension (3 bytes) } + extLen := 3; + while (extLen > 0) and (dir^.fileExtension[extLen - 1] = ' ') do + extLen := extLen - 1; + + { Build combined filename: NAME.EXT or just NAME } + if extLen > 0 then begin + nameBuf := pchar(kalloc(nameLen + 1 + extLen + 1)); + memcpy(uint32(@dir^.fileName[0]), uint32(nameBuf), nameLen); + nameBuf[nameLen] := '.'; + memcpy(uint32(@dir^.fileExtension[0]), uint32(@nameBuf[nameLen + 1]), extLen); + nameBuf[nameLen + 1 + extLen] := char(0); + end else begin + nameBuf := pchar(kalloc(nameLen + 1)); + memcpy(uint32(@dir^.fileName[0]), uint32(nameBuf), nameLen); + nameBuf[nameLen] := char(0); + end; + + entry^.fileName := nameBuf; + + if dir^.attributes = $10 then begin + entry^.entryType:= TDirectory_Entry_Type.directoryEntry; + end else begin + entry^.entryType:= TDirectory_Entry_Type.fileEntry; + end; + + entry^.fileSize := dir^.byteSize; + entry^.modifiedDate := dir^.modifiedDate; + entry^.modifiedTime := dir^.modifiedTime; + entry^.attributes := dir^.attributes; + + //add to list + dirElm:= LL_add(fat2GenericEntries); + PDirectory_Entry(dirElm)^:= entry^; + end; + end; + + LL_Free(list); + kfree(puint32(entry)); + push_trace('driver.storage.fs.fat32.fat2GenericEntries.exit'); +end; + +//need to find out why having multiple dir stings isn't working, maybe the ls command? did I fix this? +function readDirectory(volume : PStorage_volume; directory : pchar; statusOut : puint32) : PLinkedListBase; //statusout: 0 = success, 1 = dir not exsist, 2 = not directory, 3 = invalid name, 4= already exists +var + bootRecord : PBootRecord; + directoryStrings : PLinkedListBase; + directories : PLinkedListBase; + cluster : uint32; + i : uint32; + ii : uint32 = 0; + dirEntry : PDirectory; + status : puint32; +begin + status:= puint32(kalloc(sizeof(uint32))); + push_trace('driver.storage.fs.fat32.readDirectory.enter'); + status^:= ord(eNone); + bootRecord:= readBootRecord(volume); + directoryStrings:= LL_fromString(directory, '/'); + directories:= getDirEntries(volume, bootRecord^.rootCluster, bootRecord); + + if LL_size(directoryStrings) > 0 then begin + for i:=0 to (LL_Size(directoryStrings) ) do begin /// maybe -1 will work + ii:=0; + + while true do begin + if ii > LL_Size(directories) - 1 then begin + status^:= ord(eDirectoryDoesNotExist); + break; + end; + dirEntry:= PDirectory(LL_Get(directories, ii)); + + if compareByteArray8( dirEntry^.fileName, cleanString( pchar(puint32(LL_Get(directoryStrings, i))^), status)) then begin + cluster:= dirFirstCluster(dirEntry); + break; + end; + ii+=1; + end; + + if status^ <> 0 then break; + + LL_Free(directories); + directories:= getDirEntries(volume, cluster, bootRecord); + + if i = LL_Size(directoryStrings) - 1 then break; + end; + end else begin + while true do begin + if ii > LL_Size(directories) - 1 then break; + dirEntry:= PDirectory(LL_Get(directories, ii)); + ii+=1; + end; + end; + + readDirectory:= directories; + + if statusOut <> nil then statusOut^ := status^; + kfree(status); + LL_Free(directoryStrings); + kfree(puint32(bootRecord)); + push_trace('driver.storage.fs.fat32.readDirectory.exit'); +end; + +function readDirectoryGen(volume : PStorage_volume; directory : pchar; status : puint32) : PLinkedListBase; //returns: 0 = success, 1 = dir not exsist, 2 = not directory, 3 = invalid name, 4= already exists +begin + push_trace('driver.storage.fs.fat32.readDirectoryGen.enter'); + readDirectoryGen:= fat2GenericEntries(readDirectory(volume, directory, status)); + push_trace('driver.storage.fs.fat32.readDirectoryGen.exit'); +end; + +//need to allow for setting file extension +function writeDirectory(volume : PStorage_volume; directory : pchar; dirName : pchar; attributes : uint32; statusOut : puint32) : uint32; // need to handle parent table cluster overflow, need to take attributes +var + directories : PLinkedListBase; + parentDirectory : PDirectory; + parentCluster : uint32; + extents : PLinkedListBase; + extent : PClusterExtent; + cluster : uint32; + bootRecord : PBootRecord; + buffer : puint32; + bufferPointer : PDirectory; + dataStart : uint32; + EntriesPerSector : uint32; + sectorLocation : uint32; + dataOffset : uint32; + i : uint32; + + thisArray : byteArray8 = ('.',' ',' ',' ',' ',' ',' ',' '); + parentArray : byteArray8 = ('.','.',' ',' ',' ',' ',' ',' '); + status : puint32; + + namePart : pchar; + extPart : pchar; +begin + push_trace('driver.storage.fs.fat32.writeDirectory.enter'); + io.syslog.logln('FAT32', 'writeDirectory: enter'); + writeDirectory := 0; + status:= puint32(kalloc(sizeof(uint32))); + status^:= ord(eNone); + + { Validate the directory name as a valid FAT32 8.3 name } + if not isValidFAT32Name(dirName) then begin + io.syslog.logln('FAT32', 'writeDirectory: invalid FAT32 name, aborting'); + statusOut^:= ord(eInvalidFileName); + kfree(status); + exit; + end; + + push_trace('driver.storage.fs.fat32.writeDirectory.readDir'); + directories:= readDirectory(volume, directory, status); + + { If the parent directory does not exist, bail out } + if status^ <> ord(eNone) then begin + io.syslog.logln('FAT32', 'writeDirectory: parent directory not found'); + statusOut^:= status^; + kfree(status); + LL_Free(directories); + exit; + end; + + { Split dirName for duplicate checking } + splitFileNameParts(dirName, namePart, extPart); + + if(LL_size(directories) > 1) then begin + for i:=0 to LL_Size(directories) - 1 do begin + if compareByteArray8(PDirectory(LL_get(directories, i))^.fileName, cleanString( namePart , status)) + and matchExtension(PDirectory(LL_get(directories, i))^.fileExtension, extPart) then begin + status^:= ord(eDirectoryAlreadyExists); + io.syslog.logln('FAT32', 'writeDirectory: entry already exists'); + end; + end; + end; + + kfree(void(namePart)); + kfree(void(extPart)); + + bootRecord:= readBootRecord(volume); + datastart:= fatDataStartLBA(volume, bootRecord); + + if status^ = ord(eNone) then begin + push_trace('driver.storage.fs.fat32.writeDirectory.createEntry'); + + { Check if directory sector is full } + EntriesPerSector:= uint32(bootRecord^.sectorSize) div uint32(sizeof(TDirectory)); + if LL_size(directories) >= EntriesPerSector then begin + { Current sector is full — would need cluster chain extension (not yet supported) } + io.syslog.logln('FAT32', 'writeDirectory: directory sector full'); + statusOut^:= ord(eDirectoryFull); + kfree(status); + kfree(puint32(bootRecord)); + LL_Free(directories); + exit; + end; + + parentDirectory:= PDirectory(LL_Get(directories, 0)); + parentCluster:= dirFirstCluster(parentDirectory); + + extents := findFreeClusterExtents(volume, 1, bootRecord); + if extents = nil then begin + { No free clusters available — disk is full } + io.syslog.logln('FAT32', 'writeDirectory: disk full'); + statusOut^:= ord(eDiskFull); + kfree(status); + kfree(puint32(bootRecord)); + LL_Free(directories); + exit; + end; + extent := PClusterExtent(LL_Get(extents, 0)); + cluster := extent^.StartCluster; + io.syslog.logln('FAT32', 'writeDirectory: allocated cluster'); + LL_Free(extents); + buffer:= puint32(kalloc(bootRecord^.sectorSize)); + + if attributes = $10 then begin // if directory + + memset(uint32(buffer), 0, bootRecord^.sectorSize); + + bufferPointer:= @PDirectory(buffer)[0]; + bufferPointer^.fileName:= thisArray; //TODO implement time + bufferPointer^.attributes:= attributes; + setDirFirstCluster(bufferPointer, cluster); + + bufferPointer:= @PDirectory(buffer)[1]; + bufferPointer^.fileName:= parentArray; //TODO implement time + bufferPointer^.attributes:= attributes; + setDirFirstCluster(bufferPointer, parentCluster); + + //write to disk + driver.storage.mgr.storage_write(volume^.device, clusterToLBA(dataStart, bootRecord^.spc, cluster), 1, buffer); + + //write fat + writeFat(volume, cluster, $FFFFFFF8, bootRecord); + end; + memset(uint32(buffer), 0, bootRecord^.sectorSize); + //calculate write cluster using directories and parentCluster + sectorLocation:= LL_size(directories) * sizeof(TDirectory) div bootRecord^.sectorSize; + + //dataOffset:= datastart + ( (LL_size(directories) * sizeof(PDirectory)) - (sizeUsed * bootRecord^.sectorSize)); + driver.storage.mgr.storage_read(volume^.device, + clusterToLBA(dataStart, bootRecord^.spc, parentCluster) + sectorLocation, 1, buffer); + + //construct my dir entry + EntriesPerSector:= uint32(bootRecord^.sectorSize) div uint32(sizeof(TDirectory)); + bufferPointer:= @PDirectory(buffer)[LL_size(directories) - ((LL_size(directories) div EntriesPerSector) * EntriesPerSector)]; + + { Split dirName into name + extension parts for FAT32 8.3 format } + splitFileNameParts(dirName, namePart, extPart); + bufferPointer^.fileName:= cleanString(namePart, status); + bufferPointer^.attributes:= attributes; + if attributes = 0 then begin + { Copy extension characters for files } + fillFatExt(bufferPointer^.fileExtension, extPart); + end else begin + fillFatExt(bufferPointer^.fileExtension, nil); + end; + kfree(void(namePart)); + kfree(void(extPart)); + setDirFirstCluster(bufferPointer, cluster); + + writeDirectory:= cluster; + + push_trace('driver.storage.fs.fat32.writeDirectory.writeToDisk'); + //write to disk + driver.storage.mgr.storage_write(volume^.device, + clusterToLBA(dataStart, bootRecord^.spc, parentCluster) + sectorLocation, 1, buffer); + io.syslog.logln('FAT32', 'writeDirectory: entry written to disk'); + kfree(buffer); + end; + + statusOut^:= status^; + kfree(status); + kfree(puint32(bootRecord)); + LL_Free(directories); + io.syslog.logln('FAT32', 'writeDirectory: exit'); + +end; + +procedure writeDirectoryGen(volume : PStorage_volume; directory : pchar; dirName : pchar; attributes : uint32; statusOut : puint32); // need to handle parent table cluster overflow, need to take attributes +begin + push_trace('driver.storage.fs.fat32.writeDirectoryGen.enter'); + writeDirectory(volume, directory, dirName, attributes, statusOut); + push_trace('driver.storage.fs.fat32.writeDirectoryGen.exit'); +end; + +{ Split a full path like 'SYSTEM/FILE.TXT' into parent dir ('SYSTEM') and name ('FILE.TXT'). + If no separator, parentOut is empty string and nameOut is the whole path. + Caller must free parentOut and nameOut. } +procedure splitPathParts(fullPath : pchar; var parentOut : pchar; var nameOut : pchar); +var + pathLen, lastSlash, i : uint32; +begin + parentOut := nil; + nameOut := nil; + if fullPath = nil then begin + parentOut := stringNew(0); + nameOut := stringNew(0); + exit; + end; + pathLen := stringSize(fullPath); + if pathLen = 0 then begin + parentOut := stringNew(0); + nameOut := stringNew(0); + exit; + end; + + { Find last '/' separator } + lastSlash := pathLen; { sentinel: no slash found } + for i := 0 to pathLen - 1 do begin + if fullPath[i] = '/' then + lastSlash := i; + end; + + if lastSlash < pathLen then begin + parentOut := stringTrim(fullPath, lastSlash); + nameOut := stringCopy(pchar(@fullPath[lastSlash + 1])); + end else begin + parentOut := stringNew(0); + nameOut := stringCopy(fullPath); + end; +end; + +{ Free a FAT chain starting from the given cluster — marks all clusters as free (0). + Includes loop protection: aborts if iteration count exceeds maxCluster or cluster + is out of range (< 2 or >= maxCluster). } +procedure freeFatChain(volume : PStorage_volume; startCluster : uint32; bootRecord : PBootRecord); +var + currentCluster : uint32; + nextCluster : uint32; + maxCluster : uint32; + iter : uint32; +begin + push_trace('driver.storage.fs.fat32.freeFatChain.enter'); + if (bootRecord^.FATSize > 0) and (bootRecord^.sectorsize > 0) then + maxCluster := (bootRecord^.FATSize * bootRecord^.sectorsize) div 4 + else + maxCluster := 0; + currentCluster := startCluster; + iter := 0; + while true do begin + if (currentCluster < 2) or (currentCluster >= maxCluster) then begin + io.syslog.logln('FAT32', 'freeFatChain: cluster out of range, aborting'); + break; + end; + if iter >= maxCluster then begin + io.syslog.logln('FAT32', 'freeFatChain: iteration limit hit, possible cycle'); + break; + end; + nextCluster := readFat(volume, currentCluster, bootRecord); + writeFat(volume, currentCluster, 0, bootRecord); + iter := iter + 1; + + if (nextCluster and $0FFFFFFF) >= $0FFFFFF8 then break; { end of chain } + if (nextCluster and $0FFFFFFF) = $0FFFFFF7 then break; { bad cluster } + if nextCluster = 0 then break; { already free } + + currentCluster := nextCluster; + end; + push_trace('driver.storage.fs.fat32.freeFatChain.exit'); +end; + +{ Resolve a full path into parentCluster + leafName. + Splits the path, reads the parent directory, and returns + the parent cluster. On failure sets statusOut and returns false. + Caller must kfree parentDir and leafName when done. + bootRecord must already be read by the caller. } +function resolveParentCluster( + volume : PStorage_Volume; + path : pchar; + bootRecord : PBootRecord; + var parentDir : pchar; + var leafName : pchar; + var parentClust : uint32; + var statusOut : uint32 +) : boolean; +var + directories : PLinkedListBase; + parentEntry : PDirectory; + status : puint32; +begin + resolveParentCluster := false; + splitPathParts(path, parentDir, leafName); + + if (leafName = nil) or (stringSize(leafName) = 0) then begin + statusOut := ord(eInvalidFileName); + exit; + end; + + if (parentDir = nil) or (stringSize(parentDir) = 0) then begin + parentClust := bootRecord^.rootCluster; + resolveParentCluster := true; + exit; + end; + + status := puint32(kalloc(4)); + status^ := ord(eNone); + directories := readDirectory(volume, parentDir, status); + if status^ <> ord(eNone) then begin + statusOut := status^; + LL_Free(directories); + kfree(status); + exit; + end; + if LL_size(directories) < 1 then begin + statusOut := ord(eDirectoryDoesNotExist); + LL_Free(directories); + kfree(status); + exit; + end; + parentEntry := PDirectory(LL_Get(directories, 0)); + parentClust := dirFirstCluster(parentEntry); + LL_Free(directories); + kfree(status); + resolveParentCluster := true; +end; + +{ Delete a file from the volume. filePath is relative to volume root, e.g. 'SYSTEM/FILE.TXT' or 'FILE.TXT' } +procedure deleteFile(volume : PStorage_Volume; filePath : pchar; statusOut : puint32); +var + parentDir : pchar; + fileName : pchar; + namePart : pchar; + extPart : pchar; + bootRecord : PBootRecord; + parentCluster : uint32; + status : puint32; + buffer : puint32; + dir : PDirectory; + cluster : uint32; + cleanFileName : byteArray8; + loc : TDirEntryLocation; + resolveStatus : uint32; +begin + push_trace('driver.storage.fs.fat32.deleteFile.enter'); + io.syslog.logln('FAT32', 'deleteFile: enter'); + status := puint32(kalloc(4)); + status^ := ord(eNone); + + bootRecord := readBootRecord(volume); + + { Resolve parent directory + leaf name from full path } + resolveStatus := ord(eNone); + if not resolveParentCluster(volume, filePath, bootRecord, parentDir, fileName, parentCluster, resolveStatus) then begin + io.syslog.logln('FAT32', 'deleteFile: resolve failed'); + if statusOut <> nil then statusOut^ := resolveStatus; + kfree(status); + kfree(puint32(bootRecord)); + if parentDir <> nil then kfree(void(parentDir)); + if fileName <> nil then kfree(void(fileName)); + exit; + end; + + { Split filename for 8.3 matching } + splitFileNameParts(fileName, namePart, extPart); + cleanFileName := cleanString(namePart, status); + + { Locate target entry in parent directory } + buffer := puint32(kalloc(bootRecord^.sectorSize)); + if not locateDirEntry(volume, parentCluster, bootRecord, cleanFileName, extPart, loc, buffer) then begin + io.syslog.logln('FAT32', 'deleteFile: file not found'); + if statusOut <> nil then statusOut^ := ord(eFileDoesNotExist); + kfree(buffer); + kfree(void(namePart)); + kfree(void(extPart)); + kfree(status); + kfree(puint32(bootRecord)); + kfree(void(parentDir)); + kfree(void(fileName)); + exit; + end; + + dir := @PDirectory(buffer)[loc.EntryIdx]; + + { Don't allow deleting directories via deleteFile } + if (dir^.attributes and $10) = $10 then begin + io.syslog.logln('FAT32', 'deleteFile: target is a directory, not a file'); + if statusOut <> nil then statusOut^ := ord(eNotADirectory); + kfree(buffer); + kfree(void(namePart)); + kfree(void(extPart)); + kfree(status); + kfree(puint32(bootRecord)); + kfree(void(parentDir)); + kfree(void(fileName)); + exit; + end; + + cluster := dirFirstCluster(dir); + + { Free the FAT chain for this file } + io.syslog.logln('FAT32', 'deleteFile: freeing FAT chain'); + if cluster >= 2 then + freeFatChain(volume, cluster, bootRecord); + + { Mark entry deleted and write back } + dir^.fileName[0] := char($E5); + driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, buffer); + + if statusOut <> nil then statusOut^ := ord(eNone); + + kfree(buffer); + kfree(void(namePart)); + kfree(void(extPart)); + kfree(status); + kfree(puint32(bootRecord)); + kfree(void(parentDir)); + kfree(void(fileName)); + push_trace('driver.storage.fs.fat32.deleteFile.exit'); + io.syslog.logln('FAT32', 'deleteFile: done'); +end; + +{ Delete a directory from the volume. path is relative to volume root. Directory must be empty. } +procedure deleteDir(volume : PStorage_Volume; path : pchar; statusOut : puint32); +var + parentDir : pchar; + dirName : pchar; + namePart : pchar; + extPart : pchar; + bootRecord : PBootRecord; + childDirs : PLinkedListBase; + parentCluster : uint32; + status : puint32; + buffer : puint32; + dir : PDirectory; + cluster : uint32; + childCount : uint32; + cleanFileName : byteArray8; + loc : TDirEntryLocation; + resolveStatus : uint32; +begin + push_trace('driver.storage.fs.fat32.deleteDir.enter'); + io.syslog.logln('FAT32', 'deleteDir: enter'); + status := puint32(kalloc(4)); + status^ := ord(eNone); + + bootRecord := readBootRecord(volume); + + { Resolve parent directory + leaf name from full path } + resolveStatus := ord(eNone); + if not resolveParentCluster(volume, path, bootRecord, parentDir, dirName, parentCluster, resolveStatus) then begin + io.syslog.logln('FAT32', 'deleteDir: resolve failed'); + if statusOut <> nil then statusOut^ := resolveStatus; + kfree(status); + kfree(puint32(bootRecord)); + if parentDir <> nil then kfree(void(parentDir)); + if dirName <> nil then kfree(void(dirName)); + exit; + end; + + { Split dirName for 8.3 matching } + splitFileNameParts(dirName, namePart, extPart); + cleanFileName := cleanString(namePart, status); + + { Locate target entry in parent directory } + buffer := puint32(kalloc(bootRecord^.sectorSize)); + if not locateDirEntry(volume, parentCluster, bootRecord, cleanFileName, extPart, loc, buffer) then begin + io.syslog.logln('FAT32', 'deleteDir: directory not found'); + if statusOut <> nil then statusOut^ := ord(eDirectoryDoesNotExist); + kfree(buffer); + kfree(void(namePart)); + kfree(void(extPart)); + kfree(status); + kfree(puint32(bootRecord)); + kfree(void(parentDir)); + kfree(void(dirName)); + exit; + end; + + dir := @PDirectory(buffer)[loc.EntryIdx]; + + { Must be a directory } + if (dir^.attributes and $10) <> $10 then begin + if statusOut <> nil then statusOut^ := ord(eNotADirectory); + kfree(buffer); + kfree(void(namePart)); + kfree(void(extPart)); + kfree(status); + kfree(puint32(bootRecord)); + kfree(void(parentDir)); + kfree(void(dirName)); + exit; + end; + + cluster := dirFirstCluster(dir); + + { Check if directory is empty — should contain only '.' and '..' } + childDirs := getDirEntries(volume, cluster, bootRecord); + childCount := LL_size(childDirs); + LL_Free(childDirs); + + if childCount > 2 then begin + io.syslog.logln('FAT32', 'deleteDir: directory not empty, refusing to delete'); + if statusOut <> nil then statusOut^ := ord(eDirectoryNotEmpty); + kfree(buffer); + kfree(void(namePart)); + kfree(void(extPart)); + kfree(status); + kfree(puint32(bootRecord)); + kfree(void(parentDir)); + kfree(void(dirName)); + exit; + end; + + { Free the FAT chain for this directory's data } + if cluster >= 2 then + freeFatChain(volume, cluster, bootRecord); + + { Mark entry deleted and write back } + dir^.fileName[0] := char($E5); + driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, buffer); + + if statusOut <> nil then statusOut^ := ord(eNone); + + kfree(buffer); + kfree(void(namePart)); + kfree(void(extPart)); + kfree(status); + kfree(puint32(bootRecord)); + kfree(void(parentDir)); + kfree(void(dirName)); + push_trace('driver.storage.fs.fat32.deleteDir.exit'); + io.syslog.logln('FAT32', 'deleteDir: done'); +end; + +{ Rename a file or directory on disk. filePath is the current relative path, + newName is the new leaf name (e.g. 'NEWFILE.TXT'). Only changes the 8.3 + directory entry in-place — does not move between directories. } +procedure renameFile(volume : PStorage_Volume; filePath : pchar; newName : pchar; statusOut : puint32); +var + parentDir : pchar; + fileName : pchar; + namePart : pchar; + extPart : pchar; + newNamePart : pchar; + newExtPart : pchar; + bootRecord : PBootRecord; + parentCluster : uint32; + j : uint32; + status : puint32; + buffer : puint32; + dir : PDirectory; + cleanName : byteArray8; + newCleanName : byteArray8; + loc : TDirEntryLocation; + resolveStatus : uint32; +begin + push_trace('driver.storage.fs.fat32.renameFile.enter'); + io.syslog.logln('FAT32', 'renameFile: enter'); + status := puint32(kalloc(4)); + status^ := ord(eNone); + + bootRecord := readBootRecord(volume); + + { Validate new name before doing any work } + if (newName = nil) or (stringSize(newName) = 0) then begin + io.syslog.logln('FAT32', 'renameFile: invalid new name'); + if statusOut <> nil then statusOut^ := ord(eInvalidFileName); + kfree(status); + kfree(puint32(bootRecord)); + exit; + end; + + { Resolve parent directory + leaf name from full path } + resolveStatus := ord(eNone); + if not resolveParentCluster(volume, filePath, bootRecord, parentDir, fileName, parentCluster, resolveStatus) then begin + io.syslog.logln('FAT32', 'renameFile: resolve failed'); + if statusOut <> nil then statusOut^ := resolveStatus; + kfree(status); + kfree(puint32(bootRecord)); + if parentDir <> nil then kfree(void(parentDir)); + if fileName <> nil then kfree(void(fileName)); + exit; + end; + + { Split current filename for 8.3 matching } + splitFileNameParts(fileName, namePart, extPart); + cleanName := cleanString(namePart, status); + + { Locate target entry in parent directory } + buffer := puint32(kalloc(bootRecord^.sectorSize)); + if not locateDirEntry(volume, parentCluster, bootRecord, cleanName, extPart, loc, buffer) then begin + io.syslog.logln('FAT32', 'renameFile: file not found'); + if statusOut <> nil then statusOut^ := ord(eFileDoesNotExist); + kfree(buffer); + kfree(void(namePart)); + kfree(void(extPart)); + kfree(status); + kfree(puint32(bootRecord)); + kfree(void(parentDir)); + kfree(void(fileName)); + exit; + end; + + dir := @PDirectory(buffer)[loc.EntryIdx]; + + { Apply new 8.3 name in-place } + splitFileNameParts(newName, newNamePart, newExtPart); + newCleanName := cleanString(newNamePart, status); + for j := 0 to 7 do + dir^.fileName[j] := newCleanName[j]; + fillFatExt(dir^.fileExtension, newExtPart); + kfree(void(newNamePart)); + kfree(void(newExtPart)); + + { Write back this one sector } + driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, buffer); + + if statusOut <> nil then statusOut^ := ord(eNone); + + kfree(buffer); + kfree(void(namePart)); + kfree(void(extPart)); + kfree(status); + kfree(puint32(bootRecord)); + kfree(void(parentDir)); + kfree(void(fileName)); + push_trace('driver.storage.fs.fat32.renameFile.exit'); + io.syslog.logln('FAT32', 'renameFile: done'); +end; + +// function checkExists(volume : PStorage_Volume; directory : pchar; fileName : pchar; fileExtension : pchar; entry : PDirectory_Entry) : uint32; +// var +// bootRecord : PBootRecord; +// directories : PLinkedListBase; +// dir : PDirectory; +// genDir : PDirectory_Entry; +// data : puint32; +// statusOut : puint32; +// i : uint32; +// exists : boolean = false; +// begin +// bootRecord := readBootRecord(volume); +// directories := readDirectory(volume, directory, statusOut); +// datastart:= volume^.sectorStart + 1 + bootRecord^.FATSize + bootRecord^.rsvSectors; + +// for i:=0 to LL_Size(directories) -1 do begin +// dir:= PDirectory(LL_Get(directories, i)); +// if (dir^.fileName = entry^.fileName) and (dir^.fileExtension = entry^.extension) then begin +// exists:= true; +// break; +// end; +// end; + +// PDirectory_Entry := PDirectory_Entry(kalloc(20)); + +// PDirectory_Entry^.fileName := pchar(@dir^.fileName); +// PDirectory_Entry^.extension := pchar(@dir^.fileExtension); + +// end; + +//TODO check directory commands for errors with a clean disk + +{ ========================================================================== } +{ Async format state machine (create_volume_async) } +{ ========================================================================== } + +procedure fmt_step_complete(error : TError; userdata : pointer); forward; + +procedure fmt_run_next(ctx : PFmtContext); +var + writeCount : uint32; +begin + case ctx^.Step of + fmtBootSector: begin + io.syslog.logln('FAT32', 'fmt: fmtBootSector'); + { Write boot sector at partition start } + driver.storage.mgr.storage_write_async(ctx^.Disk, ctx^.SectorStart, 1, + ctx^.Buffer, @fmt_step_complete, pointer(ctx)); + end; + fmtZeroFAT: begin + io.syslog.logln('FAT32', 'fmt: fmtZeroFAT batch'); + { Issue next FAT zero batch } + if ctx^.BatchPos >= ctx^.FATSize then begin + io.syslog.logln('FAT32', 'fmt: FAT zero complete, moving to fmtFATEntries'); + { FAT zeroing complete — move to FAT entries } + kfree(ctx^.ZeroBuffer); + ctx^.ZeroBuffer := nil; + kfree(ctx^.Buffer); + ctx^.Buffer := puint32(kalloc(ctx^.Disk^.sectorSize)); + memset(uint32(ctx^.Buffer), 0, ctx^.Disk^.sectorSize); + puint32(ctx^.Buffer)[0] := $0FFFFFF8; { media type (reserved entry 0) } + puint32(ctx^.Buffer)[1] := $0FFFFFFF; { clean marker (reserved entry 1) } + puint32(ctx^.Buffer)[2] := $0FFFFFF8; { root cluster EOC (entry 2) } + puint32(ctx^.Buffer)[3] := $0FFFFFF8; { SYSTEM dir cluster EOC (entry 3) } + ctx^.Step := fmtFATEntries; + fmt_run_next(ctx); + exit; + end; + if (ctx^.FATSize - ctx^.BatchPos) >= ctx^.BatchSize then + writeCount := ctx^.BatchSize + else + writeCount := ctx^.FATSize - ctx^.BatchPos; + driver.storage.mgr.storage_write_async(ctx^.Disk, + ctx^.FATStart + ctx^.BatchPos, writeCount, ctx^.ZeroBuffer, + @fmt_step_complete, pointer(ctx)); + end; + fmtFATEntries: begin + io.syslog.logln('FAT32', 'fmt: fmtFATEntries'); + { Write FAT entries sector } + driver.storage.mgr.storage_write_async(ctx^.Disk, ctx^.FATStart, 1, + ctx^.Buffer, @fmt_step_complete, pointer(ctx)); + end; + fmtRootDir: begin + io.syslog.logln('FAT32', 'fmt: fmtRootDir'); + { Build root directory: "." + ".." + "SYSTEM" entries } + memset(uint32(ctx^.Buffer), 0, ctx^.Disk^.sectorSize); + { "." entry — volume label } + PDirectory(ctx^.Buffer)[0].fileName[0] := '.'; + PDirectory(ctx^.Buffer)[0].fileName[1] := ' '; + PDirectory(ctx^.Buffer)[0].fileName[2] := ' '; + PDirectory(ctx^.Buffer)[0].fileName[3] := ' '; + PDirectory(ctx^.Buffer)[0].fileName[4] := ' '; + PDirectory(ctx^.Buffer)[0].fileName[5] := ' '; + PDirectory(ctx^.Buffer)[0].fileName[6] := ' '; + PDirectory(ctx^.Buffer)[0].fileName[7] := ' '; + PDirectory(ctx^.Buffer)[0].attributes := $08; + PDirectory(ctx^.Buffer)[0].clusterLow := ctx^.RootCluster; + { ".." entry } + PDirectory(ctx^.Buffer)[1].fileName[0] := '.'; + PDirectory(ctx^.Buffer)[1].fileName[1] := '.'; + PDirectory(ctx^.Buffer)[1].fileName[2] := ' '; + PDirectory(ctx^.Buffer)[1].fileName[3] := ' '; + PDirectory(ctx^.Buffer)[1].fileName[4] := ' '; + PDirectory(ctx^.Buffer)[1].fileName[5] := ' '; + PDirectory(ctx^.Buffer)[1].fileName[6] := ' '; + PDirectory(ctx^.Buffer)[1].fileName[7] := ' '; + PDirectory(ctx^.Buffer)[1].attributes := $10; + PDirectory(ctx^.Buffer)[1].clusterLow := ctx^.RootCluster; + { "SYSTEM" directory entry pointing to cluster 3 } + PDirectory(ctx^.Buffer)[2].fileName[0] := 'S'; + PDirectory(ctx^.Buffer)[2].fileName[1] := 'Y'; + PDirectory(ctx^.Buffer)[2].fileName[2] := 'S'; + PDirectory(ctx^.Buffer)[2].fileName[3] := 'T'; + PDirectory(ctx^.Buffer)[2].fileName[4] := 'E'; + PDirectory(ctx^.Buffer)[2].fileName[5] := 'M'; + PDirectory(ctx^.Buffer)[2].fileName[6] := ' '; + PDirectory(ctx^.Buffer)[2].fileName[7] := ' '; + PDirectory(ctx^.Buffer)[2].attributes := $10; + PDirectory(ctx^.Buffer)[2].clusterLow := 3; + driver.storage.mgr.storage_write_async(ctx^.Disk, + clusterToLBA(ctx^.DataStart, ctx^.SPC, ctx^.RootCluster), 1, ctx^.Buffer, + @fmt_step_complete, pointer(ctx)); + end; + fmtSystemDir: begin + io.syslog.logln('FAT32', 'fmt: fmtSystemDir'); + { Build SYSTEM directory: "." and ".." } + memset(uint32(ctx^.Buffer), 0, ctx^.Disk^.sectorSize); + PDirectory(ctx^.Buffer)[0].fileName[0] := '.'; + PDirectory(ctx^.Buffer)[0].fileName[1] := ' '; + PDirectory(ctx^.Buffer)[0].fileName[2] := ' '; + PDirectory(ctx^.Buffer)[0].fileName[3] := ' '; + PDirectory(ctx^.Buffer)[0].fileName[4] := ' '; + PDirectory(ctx^.Buffer)[0].fileName[5] := ' '; + PDirectory(ctx^.Buffer)[0].fileName[6] := ' '; + PDirectory(ctx^.Buffer)[0].fileName[7] := ' '; + PDirectory(ctx^.Buffer)[0].attributes := $10; + PDirectory(ctx^.Buffer)[0].clusterLow := 3; + PDirectory(ctx^.Buffer)[1].fileName[0] := '.'; + PDirectory(ctx^.Buffer)[1].fileName[1] := '.'; + PDirectory(ctx^.Buffer)[1].fileName[2] := ' '; + PDirectory(ctx^.Buffer)[1].fileName[3] := ' '; + PDirectory(ctx^.Buffer)[1].fileName[4] := ' '; + PDirectory(ctx^.Buffer)[1].fileName[5] := ' '; + PDirectory(ctx^.Buffer)[1].fileName[6] := ' '; + PDirectory(ctx^.Buffer)[1].fileName[7] := ' '; + PDirectory(ctx^.Buffer)[1].attributes := $10; + PDirectory(ctx^.Buffer)[1].clusterLow := ctx^.RootCluster; + driver.storage.mgr.storage_write_async(ctx^.Disk, + clusterToLBA(ctx^.DataStart, ctx^.SPC, 3), 1, ctx^.Buffer, + @fmt_step_complete, pointer(ctx)); + end; + fmtDone: begin + io.syslog.logln('FAT32', 'fmt: fmtDone — format complete'); + { All done — clean up and notify caller } + kfree(ctx^.Buffer); + ctx^.Buffer := nil; + if ctx^.Callback <> nil then + ctx^.Callback(eNone, ctx^.CallbackData); + kfree(puint32(ctx)); + end; + end; +end; + +procedure fmt_step_complete(error : TError; userdata : pointer); +var + ctx : PFmtContext; +begin + ctx := PFmtContext(userdata); + if error <> eNone then begin + io.syslog.logln('FAT32', 'fmt_step_complete: I/O error — aborting format'); + { Error — abort format, clean up, notify caller } + if ctx^.Buffer <> nil then kfree(ctx^.Buffer); + if ctx^.ZeroBuffer <> nil then kfree(ctx^.ZeroBuffer); + if ctx^.Callback <> nil then + ctx^.Callback(error, ctx^.CallbackData); + kfree(puint32(ctx)); + exit; + end; + + { Advance to next step } + case ctx^.Step of + fmtBootSector: begin + ctx^.Step := fmtZeroFAT; + ctx^.BatchPos := 0; + end; + fmtZeroFAT: begin + ctx^.BatchPos := ctx^.BatchPos + ctx^.BatchSize; + { Step stays fmtZeroFAT — fmt_run_next checks if more batches remain } + end; + fmtFATEntries: begin + ctx^.Step := fmtRootDir; + end; + fmtRootDir: begin + ctx^.Step := fmtSystemDir; + end; + fmtSystemDir: begin + ctx^.Step := fmtDone; + end; + end; + fmt_run_next(ctx); +end; + +procedure create_volume_async(volume : PStorage_Volume; sectors : uint32; start : uint32; + config : puint32; callback : TIOCallback; callbackData : pointer); +var + ctx : PFmtContext; + bootRecord : PBootRecord; + spc : uint32; +begin + push_trace('driver.storage.fs.fat32.create_volume_async'); + io.syslog.logln('FAT32', 'create_volume_async: enter'); + + ctx := PFmtContext(kalloc(sizeof(TFmtContext))); + if ctx = nil then begin + io.syslog.logln('FAT32', 'create_volume_async: OOM allocating context'); + if callback <> nil then callback(eOutOfMemory, callbackData); + exit; + end; + + ctx^.Volume := volume; + ctx^.Disk := volume^.device; + ctx^.SectorStart := start; + ctx^.RootCluster := 2; + ctx^.Callback := callback; + ctx^.CallbackData := callbackData; + ctx^.ZeroBuffer := nil; + + if config <> nil then + spc := config^ + else + spc := fatDefaultSPC(sectors, ctx^.Disk^.sectorSize); + if spc = 0 then + spc := fatDefaultSPC(sectors, ctx^.Disk^.sectorSize); + ctx^.SPC := spc; + + ctx^.FATSize := fatComputeFATSize(sectors, spc, ctx^.Disk^.sectorSize); + ctx^.FATStart := start + 32; { boot sector included in reserved sectors } + ctx^.DataStart := ctx^.FATStart + ctx^.FATSize; + + { Allocate boot sector buffer and build boot record } + ctx^.Buffer := puint32(kalloc(ctx^.Disk^.sectorSize + 512)); + if ctx^.Buffer = nil then begin + if callback <> nil then callback(eOutOfMemory, callbackData); + kfree(puint32(ctx)); + exit; + end; + memset(uint32(ctx^.Buffer), 0, ctx^.Disk^.sectorSize); + + bootRecord := PBootRecord(ctx^.Buffer); + bootRecord^.jmp2boot := $0; + bootRecord^.OEMName[0] := 'A'; + bootRecord^.OEMName[1] := 'S'; + bootRecord^.OEMName[2] := 'U'; + bootRecord^.OEMName[3] := 'R'; + bootRecord^.OEMName[4] := 'O'; + bootRecord^.OEMName[5] := ' '; + bootRecord^.OEMName[6] := 'V'; + bootRecord^.OEMName[7] := '1'; + bootRecord^.sectorSize := ctx^.Disk^.sectorSize; + bootRecord^.spc := uint8(spc); + bootRecord^.rsvSectors := 32; + bootRecord^.numFats := 1; + bootRecord^.mediaDescp := $F8; + bootRecord^.hiddenSectors := start; + bootRecord^.manySectors := sectors; + bootRecord^.FATSize := ctx^.FATSize; + bootRecord^.rootCluster := ctx^.RootCluster; + bootRecord^.FSInfoCluster := 0; + bootRecord^.driveNumber := $80; + bootRecord^.volumeID := 62; + bootRecord^.bsignature := $29; + bootRecord^.identString[0] := 'F'; + bootRecord^.identString[1] := 'A'; + bootRecord^.identString[2] := 'T'; + bootRecord^.identString[3] := '3'; + bootRecord^.identString[4] := '2'; + bootRecord^.identString[5] := ' '; + bootRecord^.identString[6] := ' '; + bootRecord^.identString[7] := ' '; + + { Boot sector signature at bytes 510-511 } + puint8(uint32(ctx^.Buffer) + 510)^ := $55; + puint8(uint32(ctx^.Buffer) + 511)^ := $AA; + + { Allocate zero buffer for FAT batches } + if ctx^.FATSize > 128 then + ctx^.BatchSize := 128 + else + ctx^.BatchSize := ctx^.FATSize; + ctx^.BatchPos := 0; + ctx^.ZeroBuffer := puint32(kalloc(ctx^.Disk^.sectorSize * ctx^.BatchSize)); + if ctx^.ZeroBuffer = nil then begin + kfree(ctx^.Buffer); + if callback <> nil then callback(eOutOfMemory, callbackData); + kfree(puint32(ctx)); + exit; + end; + memset(uint32(ctx^.ZeroBuffer), 0, ctx^.Disk^.sectorSize * ctx^.BatchSize); + + { Kick off the first step: write boot sector } + io.syslog.logln('FAT32', 'create_volume_async: kicking off fmtBootSector'); + ctx^.Step := fmtBootSector; + fmt_run_next(ctx); +end; + +{ ========================================================================== } +{ Synchronous create_volume (task context only) } +{ ========================================================================== } + +procedure create_volume(volume : PStorage_Volume; sectors : uint32; start : uint32; config : puint32); +var + buffer : puint32; + zeroBuffer : puint32; + bootRecord : PBootRecord; + dataStart : uint32; + fatStart : uint32; + FATSize : uint32; + batchSize : uint32; + batchPos : uint32; + + asuroArray : byteArray8 = ('A','S','U','R','O',' ','V','1'); + fatArray : byteArray8 = ('F','A','T','3','2',' ',' ',' '); + thisArray : byteArray8 = ('.',' ',' ',' ',' ',' ',' ',' '); + parentArray : byteArray8 = ('.','.',' ',' ',' ',' ',' ',' '); + + asuroFileArray : byteArray8 = ('A','S','U','R','O',' ',' ',' '); + mountFileArray : byteArray8 = ('M','O','U','N','T',' ',' ',' '); + programFileArray : byteArray8 = ('P','R','O','G','R','A','M','S'); + rootCluster : uint32 = 2; + + sysArray : byteArray8 = ('S','Y','S','T','E','M',' ',' '); + progArray : byteArray8 = ('P','R','O','G','R','A','M','S'); + userArray : byteArray8 = ('U','S','E','R',' ',' ',' ',' '); + + status : puint32; + disk : PStorage_device; + spc : uint32; + +begin + push_trace('driver.storage.fs.fat32.create_volume()'); + io.syslog.logln('FAT32', 'create_volume (sync): enter'); + + disk := volume^.device; + + { Default sectors-per-cluster to 1 if config is nil } + if config <> nil then + spc := config^ + else + spc := fatDefaultSPC(sectors, disk^.sectorSize); + if spc = 0 then + spc := fatDefaultSPC(sectors, disk^.sectorSize); + + //driver.storage.fs.fat32 structure + (* BootRecord *) + (* reserved sectors *) + (* File Allocation Table *) + (* Data Area *) + + buffer:= puint32(kalloc(disk^.sectorSize+512)); + memset(uint32(buffer), 0, disk^.sectorSize); + + bootRecord:= PBootRecord(buffer); + + FATSize:= fatComputeFATSize(sectors, spc, disk^.sectorsize); + + bootRecord^.jmp2boot := $0; //TODO impliment boot jump + bootRecord^.OEMName := asuroArray; + bootRecord^.sectorSize := disk^.sectorsize; + bootRecord^.spc := uint8(spc); + bootRecord^.rsvSectors := 32; //32 is standard + bootRecord^.numFats := 1; + bootRecord^.mediaDescp := $F8; + bootRecord^.hiddenSectors := start; + bootRecord^.manySectors := sectors; + bootRecord^.FATSize := FATSize; + bootRecord^.rootCluster := rootCluster; + bootRecord^.FSInfoCluster := 0; + bootRecord^.driveNumber := $80; + bootRecord^.volumeID := 62; //+ puint32(@driver.timer.rtc.getDateTime())^; + bootRecord^.bsignature := $29; + bootRecord^.identString := fatArray; + + + { Write the boot sector signature marker at bytes 510-511 } + puint8(uint32(buffer) + 510)^ := $55; + puint8(uint32(buffer) + 511)^ := $AA; + + driver.storage.mgr.storage_write(disk, start, 1, puint32(buffer)); + io.syslog.logln('FAT32', 'create_volume (sync): boot sector written'); + + fatStart:= start + bootRecord^.rsvSectors; + dataStart:= fatStart + bootRecord^.FATSize; + + { Batch zero the FAT: write 128 sectors at a time instead of 1 } + if FATSize > 128 then + batchSize := 128 + else + batchSize := FATSize; + zeroBuffer:= puint32(kalloc( disk^.sectorSize * batchSize )); + memset(uint32(zeroBuffer), 0, disk^.sectorSize * batchSize); + + batchPos := 0; + while batchPos < FATSize do begin + if (FATSize - batchPos) >= batchSize then + driver.storage.mgr.storage_write(disk, fatStart + batchPos, batchSize, zeroBuffer) + else + driver.storage.mgr.storage_write(disk, fatStart + batchPos, FATSize - batchPos, zeroBuffer); + batchPos += batchSize; + end; + + kfree(buffer); + kfree(zeroBuffer); + io.syslog.logln('FAT32', 'create_volume (sync): FAT zeroed'); + + buffer:= puint32(kalloc(disk^.sectorSize)); + memset(uint32(buffer), 0, disk^.sectorSize); + + puint32(buffer)[0]:= $0FFFFFF8; //media type marker (reserved entry 0) + puint32(buffer)[1]:= $0FFFFFFF; //clean/dirty marker (reserved entry 1) + puint32(buffer)[2]:= $0FFFFFF8; //root cluster end-of-chain (entry 2) + + driver.storage.mgr.storage_write(disk, fatStart, 1, buffer); + io.syslog.logln('FAT32', 'create_volume (sync): FAT entries written'); + + kfree(buffer); + + buffer:= puint32(kalloc(disk^.sectorsize)); + memset(uint32(buffer), 0, disk^.sectorsize); + + PDirectory(buffer)[0].fileName := thisArray; + PDirectory(buffer)[0].attributes := $08; + PDirectory(buffer)[0].clusterLow := rootCluster; + + PDirectory(buffer)[1].fileName := parentArray; + PDirectory(buffer)[1].attributes := $10; + PDirectory(buffer)[1].clusterLow := rootCluster; + + driver.storage.mgr.storage_write(disk, clusterToLBA(dataStart, spc, rootCluster), 1, buffer); + io.syslog.logln('FAT32', 'create_volume (sync): root dir written'); + + memset(uint32(buffer), 0, disk^.sectorsize); + + status := puint32(kalloc(sizeof(uint32))); + writeDirectory(volume, '', 'SYSTEM', $10, status); + io.syslog.logln('FAT32', 'create_volume (sync): SYSTEM dir created'); + kfree(status); + + kfree(buffer); + io.syslog.logln('FAT32', 'create_volume (sync): done'); + + + +end; + +{ Count free clusters by scanning the FAT sector by sector. + Returns the number of free clusters (FAT entries == 0). } +function countFreeFATClusters(volume : PStorage_Volume; bootRecord : PBootRecord) : uint32; +var + fatStart : uint32; + maxCluster : uint32; + entriesPerSect : uint32; + sectorIdx : uint32; + entryIdx : uint32; + clusterNum : uint32; + freeCount : uint32; + fatSectors : uint32; + fatBuffer : puint32; +begin + countFreeFATClusters := 0; + if (bootRecord^.sectorsize = 0) or (bootRecord^.FATSize = 0) then exit; + + fatStart := volume^.sectorStart + bootRecord^.rsvSectors; + maxCluster := (bootRecord^.FATSize * bootRecord^.sectorsize) div 4; + entriesPerSect := bootRecord^.sectorsize div 4; + fatSectors := bootRecord^.FATSize; + + fatBuffer := puint32(kalloc(bootRecord^.sectorsize)); + freeCount := 0; + + for sectorIdx := 0 to fatSectors - 1 do begin + if sectorIdx * entriesPerSect >= maxCluster then break; + driver.storage.mgr.storage_read(volume^.device, fatStart + sectorIdx, 1, fatBuffer); + for entryIdx := 0 to entriesPerSect - 1 do begin + clusterNum := sectorIdx * entriesPerSect + entryIdx; + if clusterNum < 2 then continue; + if clusterNum >= maxCluster then break; + if fatBuffer[entryIdx] = 0 then + freeCount := freeCount + 1; + end; + end; + + kfree(fatBuffer); + countFreeFATClusters := freeCount; +end; + +function identify_volume(volume : PStorage_Volume) : boolean; +var + buffer : puint32; + bootRecord : PBootRecord; + bufSize : uint32; + fatNameOk : boolean; + spc : uint8; + i : uint32; +begin + push_trace('driver.storage.fs.fat32.identify_volume'); + io.syslog.logln('FAT32', 'identify_volume: enter'); + identify_volume := false; + if volume^.device = nil then exit; + if volume^.device^.dispatchRead = nil then exit; + + bufSize := volume^.device^.sectorSize; + if bufSize < 512 then bufSize := 512; + buffer := puint32(kalloc(bufSize)); + memset(uint32(buffer), 0, bufSize); + + driver.storage.mgr.storage_read(volume^.device, volume^.sectorStart, 1, buffer); + bootRecord := PBootRecord(buffer); + + fatNameOk := true; + for i := 0 to 4 do begin + if bootRecord^.identString[i] <> 'FAT32'[i + 1] then begin + fatNameOk := false; + break; + end; + end; + + spc := bootRecord^.spc; + + if bootSectorHasSignature(buffer) and + (bootRecord^.bsignature = $29) and + fatNameOk and + (bootRecord^.sectorSize = volume^.device^.sectorSize) and + (bootRecord^.sectorSize >= 512) and + (spc <> 0) and + ((spc and (spc - 1)) = 0) and + (bootRecord^.rsvSectors > 0) and + (bootRecord^.FATSize > 0) and + (bootRecord^.numFats > 0) and + (bootRecord^.rootCluster >= 2) then begin + io.syslog.logln('FAT32', 'identify_volume: FAT32 signature matched'); + identify_volume := true; + volume^.freeSectors := countFreeFATClusters(volume, bootRecord) * bootRecord^.spc; + end else begin + io.syslog.logln('FAT32', 'identify_volume: not a FAT32 volume'); + end; + + kfree(buffer); +end; + +procedure detect_volumes(disk : PStorage_Device); +var + buffer : puint32; + bufSize : uint32; + i : uint8; + volume : PStorage_volume; +begin + push_trace('driver.storage.fs.fat32.detectVolumes()'); + + bufSize := disk^.sectorSize; + if bufSize < 512 then bufSize := 512; + buffer := puint32(kalloc(bufSize)); + memset(uint32(buffer), 0, bufSize); + + { Read from sector 0 to check for FAT32 boot record } + if disk^.dispatchRead = nil then begin + io.syslog.writestringln('FAT32: detect_volumes: device has no read dispatch.'); + kfree(buffer); + exit; + end; + + driver.storage.mgr.storage_read(disk, 0, 1, buffer); + + if bootSectorHasSignature(buffer) and (PBootRecord(buffer)^.bsignature = $29) then begin + io.syslog.writestringln('FAT32: volume found!'); + volume := PStorage_volume(kalloc(sizeof(TStorage_Volume))); + memset(uint32(volume), 0, sizeof(TStorage_Volume)); + volume^.device := disk; + volume^.sectorStart := 0; + volume^.sectorSize := PBootRecord(buffer)^.sectorSize; + volume^.sectorCount := disk^.maxSectorCount; + volume^.filesystem := @filesystem; + volume^.freeSectors := countFreeFATClusters(volume, PBootRecord(buffer)) * PBootRecord(buffer)^.spc; + volume^.isBootDrive := false; + + driver.storage.vol.mgr.register_volume(disk, volume); + end; + + kfree(buffer); +end; + +{ ---- Per-file open / close for VFS metadata caching ---- } + +{ Called by VFS on OpenFile. Reads the boot record (cached per-volume), + scans the directory for the file, and returns a TFATOpenFile context + that writeFileAtOffset / readFileAtOffset can reuse on every call. + Also populates fileSize so the VFS can skip calling fileSizeCallback. } +function fat32OpenFile(volume : PStorage_Volume; directory : pchar; + fileName : pchar; var fileSize : uint32) : pointer; +var + ofi : PFATOpenFile; + bootRecord : PBootRecord; + dirs : PLinkedListBase; + status : puint32; + tempdir : PDirectory; + i : uint32; + cleanFileName : byteArray8; + otherCFN : byteArray8; + namePart : pchar; + extPart : pchar; +begin + push_trace('driver.storage.fs.fat32.openFile.enter'); + fileSize := 0; + + bootRecord := getCachedBootRecord(volume); + + ofi := PFATOpenFile(kalloc(SizeOf(TFATOpenFile))); + memset(uint32(ofi), 0, SizeOf(TFATOpenFile)); + ofi^.Volume := volume; + ofi^.BootRecord := bootRecord; + ofi^.DataStart := fatDataStartLBA(volume, bootRecord); + ofi^.ScratchSize := bootRecord^.sectorSize; + ofi^.ScratchSector := puint32(kalloc(ofi^.ScratchSize)); + if ofi^.ScratchSector <> nil then + memset(uint32(ofi^.ScratchSector), 0, ofi^.ScratchSize); + ofi^.Exists := false; + + { Read directory entries } + status := puint32(kalloc(sizeof(uint32))); + status^ := 0; + dirs := readDirectory(volume, directory, status); + + { Determine parent cluster (Lesson 18: root has no '.' entry) } + if (directory = nil) or (stringSize(directory) = 0) then + ofi^.DirCluster := bootRecord^.rootCluster + else begin + if (dirs <> nil) and (LL_size(dirs) > 0) then begin + tempdir := PDirectory(LL_get(dirs, 0)); + if tempdir^.fileName[0] = '.' then + ofi^.DirCluster := dirFirstCluster(tempdir) + else + ofi^.DirCluster := bootRecord^.rootCluster; + end else + ofi^.DirCluster := bootRecord^.rootCluster; + end; + + { Parse file name and compute cleaned 8.3 name } + splitFileNameParts(fileName, namePart, extPart); + cleanFileName := cleanString(namePart, status); + ofi^.CleanName := cleanFileName; + ofi^.ExtPart := stringCopy(extPart); + + { Search for file in directory entries } + if (dirs <> nil) and (LL_size(dirs) > 0) then begin + for i := 0 to LL_Size(dirs) - 1 do begin + tempdir := PDirectory(LL_get(dirs, i)); + otherCFN := cleanString(tempdir^.filename, status); + if compareByteArray8(cleanFileName, otherCFN) and matchExtension(tempdir^.fileExtension, extPart) then begin + ofi^.FirstCluster := dirFirstCluster(tempdir); + ofi^.ByteSize := tempdir^.byteSize; + ofi^.Exists := true; + end; + end; + end; + + ofi^.AllocClusters := fatClustersForBytes(ofi^.ByteSize, + uint32(bootRecord^.spc) * uint32(bootRecord^.sectorSize)); + if ofi^.Exists and (ofi^.FirstCluster >= 2) and (ofi^.ByteSize > 0) then + ofi^.Extents := fatBuildExtentList(volume, ofi^.FirstCluster, bootRecord); + + kfree(void(namePart)); + kfree(void(extPart)); + if dirs <> nil then LL_Free(dirs); + kfree(puint32(status)); + + fileSize := ofi^.ByteSize; + fat32OpenFile := pointer(ofi); + push_trace('driver.storage.fs.fat32.openFile.exit'); +end; + +{ Called by VFS on CloseFile to free the per-file context. } +procedure fat32CloseFile(ctx : pointer); +var + ofi : PFATOpenFile; + fatCache : PFATCache; + dirBuf : puint32; + rawDir : PDirectory; + loc : TDirEntryLocation; +begin + if ctx = nil then exit; + ofi := PFATOpenFile(ctx); + if ofi^.FatDirty and (ofi^.Volume <> nil) then begin + fatCache := fatCacheGet(ofi^.Volume); + if fatCache <> nil then + fatCacheFlush(fatCache, ofi^.Volume); + end; + if ofi^.MetaDirty and (ofi^.Volume <> nil) and (ofi^.BootRecord <> nil) then begin + dirBuf := puint32(kalloc(ofi^.BootRecord^.sectorSize)); + if (dirBuf <> nil) and locateDirEntry(ofi^.Volume, ofi^.DirCluster, ofi^.BootRecord, + ofi^.CleanName, ofi^.ExtPart, loc, dirBuf) then begin + rawDir := @PDirectory(dirBuf)[loc.EntryIdx]; + rawDir^.byteSize := ofi^.ByteSize; + if ofi^.FirstCluster <> 0 then + setDirFirstCluster(rawDir, ofi^.FirstCluster); + driver.storage.mgr.storage_write(ofi^.Volume^.device, loc.SectorLBA, 1, dirBuf); + end; + if dirBuf <> nil then + kfree(dirBuf); + end; + if ofi^.ExtPart <> nil then + kfree(void(ofi^.ExtPart)); + if ofi^.Extents <> nil then + LL_Free(ofi^.Extents); + if ofi^.ScratchSector <> nil then + kfree(ofi^.ScratchSector); + kfree(puint32(ofi)); +end; + +{ Write byteCount bytes to a file beginning at byte offset. + If offset + byteCount exceeds the current file size, the file is extended + by allocating new clusters. Returns the number of bytes actually written. } +function writeFileAtOffset(volume : PStorage_Volume; directory : pchar; + fileName : pchar; offset : uint32; + buffer : puint32; byteCount : uint32; + ctx : pointer) : uint32; +const + MAX_RUN_SECTORS = 2048; { cap per multi-sector write = 1MB } +var + lookup : TFileLookup; + run : TRunInfo; + ofi : PFATOpenFile; + i : uint32; + cluster : uint32; + dataStart : uint32; + cleanFileName : byteArray8; + otherCFN : byteArray8; + tempdir : PDirectory; + namePart : pchar; + extPart : pchar; + ioBuf : puint32; + bytesPerCluster : uint32; + startClusterIdx : uint32; + inClusterOffset : uint32; + chainPos : uint32; + curCluster : uint32; + srcPos : uint32; + remaining : uint32; + fileSize : uint32; + origByteSize : uint32; + newEnd : uint32; + oldClusters : uint32; + needClusters : uint32; + targetClusters : uint32; + extraClusters : uint32; + newExtents : PLinkedListBase; + lastCluster : uint32; + nextFat : uint32; + extent : PClusterExtent; + { dir entry update — raw sector scan (Lesson 18) } + dirCluster : uint32; + dirBuf : puint32; + rawDir : PDirectory; + loc : TDirEntryLocation; + { Multi-sector write variables } + spc : uint32; + secSize : uint32; + skipBytes : uint32; + copyBytes : uint32; + firstSectorOff : uint32; + partialBytes : uint32; + fullSectors : uint32; + batchBytes : uint32; + sectorLBA : uint32; + fatCache : PFATCache; + chainWalkLimit : uint32; + firstNewCluster : uint32; + reserveClusters : uint32; +begin + push_trace('driver.storage.fs.fat32.writeFileAtOffset.enter'); + writeFileAtOffset := 0; + ofi := PFATOpenFile(ctx); + + if ofi <> nil then begin + { Fast path: use cached open-file metadata } + lookup.BootRecord := ofi^.BootRecord; + lookup.Dirs := nil; + lookup.Dir := nil; + lookup.Status := nil; + lookup.Exists := ofi^.Exists; + dataStart := ofi^.DataStart; + dirCluster := ofi^.DirCluster; + cleanFileName := ofi^.CleanName; + extPart := ofi^.ExtPart; + namePart := nil; + if lookup.Exists then begin + origByteSize := ofi^.ByteSize; + cluster := ofi^.FirstCluster; + end else begin + origByteSize := 0; + cluster := 0; + end; + end else begin + { Slow path: rediscover everything from disk } + lookup.Status := puint32(kalloc(sizeof(uint32))); + lookup.Status^ := 0; + lookup.BootRecord := readBootRecord(volume); + lookup.Dirs := readDirectory(volume, directory, lookup.Status); + lookup.Exists := false; + dataStart := fatDataStartLBA(volume, lookup.BootRecord); + + splitFileNameParts(fileName, namePart, extPart); + cleanFileName := cleanString(namePart, lookup.Status); + + if LL_size(lookup.Dirs) > 0 then begin + for i := 0 to LL_Size(lookup.Dirs) - 1 do begin + tempdir := PDirectory(LL_get(lookup.Dirs, i)); + otherCFN := cleanString(tempdir^.filename, lookup.Status); + if compareByteArray8(cleanFileName, otherCFN) and matchExtension(tempdir^.fileExtension, extPart) then begin + lookup.Dir := tempdir; + lookup.Exists := true; + end; + end; + end; + + { Determine parent cluster (Lesson 18: root has no '.' entry) } + if (directory = nil) or (stringSize(directory) = 0) then + dirCluster := lookup.BootRecord^.rootCluster + else begin + if LL_size(lookup.Dirs) > 0 then begin + tempdir := PDirectory(LL_get(lookup.Dirs, 0)); + if tempdir^.fileName[0] = '.' then + dirCluster := dirFirstCluster(tempdir) + else + dirCluster := lookup.BootRecord^.rootCluster; + end else + dirCluster := lookup.BootRecord^.rootCluster; + end; + + if lookup.Exists then begin + origByteSize := lookup.Dir^.byteSize; + cluster := dirFirstCluster(lookup.Dir); + end else begin + origByteSize := 0; + cluster := 0; + end; + end; + fileSize := origByteSize; + if lookup.BootRecord^.sectorSize > 0 then + chainWalkLimit := (lookup.BootRecord^.FATSize * lookup.BootRecord^.sectorSize) div 4 + else + chainWalkLimit := 0; + + if not lookup.Exists then begin + { --- Create a new zero-length file entry on disk --- } + { dirCluster, cleanFileName, extPart already set above } + + dirBuf := puint32(kalloc(lookup.BootRecord^.sectorSize)); + + if not locateFreeDirEntry(volume, dirCluster, lookup.BootRecord, loc, dirBuf) then begin + kfree(dirBuf); + io.syslog.logln('FAT32', 'writeFileAtOffset: exit-A no free dir slot'); + if ofi = nil then begin + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); + end; + exit; + end; + + rawDir := @PDirectory(dirBuf)[loc.EntryIdx]; + memset(uint32(rawDir), 0, sizeof(TDirectory)); + rawDir^.fileName := cleanFileName; + rawDir^.attributes := 0; { regular file } + fillFatExt(rawDir^.fileExtension, extPart); + rawDir^.clusterLow := 0; + rawDir^.clusterHigh := 0; + rawDir^.byteSize := 0; + driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, dirBuf); + + kfree(dirBuf); + + { Invalidate chain cache — new file } + sioc_valid := false; + end; + newEnd := offset + byteCount; + bytesPerCluster := uint32(lookup.BootRecord^.spc) * uint32(lookup.BootRecord^.sectorSize); + + { ---- Extend file if writing past EOF ---- } + if newEnd > fileSize then begin + if ofi <> nil then + oldClusters := ofi^.AllocClusters + else + oldClusters := fatClustersForBytes(fileSize, bytesPerCluster); + needClusters := fatClustersForBytes(newEnd, bytesPerCluster); + targetClusters := needClusters; + if (ofi <> nil) and (needClusters > oldClusters) then begin + reserveClusters := fatPreallocClusterCount(bytesPerCluster); + targetClusters := oldClusters + reserveClusters; + if targetClusters < needClusters then + targetClusters := needClusters; + end; + + if targetClusters > oldClusters then begin + extraClusters := targetClusters - oldClusters; + if oldClusters > 0 then begin + { Walk to last cluster using cache if available } + if sioc_valid and (sioc_vol = volume) and (sioc_fileClust = cluster) then + lastCluster := sioc_chainClust + else + lastCluster := cluster; + nextFat := readFat(volume, lastCluster, lookup.BootRecord); + while ((nextFat and $0FFFFFFF) < $0FFFFFF8) and (nextFat <> 0) do begin + if chainWalkLimit = 0 then + break; + chainWalkLimit := chainWalkLimit - 1; + if chainWalkLimit = 0 then begin + io.syslog.logln('FAT32', 'writeFileAtOffset: FAT loop detected while finding tail'); + if ofi = nil then begin + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); + end; + exit; + end; + lastCluster := nextFat; + nextFat := readFat(volume, lastCluster, lookup.BootRecord); + end; + end else begin + { Zero-length file: first new cluster becomes start cluster } + lastCluster := 0; + end; + + if fatFindContiguousFreeRange(volume, extraClusters, lookup.BootRecord, firstNewCluster) then begin + fatLinkContiguousRange(volume, lookup.BootRecord, lastCluster, firstNewCluster, extraClusters, lastCluster); + if oldClusters = 0 then + cluster := firstNewCluster; + if ofi <> nil then begin + if ofi^.Extents = nil then + ofi^.Extents := LL_New(sizeof(TClusterExtent)); + if ofi^.Extents <> nil then + fatAppendExtent(ofi^.Extents, firstNewCluster, extraClusters); + end; + end else begin + newExtents := findFreeClusterExtents(volume, extraClusters, lookup.BootRecord); + if newExtents = nil then begin + io.syslog.logln('FAT32', 'writeFileAtOffset: exit-B disk full'); + { Undo just-created dir entry if file was new } + if not lookup.Exists then begin + dirBuf := puint32(kalloc(lookup.BootRecord^.sectorSize)); + driver.storage.mgr.storage_read(volume^.device, loc.SectorLBA, 1, dirBuf); + PDirectory(dirBuf)[loc.EntryIdx].fileName[0] := char($E5); + driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, dirBuf); + kfree(dirBuf); + end; + if ofi = nil then begin + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); + end; + exit; + end; + + { Link new extents } + if LL_Size(newExtents) > 0 then begin + for i := 0 to LL_Size(newExtents) - 1 do begin + extent := PClusterExtent(LL_Get(newExtents, i)); + fatLinkContiguousRange(volume, lookup.BootRecord, lastCluster, + extent^.StartCluster, extent^.ClusterCount, lastCluster); + if (ofi <> nil) and (extent <> nil) then begin + if ofi^.Extents = nil then + ofi^.Extents := LL_New(sizeof(TClusterExtent)); + if ofi^.Extents <> nil then + fatAppendExtent(ofi^.Extents, extent^.StartCluster, extent^.ClusterCount); + end; + if (oldClusters = 0) and (i = 0) then + cluster := extent^.StartCluster; + end; + end; + LL_Free(newExtents); + end; + + if ofi <> nil then + ofi^.FatDirty := true; + + if ofi <> nil then + ofi^.AllocClusters := targetClusters; + + { Update chain cache: last new cluster is the new tail } + updateSIOC(volume, cluster, targetClusters - 1, lastCluster); + + end; + + fileSize := newEnd; + end; + + { ---- Walk to starting cluster using cache, then write sector by sector ---- } + startClusterIdx := offset div bytesPerCluster; + inClusterOffset := offset mod bytesPerCluster; + + if not seekToClusterIndex(volume, cluster, startClusterIdx, lookup.BootRecord, curCluster, chainPos) then begin + io.syslog.logln('FAT32', 'writeFileAtOffset: exit-C chain broke seeking to start'); + if ofi = nil then begin + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); + end; + exit; + end; + + spc := uint32(lookup.BootRecord^.spc); + secSize := uint32(lookup.BootRecord^.sectorSize); + if (ofi <> nil) and (ofi^.ScratchSector <> nil) and (ofi^.ScratchSize >= secSize) then + ioBuf := ofi^.ScratchSector + else + ioBuf := nil; + remaining := byteCount; + srcPos := 0; + + while (remaining > 0) do begin + { Build contiguous cluster run starting at curCluster } + run.StartCluster := curCluster; + buildContiguousRun(volume, curCluster, spc, MAX_RUN_SECTORS, lookup.BootRecord, run.RunLen, run.LastCluster); + curCluster := run.LastCluster; + + run.RunSectors := run.RunLen * spc; + run.RunBytes := run.RunSectors * secSize; + run.LBA := clusterToLBA(dataStart, spc, run.StartCluster); + + if srcPos = 0 then + skipBytes := inClusterOffset + else + skipBytes := 0; + + copyBytes := run.RunBytes - skipBytes; + if copyBytes > remaining then + copyBytes := remaining; + + firstSectorOff := skipBytes mod secSize; + + { Phase 1: Partial first sector — read-modify-write } + if firstSectorOff > 0 then begin + if ioBuf = nil then + ioBuf := puint32(kalloc(secSize)); + if ioBuf = nil then + break; + sectorLBA := run.LBA + (skipBytes div secSize); + partialBytes := secSize - firstSectorOff; + if partialBytes > copyBytes then + partialBytes := copyBytes; + driver.storage.mgr.storage_read(volume^.device, sectorLBA, 1, ioBuf); + core.util.memcpy(uint32(buffer) + srcPos, uint32(ioBuf) + firstSectorOff, partialBytes); + driver.storage.mgr.storage_write(volume^.device, sectorLBA, 1, ioBuf); + srcPos := srcPos + partialBytes; + remaining := remaining - partialBytes; + copyBytes := copyBytes - partialBytes; + skipBytes := skipBytes + partialBytes; + end; + + { Phase 2: Full sectors — batch write directly from caller buffer } + fullSectors := copyBytes div secSize; + if fullSectors > 0 then begin + sectorLBA := run.LBA + (skipBytes div secSize); + batchBytes := fullSectors * secSize; + driver.storage.mgr.storage_write(volume^.device, sectorLBA, fullSectors, + puint32(uint32(buffer) + srcPos)); + srcPos := srcPos + batchBytes; + remaining := remaining - batchBytes; + copyBytes := copyBytes - batchBytes; + skipBytes := skipBytes + batchBytes; + end; + + { Phase 3: Partial last sector — read-modify-write } + if copyBytes > 0 then begin + if ioBuf = nil then + ioBuf := puint32(kalloc(secSize)); + if ioBuf = nil then + break; + sectorLBA := run.LBA + (skipBytes div secSize); + driver.storage.mgr.storage_read(volume^.device, sectorLBA, 1, ioBuf); + core.util.memcpy(uint32(buffer) + srcPos, uint32(ioBuf), copyBytes); + driver.storage.mgr.storage_write(volume^.device, sectorLBA, 1, ioBuf); + srcPos := srcPos + copyBytes; + remaining := remaining - copyBytes; + end; + + { Update chain cache to last cluster in this run } + updateSIOC(volume, cluster, chainPos + run.RunLen - 1, curCluster); + chainPos := chainPos + run.RunLen; + + if remaining > 0 then begin + nextFat := readFat(volume, curCluster, lookup.BootRecord); + if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then begin + io.syslog.log('FAT32', 'writeFileAtOffset: exit-D chain broke in write loop, srcPos='); + io.syslog.writeint(srcPos); + io.syslog.writestring(' remaining='); + io.syslog.writeint(remaining); + io.syslog.writestring(' cluster='); + io.syslog.writeint(curCluster); + io.syslog.writestring(' fat='); + io.syslog.writehexln(nextFat); + break; + end; + curCluster := nextFat; + end; + end; + + if (ioBuf <> nil) and ((ofi = nil) or (ioBuf <> ofi^.ScratchSector)) then + kfree(ioBuf); + + { Flush dirty FAT cache entries to disk only for uncached callers. + Open-file callers defer this to fat32CloseFile to avoid flushing on every chunk. } + if ofi = nil then begin + fatCache := fatCacheGet(volume); + if fatCache <> nil then + fatCacheFlush(fatCache, volume); + end; + + { ---- Update directory entry byteSize if file was extended ---- } + { Only commit actual bytes written — never advance size past what was really written (Lesson 22) } + if (srcPos > 0) and ((offset + srcPos) > origByteSize) then begin + if ofi <> nil then begin + ofi^.MetaDirty := true; + end else begin + { Lesson 18: search raw directory sectors, don't use LL indices. + dirCluster, cleanFileName, extPart were set once in the discovery phase. } + dirBuf := puint32(kalloc(lookup.BootRecord^.sectorSize)); + + if locateDirEntry(volume, dirCluster, lookup.BootRecord, cleanFileName, extPart, loc, dirBuf) then begin + rawDir := @PDirectory(dirBuf)[loc.EntryIdx]; + rawDir^.byteSize := offset + srcPos; + { If file was zero-length, update cluster pointers too } + if origByteSize = 0 then + setDirFirstCluster(rawDir, cluster); + driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, dirBuf); + end; + + kfree(dirBuf); + end; + end; + + { Update cached open-file metadata so the next read/write sees the new state } + if ofi <> nil then begin + if (srcPos > 0) and ((offset + srcPos) > ofi^.ByteSize) then + ofi^.ByteSize := offset + srcPos; + if (ofi^.FirstCluster = 0) and (cluster <> 0) then begin + ofi^.FirstCluster := cluster; + ofi^.Exists := true; + ofi^.MetaDirty := true; + end; + end; + + if ofi = nil then begin + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); + end; + writeFileAtOffset := srcPos; + push_trace('driver.storage.fs.fat32.writeFileAtOffset.exit'); +end; + +{ Read byteCount bytes from a file beginning at byte offset 'offset'. + Returns the number of bytes actually read (may be less than byteCount if EOF reached). + + NOTE: This implementation reads sectors sequentially from the first cluster of the + file, consistent with the existing readFile behaviour. It does NOT follow the FAT + chain between clusters \u2014 it assumes contiguous allocation. This is acceptable for + the initial streaming implementation. A future improvement should cache the last + visited (offset, cluster) pair in the open-file entry to make sequential chunked + reads O(1) per call instead of O(n) in the FAT chain length. } + +function getFileSize(volume : PStorage_Volume; directory : pchar; fileName : pchar) : uint32; +var + bootRecord : PBootRecord; + dirs : PLinkedListBase; + tempdir : PDirectory; + statusOut : puint32; + i : uint32; + cleanFileName : byteArray8; + otherCFN : byteArray8; + namePart : pchar; + extPart : pchar; + fsize : uint32; +begin + push_trace('driver.storage.fs.fat32.getFileSize.enter'); + getFileSize := 0; + + statusOut := puint32(kalloc(sizeof(uint32))); + statusOut^ := 0; + bootRecord := readBootRecord(volume); + dirs := readDirectory(volume, directory, statusOut); + + splitFileNameParts(fileName, namePart, extPart); + + fsize := 0; + if LL_size(dirs) > 0 then begin + cleanFileName := cleanString(namePart, statusOut); + for i := 0 to LL_Size(dirs) - 1 do begin + tempdir := PDirectory(LL_get(dirs, i)); + otherCFN := cleanString(tempdir^.filename, statusOut); + if compareByteArray8(cleanFileName, otherCFN) and matchExtension(tempdir^.fileExtension, extPart) then begin + fsize := tempdir^.byteSize; + break; + end; + end; + end; + + kfree(void(namePart)); + kfree(void(extPart)); + LL_Free(dirs); + kfree(puint32(bootRecord)); + kfree(puint32(statusOut)); + getFileSize := fsize; + push_trace('driver.storage.fs.fat32.getFileSize.exit'); +end; + +function readFileAtOffset(volume : PStorage_Volume; directory : pchar; fileName : pchar; offset : uint32; buffer : puint32; byteCount : uint32; ctx : pointer) : uint32; +const + MAX_RUN_SECTORS = 2048; { cap per multi-sector read = 1 MB } +var + lookup : TFileLookup; + run : TRunInfo; + ofi : PFATOpenFile; + i : uint32; + cluster : uint32; + dataStart : uint32; + cleanFileName : byteArray8; + otherCFN : byteArray8; + tempdir : PDirectory; + namePart : pchar; + extPart : pchar; + bytesPerCluster : uint32; + startClusterIdx : uint32; + inClusterOffset : uint32; + chainPos : uint32; + curCluster : uint32; + destPos : uint32; + remaining : uint32; + fileSize : uint32; + nextFat : uint32; + spc : uint32; + secSize : uint32; + { Multi-sector run variables } + runBuf : puint32; + skipBytes : uint32; { bytes to skip at start of run buffer } + copyBytes : uint32; + chunkSectors : uint32; + chunkBytes : uint32; +begin + push_trace('driver.storage.fs.fat32.readFileAtOffset.enter'); + readFileAtOffset := 0; + ofi := PFATOpenFile(ctx); + + if ofi <> nil then begin + { Fast path: use cached open-file metadata } + lookup.BootRecord := ofi^.BootRecord; + lookup.Dirs := nil; + lookup.Dir := nil; + lookup.Status := nil; + lookup.Exists := ofi^.Exists; + dataStart := ofi^.DataStart; + if lookup.Exists then begin + fileSize := ofi^.ByteSize; + cluster := ofi^.FirstCluster; + end else begin + fileSize := 0; + cluster := 0; + end; + end else begin + { Slow path: rediscover everything from disk } + lookup.Status := puint32(kalloc(sizeof(uint32))); + lookup.Status^ := 0; + lookup.BootRecord := readBootRecord(volume); + lookup.Dirs := readDirectory(volume, directory, lookup.Status); + lookup.Exists := false; + dataStart := fatDataStartLBA(volume, lookup.BootRecord); + + splitFileNameParts(fileName, namePart, extPart); + + if LL_size(lookup.Dirs) > 0 then begin + cleanFileName := cleanString(namePart, lookup.Status); + for i := 0 to LL_Size(lookup.Dirs) - 1 do begin + tempdir := PDirectory(LL_get(lookup.Dirs, i)); + otherCFN := cleanString(tempdir^.filename, lookup.Status); + if compareByteArray8(cleanFileName, otherCFN) and matchExtension(tempdir^.fileExtension, extPart) then begin + lookup.Dir := tempdir; + lookup.Exists := true; + end; + end; + end; + + kfree(void(namePart)); + kfree(void(extPart)); + + if lookup.Exists then begin + fileSize := lookup.Dir^.byteSize; + cluster := dirFirstCluster(lookup.Dir); + end else begin + fileSize := 0; + cluster := 0; + end; + end; + + if not lookup.Exists then begin + if ofi = nil then begin + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); + end; + push_trace('driver.storage.fs.fat32.readFileAtOffset.notFound'); + exit; + end; + + if offset >= fileSize then begin + if ofi = nil then begin + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); + end; + exit; + end; + + { Clamp read length to not exceed file size } + remaining := byteCount; + if offset + remaining > fileSize then + remaining := fileSize - offset; + + spc := uint32(lookup.BootRecord^.spc); + secSize := uint32(lookup.BootRecord^.sectorSize); + bytesPerCluster := spc * secSize; + startClusterIdx := offset div bytesPerCluster; + inClusterOffset := offset mod bytesPerCluster; + + { Determine curCluster at startClusterIdx using SIOC cache } + if not seekToClusterIndex(volume, cluster, startClusterIdx, lookup.BootRecord, curCluster, chainPos) then begin + if ofi = nil then begin + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); + end; + exit; + end; + + destPos := 0; + + while (remaining > 0) do begin + { Build a contiguous cluster run starting at curCluster } + run.StartCluster := curCluster; + buildContiguousRun(volume, curCluster, spc, MAX_RUN_SECTORS, lookup.BootRecord, run.RunLen, curCluster); + + run.RunSectors := run.RunLen * spc; + run.RunBytes := run.RunSectors * secSize; + run.LBA := clusterToLBA(dataStart, spc, run.StartCluster); + + { Calculate how many bytes from this run we actually need } + if destPos = 0 then + skipBytes := inClusterOffset + else + skipBytes := 0; + + copyBytes := run.RunBytes - skipBytes; + if copyBytes > remaining then + copyBytes := remaining; + + { Determine if we can read directly into caller's buffer (aligned, full run) } + if (skipBytes = 0) and (copyBytes >= run.RunBytes) then begin + { Direct read into output buffer — no temp allocation } + driver.storage.mgr.storage_read(volume^.device, run.LBA, run.RunSectors, + puint32(uint32(buffer) + destPos)); + destPos := destPos + copyBytes; + remaining := remaining - copyBytes; + end else if (skipBytes = 0) and ((copyBytes mod secSize) = 0) then begin + { Partial run but sector-aligned — read only needed sectors } + chunkSectors := copyBytes div secSize; + driver.storage.mgr.storage_read(volume^.device, run.LBA, chunkSectors, + puint32(uint32(buffer) + destPos)); + destPos := destPos + copyBytes; + remaining := remaining - copyBytes; + end else begin + { Unaligned — need temp buffer for partial first/last sectors } + { Only allocate for sectors we actually touch } + chunkSectors := (skipBytes + copyBytes + secSize - 1) div secSize; + if chunkSectors > run.RunSectors then chunkSectors := run.RunSectors; + chunkBytes := chunkSectors * secSize; + runBuf := puint32(kalloc(chunkBytes)); + run.LBA := clusterToLBA(dataStart, spc, run.StartCluster) + (skipBytes div secSize); + + { Adjust skipBytes for sectors we're skipping entirely } + skipBytes := skipBytes mod secSize; + driver.storage.mgr.storage_read(volume^.device, run.LBA, chunkSectors, runBuf); + core.util.memcpy(uint32(runBuf) + skipBytes, uint32(buffer) + destPos, copyBytes); + kfree(runBuf); + destPos := destPos + copyBytes; + remaining := remaining - copyBytes; + end; + + { Update SIOC chain cache to last cluster in this run } + updateSIOC(volume, cluster, chainPos + run.RunLen - 1, curCluster); + chainPos := chainPos + run.RunLen; + + { Advance to next cluster beyond this run } + if remaining > 0 then begin + nextFat := readFat(volume, curCluster, lookup.BootRecord); + if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then + break; + curCluster := nextFat; + end; + end; + + if ofi = nil then begin + LL_Free(lookup.Dirs); + kfree(puint32(lookup.BootRecord)); + kfree(puint32(lookup.Status)); + end; + readFileAtOffset := destPos; + push_trace('driver.storage.fs.fat32.readFileAtOffset.exit'); +end; + +procedure readFileAtOffsetAsync(volume : PStorage_Volume; directory : pchar; fileName : pchar; + offset : uint32; buffer : puint32; byteCount : uint32; + ctx : pointer; bytesRead : puint32; + callback : TIOCallback; callbackData : pointer); +var + ofi : PFATOpenFile; + asyncCtx : PFATFileIOAsyncCtx; + sectorLBA : uint32; + chunkSectors : uint32; + err : TError; +begin + if bytesRead <> nil then bytesRead^ := 0; + ofi := PFATOpenFile(ctx); + + if (ofi = nil) or (buffer = nil) or (byteCount = 0) then begin + if callback <> nil then callback(eInvalidArgument, callbackData); + exit; + end; + + if not ofi^.Exists then begin + if callback <> nil then callback(eFileDoesNotExist, callbackData); + exit; + end; + + if offset >= ofi^.ByteSize then begin + if callback <> nil then callback(eNone, callbackData); + exit; + end; + + if offset + byteCount > ofi^.ByteSize then + byteCount := ofi^.ByteSize - offset; + + if ((offset mod ofi^.BootRecord^.sectorSize) <> 0) or ((byteCount mod ofi^.BootRecord^.sectorSize) <> 0) then begin + if bytesRead <> nil then + bytesRead^ := readFileAtOffset(volume, directory, fileName, offset, buffer, byteCount, ctx); + if callback <> nil then callback(eNone, callbackData); + exit; + end; + + asyncCtx := fatTransferAlloc(volume); + if asyncCtx = nil then begin + if callback <> nil then callback(eOutOfMemory, callbackData); + exit; + end; + asyncCtx^.Mode := famRead; + asyncCtx^.State := ftsPrepare; + asyncCtx^.Volume := volume; + asyncCtx^.OpenFile := ofi; + asyncCtx^.Buffer := buffer; + asyncCtx^.Offset := offset; + asyncCtx^.ByteCount := byteCount; + asyncCtx^.BytesOut := bytesRead; + asyncCtx^.Callback := callback; + asyncCtx^.CallbackData := callbackData; + asyncCtx^.Cluster := ofi^.FirstCluster; + asyncCtx^.DataStart := ofi^.DataStart; + asyncCtx^.BootRecord := ofi^.BootRecord; + asyncCtx^.LastError := eNone; + asyncCtx^.CursorValid := false; + asyncCtx^.ExtentValid := false; + if fat32_fileio_prepare_single_run(asyncCtx, sectorLBA, chunkSectors) and + (asyncCtx^.ChunkBytes = asyncCtx^.ByteCount) then begin + err := driver.storage.mgr.storage_read_async( + volume^.device, sectorLBA, chunkSectors, buffer, + @fat32_fileio_direct_complete, asyncCtx); + if err <> eNone then begin + fatTransferFree(asyncCtx); + if callback <> nil then callback(err, callbackData); + end; + exit; + end; + fatWorkerEnsure(volume); + if not fatScheduleTransfer(asyncCtx) then begin + fatTransferFree(asyncCtx); + if callback <> nil then callback(eQueueFull, callbackData); + end; +end; + +procedure writeFileAtOffsetAsync(volume : PStorage_Volume; directory : pchar; fileName : pchar; + offset : uint32; buffer : puint32; byteCount : uint32; + ctx : pointer; bytesWritten : puint32; + callback : TIOCallback; callbackData : pointer); +var + ofi : PFATOpenFile; + asyncCtx : PFATFileIOAsyncCtx; + cluster : uint32; + dataStart : uint32; + origByteSize : uint32; + newEnd : uint32; + bytesPerCluster : uint32; + oldClusters : uint32; + needClusters : uint32; + targetClusters : uint32; + extraClusters : uint32; + newExtents : PLinkedListBase; + lastCluster : uint32; + nextFat : uint32; + i : uint32; + extent : PClusterExtent; + dirBuf : puint32; + rawDir : PDirectory; + loc : TDirEntryLocation; + chainWalkLimit : uint32; + firstNewCluster : uint32; + reserveClusters : uint32; + sectorLBA : uint32; + chunkSectors : uint32; + err : TError; +begin + if bytesWritten <> nil then bytesWritten^ := 0; + ofi := PFATOpenFile(ctx); + + if (ofi = nil) or (buffer = nil) or (byteCount = 0) then begin + if callback <> nil then callback(eInvalidArgument, callbackData); + exit; + end; + + if ((offset mod ofi^.BootRecord^.sectorSize) <> 0) or ((byteCount mod ofi^.BootRecord^.sectorSize) <> 0) then begin + if bytesWritten <> nil then + bytesWritten^ := writeFileAtOffset(volume, directory, fileName, offset, buffer, byteCount, ctx); + if callback <> nil then callback(eNone, callbackData); + exit; + end; + + dataStart := ofi^.DataStart; + origByteSize := ofi^.ByteSize; + if ofi^.BootRecord^.sectorSize > 0 then + chainWalkLimit := (ofi^.BootRecord^.FATSize * ofi^.BootRecord^.sectorSize) div 4 + else + chainWalkLimit := 0; + if ofi^.Exists then + cluster := ofi^.FirstCluster + else + cluster := 0; + + if not ofi^.Exists then begin + dirBuf := puint32(kalloc(ofi^.BootRecord^.sectorSize)); + if dirBuf = nil then begin + if callback <> nil then callback(eOutOfMemory, callbackData); + exit; + end; + if not locateFreeDirEntry(volume, ofi^.DirCluster, ofi^.BootRecord, loc, dirBuf) then begin + kfree(dirBuf); + if callback <> nil then callback(eDirectoryFull, callbackData); + exit; + end; + rawDir := @PDirectory(dirBuf)[loc.EntryIdx]; + memset(uint32(rawDir), 0, sizeof(TDirectory)); + rawDir^.fileName := ofi^.CleanName; + rawDir^.attributes := 0; + fillFatExt(rawDir^.fileExtension, ofi^.ExtPart); + rawDir^.clusterLow := 0; + rawDir^.clusterHigh := 0; + rawDir^.byteSize := 0; + driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, dirBuf); + kfree(dirBuf); + ofi^.Exists := true; + sioc_valid := false; + end; + + newEnd := offset + byteCount; + bytesPerCluster := uint32(ofi^.BootRecord^.spc) * uint32(ofi^.BootRecord^.sectorSize); + + if newEnd > origByteSize then begin + oldClusters := ofi^.AllocClusters; + needClusters := fatClustersForBytes(newEnd, bytesPerCluster); + targetClusters := needClusters; + if needClusters > oldClusters then begin + reserveClusters := fatPreallocClusterCount(bytesPerCluster); + targetClusters := oldClusters + reserveClusters; + if targetClusters < needClusters then + targetClusters := needClusters; + end; + + if targetClusters > oldClusters then begin + extraClusters := targetClusters - oldClusters; + if oldClusters > 0 then begin + if sioc_valid and (sioc_vol = volume) and (sioc_fileClust = cluster) then + lastCluster := sioc_chainClust + else + lastCluster := cluster; + nextFat := readFat(volume, lastCluster, ofi^.BootRecord); + while ((nextFat and $0FFFFFFF) < $0FFFFFF8) and (nextFat <> 0) do begin + if chainWalkLimit = 0 then begin + if callback <> nil then callback(eCorruptFilesystem, callbackData); + exit; + end; + chainWalkLimit := chainWalkLimit - 1; + lastCluster := nextFat; + nextFat := readFat(volume, lastCluster, ofi^.BootRecord); + end; + end else + lastCluster := 0; + + if fatFindContiguousFreeRange(volume, extraClusters, ofi^.BootRecord, firstNewCluster) then begin + fatLinkContiguousRange(volume, ofi^.BootRecord, lastCluster, firstNewCluster, extraClusters, lastCluster); + if oldClusters = 0 then + cluster := firstNewCluster; + if ofi^.Extents = nil then + ofi^.Extents := LL_New(sizeof(TClusterExtent)); + if ofi^.Extents <> nil then + fatAppendExtent(ofi^.Extents, firstNewCluster, extraClusters); + end else begin + newExtents := findFreeClusterExtents(volume, extraClusters, ofi^.BootRecord); + if newExtents = nil then begin + if callback <> nil then callback(eDiskFull, callbackData); + exit; + end; + + if LL_Size(newExtents) > 0 then begin + for i := 0 to LL_Size(newExtents) - 1 do begin + extent := PClusterExtent(LL_Get(newExtents, i)); + fatLinkContiguousRange(volume, ofi^.BootRecord, lastCluster, + extent^.StartCluster, extent^.ClusterCount, lastCluster); + if extent <> nil then begin + if ofi^.Extents = nil then + ofi^.Extents := LL_New(sizeof(TClusterExtent)); + if ofi^.Extents <> nil then + fatAppendExtent(ofi^.Extents, extent^.StartCluster, extent^.ClusterCount); + end; + if (oldClusters = 0) and (i = 0) then + cluster := extent^.StartCluster; + end; + end; + LL_Free(newExtents); + end; + + ofi^.FatDirty := true; + ofi^.AllocClusters := targetClusters; + updateSIOC(volume, cluster, targetClusters - 1, lastCluster); + end; + end; + + asyncCtx := fatTransferAlloc(volume); + if asyncCtx = nil then begin + if callback <> nil then callback(eOutOfMemory, callbackData); + exit; + end; + asyncCtx^.Mode := famWrite; + asyncCtx^.State := ftsPrepare; + asyncCtx^.Volume := volume; + asyncCtx^.OpenFile := ofi; + asyncCtx^.Buffer := buffer; + asyncCtx^.Offset := offset; + asyncCtx^.ByteCount := byteCount; + asyncCtx^.BytesOut := bytesWritten; + asyncCtx^.Callback := callback; + asyncCtx^.CallbackData := callbackData; + asyncCtx^.Cluster := cluster; + asyncCtx^.OrigByteSize := origByteSize; + asyncCtx^.DataStart := dataStart; + asyncCtx^.BootRecord := ofi^.BootRecord; + + if (cluster <> 0) and ((offset + byteCount) > ofi^.ByteSize) then + ofi^.MetaDirty := true; + + asyncCtx^.LastError := eNone; + asyncCtx^.CursorValid := false; + asyncCtx^.ExtentValid := false; + if fat32_fileio_prepare_single_run(asyncCtx, sectorLBA, chunkSectors) and + (asyncCtx^.ChunkBytes = asyncCtx^.ByteCount) then begin + err := driver.storage.mgr.storage_write_async( + volume^.device, sectorLBA, chunkSectors, buffer, + @fat32_fileio_direct_complete, asyncCtx); + if err <> eNone then begin + fatTransferFree(asyncCtx); + if callback <> nil then callback(err, callbackData); + end; + exit; + end; + fatWorkerEnsure(volume); + if not fatScheduleTransfer(asyncCtx) then begin + fatTransferFree(asyncCtx); + if callback <> nil then callback(eQueueFull, callbackData); + end; +end; + +procedure init(); +begin + { Legacy reference unit only. The rewritten public FAT32 unit handles + filesystem registration and callback selection. } +end; + +end. diff --git a/src/driver/storage/fs/driver.storage.fs.fat32.pas b/src/driver/storage/fs/driver.storage.fs.fat32.pas index 93aa9e15..83e7e494 100644 --- a/src/driver/storage/fs/driver.storage.fs.fat32.pas +++ b/src/driver/storage/fs/driver.storage.fs.fat32.pas @@ -1,23 +1,3 @@ -// Copyright 2021 Kieron Morris -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -{ - Driver->storage->driver.storage.fs.fat32 - driver.storage.fs.fat32 filesystem driver - - @author(Aaron Hance ) -} - unit driver.storage.fs.fat32; interface @@ -25,4581 +5,56 @@ interface uses boot.mgr, driver.storage.fs.mgr, - core.ds.lists, - memory.heap, - proc.mgr, - proc.types, - driver.timer.rtc, - io.stdio, - driver.storage.mgr, - driver.storage.types, - core.strings, + driver.storage.types; + +procedure init; +procedure FAT32ReleaseVolumeState(volume : PStorage_Volume); + +implementation + +uses io.syslog, - debug.tracer, - core.util, arch.x86.util, - driver.storage.vol.mgr; - -type - - TBootRecord = bitpacked record - jmp2boot : ubit24; - OEMName : array[0..7] of char; - sectorSize : uint16; - spc : uint8; - rsvSectors : uint16; - numFats : uint8; - numDirEnt : uint16; - numSectors : uint16; - mediaDescp : uint8; - sectorsPerFat : uint16; - sectorsPerTrack : uint16; - heads : uint16; - hiddenSectors : uint32; - manySectors : uint32; - FATSize : uint32; - flags : uint16; - signature : uint8; - FATVersion : uint16; - rootCluster : uint32; - FSInfoCluster : uint16; - backupCluster : uint16; - reserved0 : array[0..11] of uint8; - driveNumber : uint8; - reserved1 : uint8; - bsignature : uint8;// = $28; - volumeID : uint32; - volumeLabel : array[0..10] of uint8; - identString : array[0..7] of char;// = 'FAT32 '; - end; - PBootRecord = ^TBootRecord; - - TFATExtArray = array[0..2] of char; - - TDirectory = packed record - fileName : array[0..7] of char; - fileExtension : TFATExtArray; - attributes : uint8; - reserved0 : uint8; - timeFine : uint8; - time : uint16; - date : uint16; - accessTime : uint16; - clusterHigh : uint16; - modifiedTime : uint16; - modifiedDate : uint16; - clusterLow : uint16; - byteSize : uint32; - end; - PDirectory = ^TDirectory; - - TFilesystemInfo = record - leadSignature : uint32; - reserved0 : array[0..479] of uint8; - structSignature : uint32; - freeSectors : uint32; - nextFreeSector : uint32; - reserved1 : array[0..11] of uint8; - trailSignature : uint32; - end; - - TFatVolumeInfo = record - sectorsPerCluster : uint8; // must be power of 2 and mult by sectorsize to max 32k - end; - PFatVolumeInfo = ^TFatVolumeInfo; - - TClusterExtent = record - StartCluster : uint32; - ClusterCount : uint32; - end; - PClusterExtent = ^TClusterExtent; - -const - { FAT sector cache — 2-way set-associative, 256 sets, LRU replacement. - This keeps small-cluster sequential files from churning the FAT cache - once they grow beyond a few megabytes. } - FAT_CACHE_SETS = 256; - FAT_CACHE_WAYS = 2; - FAT_CACHE_LINES = 512; { SETS * WAYS } - FAT_TRANSFER_POOL_CAPACITY = 64; - FAT_WORK_QUEUE_CAPACITY = FAT_TRANSFER_POOL_CAPACITY; - FAT_WRITE_PREALLOC_BYTES = 8 * 1024 * 1024; - FAT_WRITE_PREALLOC_MAX_CLUSTERS = 2048; - FAT_WRITE_PREALLOC_MIN_CLUSTERS = 64; - -type - TFATCacheLine = record - SectorIdx : uint32; { which FAT sector is stored here } - Dirty : boolean; { true if written but not flushed to disk } - Valid : boolean; { true if line contains valid data } - LRU : uint8; { 0 = MRU, 1 = LRU, 2 = LRU (for 3-way), 3 = LRU (for 4-way) } - end; - - PFATCache = ^TFATCache; - TFATCache = record - Lines : array[0..FAT_CACHE_LINES - 1] of TFATCacheLine; - Data : puint32; { kalloc'd FAT_CACHE_LINES * 512 bytes of FAT sector data } - EvictBuf : puint32; { kalloc'd 512-byte scratch for eviction writes } - Busy : boolean; { spinlock: serializes miss-path eviction+load } - FatStart : uint32; { LBA of first FAT sector on disk } - Device : PStorage_Device; { device pointer for I/O } - BootRecord : PBootRecord; { cached boot record (read once) } - end; - - TFATAsyncMode = (famRead, famWrite); - - TFATTransferState = ( - ftsIdle, - ftsPrepare, - ftsIssueIO, - ftsAwaitIO, - ftsAdvance, - ftsComplete, - ftsError - ); - - { Per-file cached metadata created by fat32OpenFile and stored in the - file descriptor's FSPrivate pointer. Avoids re-reading boot record, - directory listing, and searching for the file on every read/write. } - PFATOpenFile = ^TFATOpenFile; - PFATVolumeCtx = ^TFATVolumeCtx; - PFATFileIOAsyncCtx = ^TFATFileIOAsyncCtx; - - TFATOpenFile = record - Volume : PStorage_Volume; - BootRecord : PBootRecord; { borrows from TFATCache — do NOT kfree } - DataStart : uint32; { fatDataStartLBA result } - FirstCluster : uint32; { file's start cluster (0 if new/empty) } - ByteSize : uint32; { current file size in bytes } - AllocClusters: uint32; { current reserved cluster count for appends } - Extents : PLinkedListBase;{ cached cluster extents for fast sequential I/O } - DirCluster : uint32; { parent directory's start cluster } - CleanName : byteArray8; { cleaned 8.3 filename } - ExtPart : pchar; { heap-allocated extension string — owned } - ScratchSector: puint32; { per-open-file sector scratch buffer } - ScratchSize : uint32; { size of ScratchSector in bytes } - Exists : boolean; { true if file existed at open time } - FatDirty : boolean; { true if FAT cache must be flushed on close } - MetaDirty : boolean; { true if dir entry size/cluster needs sync on close } - end; - - TFATVolumeCtx = record - Cache : TFATCache; - TransferPoolBuf : pointer; - TransferFreeList : pointer; - Worker : proc.types.PProcessContext; - WorkHead : uint32; - WorkTail : uint32; - WorkCount : uint32; - WorkQueue : array[0..FAT_WORK_QUEUE_CAPACITY - 1] of PFATFileIOAsyncCtx; - end; - - TFATFileIOAsyncCtx = record - Next : PFATFileIOAsyncCtx; - VolumeCtx : PFATVolumeCtx; - State : TFATTransferState; - Mode : TFATAsyncMode; - Volume : PStorage_Volume; - OpenFile : PFATOpenFile; - Buffer : puint32; - Offset : uint32; - ByteCount : uint32; - BytesDone : uint32; - BytesOut : puint32; - Callback : TIOCallback; - CallbackData : pointer; - Cluster : uint32; - OrigByteSize : uint32; - DataStart : uint32; - BootRecord : PBootRecord; - ChunkBytes : uint32; - LastError : TError; - CursorValid : boolean; - CursorIndex : uint32; - CursorCluster: uint32; - ExtentValid : boolean; - ExtentIdx : uint32; - ExtentStartIdx : uint32; - end; - - { Async format state machine steps } - TFmtStep = ( - fmtBootSector, - fmtZeroFAT, - fmtFATEntries, - fmtRootDir, - fmtSystemDir, - fmtDone - ); - - PFmtContext = ^TFmtContext; - TFmtContext = record - Step : TFmtStep; - Volume : PStorage_Volume; - Disk : PStorage_Device; - SectorStart : uint32; - FATStart : uint32; - DataStart : uint32; - FATSize : uint32; - BatchPos : uint32; - BatchSize : uint32; - RootCluster : uint32; - SPC : uint32; - Buffer : puint32; - ZeroBuffer : puint32; - Callback : TIOCallback; - CallbackData : pointer; - end; + driver.storage.fs.fat32.old, + driver.storage.fs.fat32.core, + driver.storage.fs.fat32.transfer; var filesystem : TFilesystem; +procedure FAT32ReleaseVolumeState(volume : PStorage_Volume); +begin + FAT32LegacyReleaseVolumeCtx(volume); + FAT32ReleaseVolumeCtx(volume); +end; + procedure init; -procedure create_volume(volume : PStorage_Volume; sectors : uint32; start : uint32; config : puint32); -procedure create_volume_async(volume : PStorage_Volume; sectors : uint32; start : uint32; config : puint32; callback : TIOCallback; callbackData : pointer); -procedure detect_volumes(disk : PStorage_Device); -//function writeDirectory(volume : PStorage_volume; directory : pchar; attributes : uint32) : uint8; // need to handle parent table cluster overflow, need to take attributes -//function readDirectory(volume : PStorage_volume; directory : pchar; listPtr : PLinkedListBase) : uint8; //returns: 0 = success, 1 = dir not exsist, 2 = not directory, 3 = error - - -implementation - -{ ---- Module-level I/O caches ---- } -var - - { Sequential I/O chain-position cache: stores the last visited - (cluster-index, cluster-number) pair so the next call can resume - instead of walking the entire chain from cluster 0. } - sioc_vol : PStorage_Volume = nil; - sioc_fileClust : uint32 = 0; - sioc_chainIdx : uint32 = 0; - sioc_chainClust : uint32 = 0; - sioc_valid : boolean = false; - - { Free-cluster allocation hint: next findFreeClusters scan starts here - instead of cluster 2, making sequential allocations O(1). } - sioc_allocHint : uint32 = 2; - sioc_allocVol : PStorage_Volume = nil; - -{ Validate a FAT32 8.3 filename. - fullName should be in "NAME.EXT" form. - Returns true if valid: name part 1-8 chars, extension 0-3 chars, - no illegal characters. } -function isValidFAT32Name(fullName : pchar) : boolean; -var - i, fnLen, dotPos, nameLen, extLen : uint32; - c : char; begin - isValidFAT32Name := false; - if fullName = nil then exit; - fnLen := stringSize(fullName); - if fnLen = 0 then exit; - - { Find last dot position } - dotPos := fnLen; - for i := 0 to fnLen - 1 do begin - if fullName[i] = '.' then - dotPos := i; - end; - - nameLen := dotPos; - if dotPos < fnLen then - extLen := fnLen - dotPos - 1 - else - extLen := 0; - - { Name part must be 1-8 characters } - if (nameLen < 1) or (nameLen > 8) then exit; - { Extension part must be 0-3 characters } - if extLen > 3 then exit; - - { Check all characters for validity } - for i := 0 to fnLen - 1 do begin - c := fullName[i]; - if i = dotPos then continue; { skip the dot separator } - { Reject control characters (< 0x20) except we don't need to check 0 since stringSize stops there } - if uint8(c) < $20 then exit; - { Reject illegal FAT32 characters: " * / : < > ? \ | } - case c of - '"', '*', '/', ':', '<', '>', '?', '\', '|': exit; - end; - end; - - isValidFAT32Name := true; -end; - -function load(ptr : void) : boolean; -begin - io.syslog.logln('FAT32', 'LOADED.') -end; - -function matchExtension(dirExt : TFATExtArray; ext : pchar) : boolean; -var - j : uint32; - c : char; - extDone : boolean; -begin - matchExtension:= true; - extDone := false; - for j:=0 to 2 do begin - if (not extDone) and (ext <> nil) and (ext[j] <> char(0)) then begin - c:= ext[j]; - { Uppercase a-z to A-Z for case-insensitive FAT32 matching } - if (c >= 'a') and (c <= 'z') then - c := char(uint8(c) - 32); - end else begin - c:= ' '; - extDone := true; - end; - if dirExt[j] <> c then begin - matchExtension:= false; - break; - end; - end; -end; - -function cleanString(str : pchar; status : puint32) : byteArray8; -var - i : uint32; - ii: uint32; -begin - push_trace('cleanstring()'); - if str = nil then begin - for i:=0 to 7 do cleanString[i]:= ' '; - exit; - end; - for i:=0 to 7 do begin - if str[i] = char(0) then begin - for ii:=i to 7 do begin - cleanString[ii]:= ' '; - end; - break; - end else begin - { Uppercase a-z to A-Z for FAT32 } - if (str[i] >= 'a') and (str[i] <= 'z') then - cleanString[i]:= char(uint8(str[i]) - 32) - else - cleanString[i]:= str[i]; - end; - end; - -end; - -{ Split a full filename like 'README.TXT' into name and extension parts. - Caller must free nameOut and extOut. } -procedure splitFileNameParts(fullName : pchar; var nameOut : pchar; var extOut : pchar); -var - fnLen, dotPos, i : uint32; -begin - push_trace('driver.storage.fs.fat32.splitFileNameParts'); - nameOut := nil; - extOut := nil; - if fullName = nil then begin - nameOut := stringNew(0); - extOut := stringNew(0); - exit; - end; - fnLen := stringSize(fullName); - dotPos := fnLen; - for i := 0 to fnLen - 1 do begin - if fullName[i] = '.' then - dotPos := i; - end; - nameOut := stringTrim(fullName, dotPos); - if dotPos < fnLen then - extOut := stringCopy(pchar(@fullName[dotPos + 1])) - else - extOut := stringNew(0); -end; - -function cleanStringCha(str : pchar) : pchar; -var - i : uint32; - ii: uint32; -begin - cleanStringCha:= pchar(kalloc(10)); - memset(uint32(cleanstringcha), 0, 10); - push_trace('cleanstringcha'); - if str = nil then begin - for i:=0 to 7 do cleanStringCha[i]:= ' '; - exit; - end; - for i:=0 to 7 do begin - if str[i] = char(0) then begin - for ii:=i to 7 do begin - cleanStringCha[ii]:= ' '; - end; - break; - end else begin - push_trace('cleanstringcha1.2'); - cleanstringcha[i]:= str[i]; - - end; - end; - -end; - -function readBootRecord(volume : PStorage_volume) : PBootRecord; -var - buffer : puint32; -begin - buffer:= puint32(kalloc(512)); - memset(uint32(buffer), 0, 512); - driver.storage.mgr.storage_read(volume^.device, volume^.sectorStart + 1, 1, buffer); - readBootRecord:= PBootRecord(buffer); -end; - -{ ---- FAT sector cache ---- } - -{ Get or lazily create the per-volume FAT32 context. - Stores the context pointer in volume^.fsPrivate. } -function fatVolumeCtxGet(volume : PStorage_Volume) : PFATVolumeCtx; -var - ctx : PFATVolumeCtx; - br : PBootRecord; - i : uint32; - base : uint32; - tx : PFATFileIOAsyncCtx; -begin - fatVolumeCtxGet := nil; - if volume = nil then exit; - - if volume^.fsPrivate <> nil then begin - fatVolumeCtxGet := PFATVolumeCtx(volume^.fsPrivate); - exit; - end; - - ctx := PFATVolumeCtx(kalloc(sizeof(TFATVolumeCtx))); - if ctx = nil then exit; - memset(uint32(ctx), 0, sizeof(TFATVolumeCtx)); - - ctx^.Cache.Data := puint32(kalloc(FAT_CACHE_LINES * 512)); - if ctx^.Cache.Data = nil then begin - kfree(puint32(ctx)); - exit; - end; - memset(uint32(ctx^.Cache.Data), 0, FAT_CACHE_LINES * 512); - - ctx^.Cache.EvictBuf := puint32(kalloc(512)); - if ctx^.Cache.EvictBuf = nil then begin - kfree(ctx^.Cache.Data); - kfree(puint32(ctx)); - exit; - end; - memset(uint32(ctx^.Cache.EvictBuf), 0, 512); - - ctx^.Cache.Busy := false; - for i := 0 to FAT_CACHE_LINES - 1 do begin - ctx^.Cache.Lines[i].Valid := false; - ctx^.Cache.Lines[i].Dirty := false; - ctx^.Cache.Lines[i].LRU := uint8(i mod FAT_CACHE_WAYS); - end; - - br := readBootRecord(volume); - ctx^.Cache.BootRecord := br; - ctx^.Cache.FatStart := volume^.sectorStart + 1 + br^.rsvSectors; - ctx^.Cache.Device := volume^.device; - - ctx^.TransferPoolBuf := kalloc(FAT_TRANSFER_POOL_CAPACITY * sizeof(TFATFileIOAsyncCtx)); - if ctx^.TransferPoolBuf <> nil then begin - base := uint32(ctx^.TransferPoolBuf); - if FAT_TRANSFER_POOL_CAPACITY > 0 then - for i := 0 to FAT_TRANSFER_POOL_CAPACITY - 1 do begin - tx := PFATFileIOAsyncCtx(base + (i * sizeof(TFATFileIOAsyncCtx))); - tx^.Next := PFATFileIOAsyncCtx(ctx^.TransferFreeList); - tx^.VolumeCtx := ctx; - tx^.State := ftsIdle; - ctx^.TransferFreeList := pointer(tx); - end; - end; - - volume^.fsPrivate := pointer(ctx); - fatVolumeCtxGet := ctx; -end; - -{ Get or lazily create the per-volume FAT cache. } -function fatCacheGet(volume : PStorage_Volume) : PFATCache; -var - ctx : PFATVolumeCtx; -begin - ctx := fatVolumeCtxGet(volume); - if ctx = nil then - fatCacheGet := nil - else - fatCacheGet := @ctx^.Cache; -end; - -function fatTransferAlloc(volume : PStorage_Volume) : PFATFileIOAsyncCtx; -var - ctx : PFATVolumeCtx; - transfer : PFATFileIOAsyncCtx; -begin - fatTransferAlloc := nil; - ctx := fatVolumeCtxGet(volume); - if ctx = nil then exit; - - asm pushf; cli end; - transfer := PFATFileIOAsyncCtx(ctx^.TransferFreeList); - if transfer <> nil then - ctx^.TransferFreeList := pointer(transfer^.Next); - asm popf end; - - if transfer = nil then exit; - - memset(uint32(transfer), 0, sizeof(TFATFileIOAsyncCtx)); - transfer^.VolumeCtx := ctx; - transfer^.State := ftsIdle; - fatTransferAlloc := transfer; -end; - -procedure fatTransferFree(ctx : PFATFileIOAsyncCtx); -var - volCtx : PFATVolumeCtx; -begin - if ctx = nil then exit; - volCtx := ctx^.VolumeCtx; - if volCtx = nil then exit; - - asm pushf; cli end; - ctx^.Next := PFATFileIOAsyncCtx(volCtx^.TransferFreeList); - volCtx^.TransferFreeList := pointer(ctx); - asm popf end; -end; - -function fatWorkerQueuePush(volCtx : PFATVolumeCtx; transfer : PFATFileIOAsyncCtx) : boolean; -begin - fatWorkerQueuePush := false; - if (volCtx = nil) or (transfer = nil) then exit; - - asm pushf; cli end; - if volCtx^.WorkCount < FAT_WORK_QUEUE_CAPACITY then begin - volCtx^.WorkQueue[volCtx^.WorkTail] := transfer; - volCtx^.WorkTail := (volCtx^.WorkTail + 1) mod FAT_WORK_QUEUE_CAPACITY; - volCtx^.WorkCount := volCtx^.WorkCount + 1; - fatWorkerQueuePush := true; - end; - asm popf end; -end; - -function fatWorkerQueuePop(volCtx : PFATVolumeCtx; var transfer : PFATFileIOAsyncCtx) : boolean; -begin - fatWorkerQueuePop := false; - transfer := nil; - if volCtx = nil then exit; - - asm pushf; cli end; - if volCtx^.WorkCount > 0 then begin - transfer := volCtx^.WorkQueue[volCtx^.WorkHead]; - volCtx^.WorkQueue[volCtx^.WorkHead] := nil; - volCtx^.WorkHead := (volCtx^.WorkHead + 1) mod FAT_WORK_QUEUE_CAPACITY; - volCtx^.WorkCount := volCtx^.WorkCount - 1; - fatWorkerQueuePop := true; - end; - asm popf end; -end; - -procedure fatWorkerWake(volCtx : PFATVolumeCtx); -begin - if (volCtx = nil) or (volCtx^.Worker = nil) then exit; - - asm pushf; cli end; - volCtx^.Worker^.PendingMsg := smCustom; - volCtx^.Worker^.MsgData := nil; - if volCtx^.Worker^.State = psAwaiting then - volCtx^.Worker^.State := psReady; - asm popf end; -end; - -procedure fat32_fileio_async_run_next(ctx : PFATFileIOAsyncCtx); forward; - -procedure fat32_transfer_worker(pctx : proc.types.PProcessContext); -var - volCtx : PFATVolumeCtx; - transfer : PFATFileIOAsyncCtx; -begin - volCtx := PFATVolumeCtx(pctx^.Local); - if volCtx = nil then begin - proc.mgr.proc_exit(1); - exit; - end; - - while true do begin - if pctx^.PendingMsg = smTerminate then begin - pctx^.PendingMsg := smNone; - proc.mgr.proc_exit(0); - exit; - end; - - if fatWorkerQueuePop(volCtx, transfer) then begin - fat32_fileio_async_run_next(transfer); - continue; - end; - - asm pushf; cli end; - if volCtx^.WorkCount = 0 then begin - pctx^.PendingMsg := smNone; - pctx^.State := psAwaiting; - end; - asm popf end; - - if pctx^.State = psAwaiting then - proc.mgr.proc_await; - end; -end; - -procedure fatWorkerEnsure(volume : PStorage_Volume); -var - volCtx : PFATVolumeCtx; -begin - volCtx := fatVolumeCtxGet(volume); - if volCtx = nil then exit; - if volCtx^.Worker <> nil then exit; - volCtx^.Worker := proc.mgr.create('fat32.xfer', @fat32_transfer_worker, void(volCtx), 1); -end; - -function fatScheduleTransfer(ctx : PFATFileIOAsyncCtx) : boolean; -var - volCtx : PFATVolumeCtx; -begin - fatScheduleTransfer := false; - if ctx = nil then exit; - - volCtx := ctx^.VolumeCtx; - if volCtx = nil then exit; - if volCtx^.Worker = nil then exit; - - if fatWorkerQueuePush(volCtx, ctx) then begin - fatWorkerWake(volCtx); - fatScheduleTransfer := true; - end; -end; - -{ Flush all dirty cache lines to disk. - CLI/STI protects cache metadata; disk I/O runs with interrupts enabled. } -procedure fatCacheFlush(cache : PFATCache; volume : PStorage_Volume); -var - i : uint32; - lba : uint32; - needFlush : boolean; -begin - { Acquire Busy lock — protects EvictBuf and prevents concurrent evictions } - while true do begin - asm pushf; cli end; - if not cache^.Busy then begin - cache^.Busy := true; - asm popf end; - break; - end; - asm popf end; - asm hlt end; - end; - - for i := 0 to FAT_CACHE_LINES - 1 do begin - asm pushf; cli end; - needFlush := cache^.Lines[i].Valid and cache^.Lines[i].Dirty; - if needFlush then begin - lba := cache^.FatStart + cache^.Lines[i].SectorIdx; - cache^.Lines[i].Dirty := false; - core.util.memcpy(uint32(cache^.Data) + (i * 512), - uint32(cache^.EvictBuf), 512); - end; - asm popf end; - - if needFlush then - driver.storage.mgr.storage_write( - cache^.Device, lba, 1, cache^.EvictBuf); - end; - - { Release lock } - asm pushf; cli end; - cache^.Busy := false; - asm popf end; -end; - -{ Invalidate all cache lines (e.g. on unmount). } -procedure fatCacheInvalidate(cache : PFATCache); -var - i : uint32; -begin - for i := 0 to FAT_CACHE_LINES - 1 do begin - cache^.Lines[i].Valid := false; - cache^.Lines[i].Dirty := false; - cache^.Lines[i].LRU := uint8(i mod FAT_CACHE_WAYS); - end; -end; - -{ Return the per-volume cached boot record without any allocation. - The FAT cache lazily reads and keeps a single copy; callers MUST NOT kfree it. } -function getCachedBootRecord(volume : PStorage_Volume) : PBootRecord; -begin - getCachedBootRecord := fatCacheGet(volume)^.BootRecord; -end; - -{ Read a single FAT entry using the sector cache. - 128 FAT entries per 512-byte sector. 2-way set-associative: set = fatSector mod FAT_CACHE_SETS. - CLI/STI protects cache metadata; disk I/O runs with interrupts enabled. } -function readFat(volume : PStorage_volume; cluster : uint32; bootRecord : PBootRecord) : uint32; -var - cache : PFATCache; - fatSecIdx : uint32; { which FAT sector this cluster lives in } - setIdx : uint32; { cache set index } - baseSlot : uint32; { first line index of this set } - entryOff : uint32; { offset within that sector (0..127) } - dataPtr : puint32; { pointer into cache data for the hit/victim slot } - lba : uint32; - doEvict : boolean; - evictLBA : uint32; - w : uint32; { way iterator } - victim : uint32; { chosen victim slot index } -begin - cache := fatCacheGet(volume); - fatSecIdx := cluster div 128; - setIdx := fatSecIdx mod FAT_CACHE_SETS; - baseSlot := setIdx * FAT_CACHE_WAYS; - entryOff := cluster mod 128; - - { Fast path: cache hit under CLI — check both ways } - asm pushf; cli end; - for w := 0 to FAT_CACHE_WAYS - 1 do begin - if cache^.Lines[baseSlot + w].Valid and (cache^.Lines[baseSlot + w].SectorIdx = fatSecIdx) then begin - dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); - readFat := dataPtr[entryOff] and $0FFFFFFF; - { Update LRU: this way is MRU } - cache^.Lines[baseSlot + w].LRU := 0; - cache^.Lines[baseSlot + (1 - w)].LRU := 1; - asm popf end; - exit; - end; - end; - asm popf end; - - { Miss path: acquire Busy spinlock to serialize eviction+load } - while true do begin - asm pushf; cli end; - if not cache^.Busy then begin - { Re-check for hit — another process may have loaded this sector } - for w := 0 to FAT_CACHE_WAYS - 1 do begin - if cache^.Lines[baseSlot + w].Valid and (cache^.Lines[baseSlot + w].SectorIdx = fatSecIdx) then begin - dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); - readFat := dataPtr[entryOff] and $0FFFFFFF; - cache^.Lines[baseSlot + w].LRU := 0; - cache^.Lines[baseSlot + (1 - w)].LRU := 1; - asm popf end; - exit; - end; - end; - cache^.Busy := true; - { Pick victim: prefer invalid way, else LRU way } - victim := baseSlot; - if (not cache^.Lines[baseSlot].Valid) then - victim := baseSlot - else if (not cache^.Lines[baseSlot + 1].Valid) then - victim := baseSlot + 1 - else if cache^.Lines[baseSlot + 1].LRU >= cache^.Lines[baseSlot].LRU then - victim := baseSlot + 1 - else - victim := baseSlot; - dataPtr := puint32(uint32(cache^.Data) + (victim * 512)); - doEvict := cache^.Lines[victim].Valid and cache^.Lines[victim].Dirty; - if doEvict then begin - evictLBA := cache^.FatStart + cache^.Lines[victim].SectorIdx; - core.util.memcpy(uint32(dataPtr), uint32(cache^.EvictBuf), 512); - end; - cache^.Lines[victim].Valid := false; - asm popf end; - break; - end; - asm popf end; - asm hlt end; - end; - - { Disk I/O with interrupts enabled — exclusive via Busy flag } - if doEvict then - driver.storage.mgr.storage_write(cache^.Device, evictLBA, 1, cache^.EvictBuf); - lba := cache^.FatStart + fatSecIdx; - driver.storage.mgr.storage_read(cache^.Device, lba, 1, dataPtr); - - { Update cache metadata, set LRU, release lock under CLI } - asm pushf; cli end; - cache^.Lines[victim].SectorIdx := fatSecIdx; - cache^.Lines[victim].Valid := true; - cache^.Lines[victim].Dirty := false; - { New entry is MRU, the other way is LRU } - cache^.Lines[victim].LRU := 0; - if victim = baseSlot then - cache^.Lines[baseSlot + 1].LRU := 1 - else - cache^.Lines[baseSlot].LRU := 1; - readFat := dataPtr[entryOff] and $0FFFFFFF; - cache^.Busy := false; - asm popf end; -end; - -{ Write a single FAT entry into cache. No immediate disk I/O — call fatCacheFlush later. - 2-way set-associative: set = fatSector mod FAT_CACHE_SETS. - CLI/STI protects cache metadata; disk I/O runs with interrupts enabled. } -procedure writeFat(volume : PStorage_volume; cluster : uint32; value : uint32; bootRecord : PBootRecord); -var - cache : PFATCache; - fatSecIdx : uint32; - setIdx : uint32; - baseSlot : uint32; - entryOff : uint32; - dataPtr : puint32; - lba : uint32; - doEvict : boolean; - evictLBA : uint32; - w : uint32; - victim : uint32; -begin - cache := fatCacheGet(volume); - fatSecIdx := cluster div 128; - setIdx := fatSecIdx mod FAT_CACHE_SETS; - baseSlot := setIdx * FAT_CACHE_WAYS; - entryOff := cluster mod 128; - - { Fast path: cache hit — check both ways } - asm pushf; cli end; - for w := 0 to FAT_CACHE_WAYS - 1 do begin - if cache^.Lines[baseSlot + w].Valid and (cache^.Lines[baseSlot + w].SectorIdx = fatSecIdx) then begin - dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); - if value = 0 then - dataPtr[entryOff] := 0 - else - dataPtr[entryOff] := (dataPtr[entryOff] and $F0000000) or (value and $0FFFFFFF); - cache^.Lines[baseSlot + w].Dirty := true; - { Update LRU: this way is MRU } - cache^.Lines[baseSlot + w].LRU := 0; - cache^.Lines[baseSlot + (1 - w)].LRU := 1; - asm popf end; - exit; - end; - end; - asm popf end; - - { Miss path: acquire Busy spinlock } - while true do begin - asm pushf; cli end; - if not cache^.Busy then begin - { Re-check for hit after acquiring lock } - for w := 0 to FAT_CACHE_WAYS - 1 do begin - if cache^.Lines[baseSlot + w].Valid and (cache^.Lines[baseSlot + w].SectorIdx = fatSecIdx) then begin - dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); - if value = 0 then - dataPtr[entryOff] := 0 - else - dataPtr[entryOff] := (dataPtr[entryOff] and $F0000000) or (value and $0FFFFFFF); - cache^.Lines[baseSlot + w].Dirty := true; - cache^.Lines[baseSlot + w].LRU := 0; - cache^.Lines[baseSlot + (1 - w)].LRU := 1; - asm popf end; - exit; - end; - end; - cache^.Busy := true; - { Pick victim: prefer invalid way, else LRU way } - victim := baseSlot; - if (not cache^.Lines[baseSlot].Valid) then - victim := baseSlot - else if (not cache^.Lines[baseSlot + 1].Valid) then - victim := baseSlot + 1 - else if cache^.Lines[baseSlot + 1].LRU >= cache^.Lines[baseSlot].LRU then - victim := baseSlot + 1 - else - victim := baseSlot; - dataPtr := puint32(uint32(cache^.Data) + (victim * 512)); - doEvict := cache^.Lines[victim].Valid and cache^.Lines[victim].Dirty; - if doEvict then begin - evictLBA := cache^.FatStart + cache^.Lines[victim].SectorIdx; - core.util.memcpy(uint32(dataPtr), uint32(cache^.EvictBuf), 512); - end; - cache^.Lines[victim].Valid := false; - asm popf end; - break; - end; - asm popf end; - asm hlt end; - end; - - { Disk I/O with interrupts enabled — exclusive via Busy flag } - if doEvict then - driver.storage.mgr.storage_write(cache^.Device, evictLBA, 1, cache^.EvictBuf); - lba := cache^.FatStart + fatSecIdx; - driver.storage.mgr.storage_read(cache^.Device, lba, 1, dataPtr); - - { Update cache, write value, set LRU, release lock under CLI } - asm pushf; cli end; - cache^.Lines[victim].SectorIdx := fatSecIdx; - cache^.Lines[victim].Valid := true; - cache^.Lines[victim].Dirty := false; - if value = 0 then - dataPtr[entryOff] := 0 - else - dataPtr[entryOff] := (dataPtr[entryOff] and $F0000000) or (value and $0FFFFFFF); - cache^.Lines[victim].Dirty := true; - cache^.Lines[victim].LRU := 0; - if victim = baseSlot then - cache^.Lines[baseSlot + 1].LRU := 1 - else - cache^.Lines[baseSlot].LRU := 1; - cache^.Busy := false; - asm popf end; -end; - -function getFatChain(volume : PStorage_volume; cluster : uint32; bootRecord : PBootRecord) : PLinkedListBase; -var - currentCluster : uint32; - currentClusterValue : uint32; - clusters : PLinkedListBase; - dirElm : puint32; - maxCluster : uint32; - iterCount : uint32; -begin - clusters:= LL_New(sizeof(uint32)); - currentCluster:= cluster; - currentClusterValue:= cluster; - if bootRecord^.sectorSize > 0 then - maxCluster := (bootRecord^.FATSize * bootRecord^.sectorSize) div 4 - else - maxCluster := 0; - iterCount := 0; - - while true do begin - if (maxCluster > 0) and (iterCount >= maxCluster) then - break; - currentClusterValue:= readFat(volume, currentClusterValue, bootRecord); - - if (currentClusterValue and $0FFFFFFF) = $0FFFFFF7 then begin - break; - end else if (currentClusterValue and $0FFFFFFF) >= $0FFFFFF8 then begin - dirElm:= LL_add(clusters); - dirElm^:= currentCluster; - break; - end else if currentClusterValue = 0 then begin - break; - end else begin - dirElm:= LL_add(clusters); - dirElm^:= currentCluster; - end; - - currentCluster := currentClusterValue; - iterCount := iterCount + 1; - end; - - getFatChain:= clusters; -end; - -function fatMaxClusterCount(bootRecord : PBootRecord) : uint32; -begin - if (bootRecord <> nil) and (bootRecord^.sectorSize > 0) then - fatMaxClusterCount := (bootRecord^.FATSize * bootRecord^.sectorSize) div 4 - else - fatMaxClusterCount := 0; -end; - -function fatDefaultSPC(sectors : uint32; sectorSize : uint32) : uint32; -begin - if sectorSize = 0 then begin - fatDefaultSPC := 1; - exit; - end; - - { Default to 4 KiB clusters for sane FAT32 throughput without excessive - slack on small files. Callers can still override via config. } - fatDefaultSPC := 4096 div sectorSize; - if fatDefaultSPC = 0 then - fatDefaultSPC := 1; - if fatDefaultSPC > 64 then - fatDefaultSPC := 64; -end; - -function fatComputeFATSize(sectors : uint32; spc : uint32; sectorSize : uint32) : uint32; -var - clusterCount : uint32; - fatBytes : uint32; -begin - if spc = 0 then - spc := 1; - if sectorSize = 0 then begin - fatComputeFATSize := 0; - exit; - end; - - clusterCount := (sectors + spc - 1) div spc; - fatBytes := clusterCount * 4; - fatComputeFATSize := (fatBytes + sectorSize - 1) div sectorSize; - if fatComputeFATSize = 0 then - fatComputeFATSize := 1; -end; - -function fatClustersForBytes(byteCount : uint32; bytesPerCluster : uint32) : uint32; -begin - if (byteCount = 0) or (bytesPerCluster = 0) then begin - fatClustersForBytes := 0; - exit; - end; - - fatClustersForBytes := (byteCount + bytesPerCluster - 1) div bytesPerCluster; -end; - -function fatPreallocClusterCount(bytesPerCluster : uint32) : uint32; -begin - if bytesPerCluster = 0 then begin - fatPreallocClusterCount := 1; - exit; - end; - - fatPreallocClusterCount := FAT_WRITE_PREALLOC_BYTES div bytesPerCluster; - if fatPreallocClusterCount = 0 then - fatPreallocClusterCount := 1; - if fatPreallocClusterCount < FAT_WRITE_PREALLOC_MIN_CLUSTERS then - fatPreallocClusterCount := FAT_WRITE_PREALLOC_MIN_CLUSTERS; - if fatPreallocClusterCount > FAT_WRITE_PREALLOC_MAX_CLUSTERS then - fatPreallocClusterCount := FAT_WRITE_PREALLOC_MAX_CLUSTERS; -end; - -procedure fatUpdateAllocHint(volume : PStorage_volume; bootRecord : PBootRecord; nextCluster : uint32); -var - maxCluster : uint32; -begin - maxCluster := fatMaxClusterCount(bootRecord); - if maxCluster <= 2 then begin - sioc_allocHint := 2; - sioc_allocVol := volume; - exit; - end; - - if nextCluster < 2 then - nextCluster := 2; - if nextCluster >= maxCluster then - nextCluster := 2; - - sioc_allocHint := nextCluster; - sioc_allocVol := volume; -end; - -function fatTryContiguousRange(volume : PStorage_volume; bootRecord : PBootRecord; - rangeStart : uint32; rangeEnd : uint32; needed : uint32; - var foundStart : uint32) : boolean; -var - scanPos : uint32; - runLen : uint32; - runPos : uint32; - fatVal : uint32; -begin - fatTryContiguousRange := false; - if (needed = 0) or (bootRecord = nil) or (rangeStart >= rangeEnd) then - exit; - - scanPos := rangeStart; - runLen := 0; - runPos := 0; - - while scanPos < rangeEnd do begin - fatVal := readFat(volume, scanPos, bootRecord); - if fatVal = 0 then begin - if runLen = 0 then - runPos := scanPos; - runLen := runLen + 1; - if runLen >= needed then begin - foundStart := runPos; - fatTryContiguousRange := true; - exit; - end; - end else - runLen := 0; - scanPos := scanPos + 1; - end; -end; - -function fatFindContiguousFreeRange(volume : PStorage_volume; amount : uint32; - bootRecord : PBootRecord; var runStart : uint32) : boolean; -var - scanStart : uint32; - maxCluster : uint32; -begin - fatFindContiguousFreeRange := false; - if amount = 0 then exit; - - maxCluster := fatMaxClusterCount(bootRecord); - if maxCluster <= 2 then exit; - - if (sioc_allocVol = volume) and (sioc_allocHint >= 2) and (sioc_allocHint < maxCluster) then - scanStart := sioc_allocHint - else - scanStart := 2; - - if fatTryContiguousRange(volume, bootRecord, scanStart, maxCluster, amount, runStart) or - ((scanStart > 2) and fatTryContiguousRange(volume, bootRecord, 2, scanStart, amount, runStart)) then - fatFindContiguousFreeRange := true; -end; - -procedure fatLinkContiguousRange(volume : PStorage_volume; bootRecord : PBootRecord; - previousTail : uint32; firstCluster : uint32; clusterCount : uint32; - var lastCluster : uint32); -var - cur : uint32; - i : uint32; -begin - lastCluster := previousTail; - if clusterCount = 0 then exit; - - if previousTail <> 0 then - writeFat(volume, previousTail, firstCluster, bootRecord); - - cur := firstCluster; - i := 1; - while i < clusterCount do begin - writeFat(volume, cur, cur + 1, bootRecord); - cur := cur + 1; - i := i + 1; - end; - - writeFat(volume, cur, $FFFFFFF8, bootRecord); - lastCluster := cur; - fatUpdateAllocHint(volume, bootRecord, lastCluster + 1); -end; - -function fatAppendExtent(extents : PLinkedListBase; firstCluster : uint32; - clusterCount : uint32) : boolean; -var - ext : PClusterExtent; - lastExt : PClusterExtent; - extCount : uint32; -begin - fatAppendExtent := false; - if (extents = nil) or (clusterCount = 0) then - exit; - - extCount := LL_Size(extents); - if extCount > 0 then begin - lastExt := PClusterExtent(LL_Get(extents, extCount - 1)); - if (lastExt <> nil) and ((lastExt^.StartCluster + lastExt^.ClusterCount) = firstCluster) then begin - lastExt^.ClusterCount := lastExt^.ClusterCount + clusterCount; - fatAppendExtent := true; - exit; - end; - end; - - ext := PClusterExtent(LL_Add(extents)); - if ext = nil then - exit; - ext^.StartCluster := firstCluster; - ext^.ClusterCount := clusterCount; - fatAppendExtent := true; -end; - -function fatBuildExtentList(volume : PStorage_volume; cluster : uint32; - bootRecord : PBootRecord) : PLinkedListBase; -var - extents : PLinkedListBase; - current : uint32; - nextCluster : uint32; - runStart : uint32; - runCount : uint32; - maxCluster : uint32; - iterCount : uint32; -begin - fatBuildExtentList := nil; - if (bootRecord = nil) or (cluster < 2) then - exit; - - extents := LL_New(sizeof(TClusterExtent)); - if extents = nil then - exit; - - if bootRecord^.sectorSize > 0 then - maxCluster := (bootRecord^.FATSize * bootRecord^.sectorSize) div 4 - else - maxCluster := 0; - - current := cluster; - runStart := cluster; - runCount := 1; - iterCount := 0; - - while true do begin - if (maxCluster > 0) and (iterCount >= maxCluster) then - break; - - nextCluster := readFat(volume, current, bootRecord); - if (nextCluster = 0) or ((nextCluster and $0FFFFFFF) = $0FFFFFF7) then - break; - - if (nextCluster and $0FFFFFFF) >= $0FFFFFF8 then begin - if not fatAppendExtent(extents, runStart, runCount) then begin - LL_Free(extents); - exit; - end; - fatBuildExtentList := extents; - exit; - end; - - if nextCluster = (current + 1) then - runCount := runCount + 1 - else begin - if not fatAppendExtent(extents, runStart, runCount) then begin - LL_Free(extents); - exit; - end; - runStart := nextCluster; - runCount := 1; - end; - - current := nextCluster; - iterCount := iterCount + 1; - end; - - if runCount > 0 then begin - if not fatAppendExtent(extents, runStart, runCount) then begin - LL_Free(extents); - exit; - end; - end; - - fatBuildExtentList := extents; -end; - -function fatLocateExtentRun(extents : PLinkedListBase; targetIdx : uint32; - hintValid : boolean; hintExtentIdx : uint32; - hintStartIdx : uint32; var runStartCluster : uint32; - var runClusterCount : uint32; var outExtentIdx : uint32; - var outExtentStartIdx : uint32) : boolean; -var - i : uint32; - clusterIx : uint32; - extCount : uint32; - ext : PClusterExtent; -begin - fatLocateExtentRun := false; - if extents = nil then - exit; - - extCount := LL_Size(extents); - if extCount = 0 then - exit; - - if hintValid and (hintExtentIdx < extCount) and (hintStartIdx <= targetIdx) then begin - i := hintExtentIdx; - clusterIx := hintStartIdx; - end else begin - i := 0; - clusterIx := 0; - end; - - while i < extCount do begin - ext := PClusterExtent(LL_Get(extents, i)); - if ext = nil then - exit; - - if targetIdx < (clusterIx + ext^.ClusterCount) then begin - runStartCluster := ext^.StartCluster + (targetIdx - clusterIx); - runClusterCount := ext^.ClusterCount - (targetIdx - clusterIx); - outExtentIdx := i; - outExtentStartIdx := clusterIx; - fatLocateExtentRun := true; - exit; - end; - - clusterIx := clusterIx + ext^.ClusterCount; - i := i + 1; - end; -end; - -function findFreeClusterExtents(volume : PStorage_volume; amount : uint32; - bootRecord : PBootRecord) : PLinkedListBase; -var - maxCluster : uint32; - scanStart : uint32; - currentAmount : uint32; - lastAllocated : uint32; - extents : PLinkedListBase; - - procedure appendExtent(firstCluster : uint32; clusterCount : uint32); - var - ext : PClusterExtent; - begin - ext := PClusterExtent(LL_Add(extents)); - ext^.StartCluster := firstCluster; - ext^.ClusterCount := clusterCount; - currentAmount := currentAmount + clusterCount; - lastAllocated := firstCluster + clusterCount - 1; - end; - - procedure scanRange(rangeStart : uint32; rangeEnd : uint32); - var - scanPos : uint32; - runStart : uint32; - runLen : uint32; - takeCount : uint32; - fatVal : uint32; - begin - scanPos := rangeStart; - while (scanPos < rangeEnd) and (currentAmount < amount) do begin - fatVal := readFat(volume, scanPos, bootRecord); - if fatVal <> 0 then begin - scanPos := scanPos + 1; - continue; - end; - - runStart := scanPos; - runLen := 0; - while scanPos < rangeEnd do begin - fatVal := readFat(volume, scanPos, bootRecord); - if fatVal <> 0 then - break; - runLen := runLen + 1; - scanPos := scanPos + 1; - end; - - takeCount := amount - currentAmount; - if runLen < takeCount then - takeCount := runLen; - appendExtent(runStart, takeCount); - end; - end; -begin - extents := LL_New(sizeof(TClusterExtent)); - currentAmount := 0; - lastAllocated := 0; - - if amount = 0 then begin - findFreeClusterExtents := extents; - exit; - end; - - maxCluster := fatMaxClusterCount(bootRecord); - if maxCluster <= 2 then begin - LL_Free(extents); - findFreeClusterExtents := nil; - exit; - end; - - if (sioc_allocVol = volume) and (sioc_allocHint >= 2) and (sioc_allocHint < maxCluster) then - scanStart := sioc_allocHint - else - scanStart := 2; - - scanRange(scanStart, maxCluster); - if (currentAmount < amount) and (scanStart > 2) then - scanRange(2, scanStart); - - if currentAmount < amount then begin - LL_Free(extents); - findFreeClusterExtents := nil; - exit; - end; - - fatUpdateAllocHint(volume, bootRecord, lastAllocated + 1); - findFreeClusterExtents := extents; -end; - -function compareByteArray8(str1 : byteArray8; str2 : byteArray8) : boolean; -var - i : uint32; -begin - push_trace('driver.storage.fs.fat32.compareArray'); - compareByteArray8:= true; - for i:=0 to 7 do begin - if str1[i] <> str2[i] then begin - compareByteArray8:= false; - break; - end; - end; -end; - -{ Extract the starting cluster number from a directory entry. } -function dirFirstCluster(dir : PDirectory) : uint32; -begin - dirFirstCluster := uint32(dir^.clusterLow) or uint32(dir^.clusterHigh shl 16); -end; - -{ Set the starting cluster number in a directory entry. } -procedure setDirFirstCluster(dir : PDirectory; cluster : uint32); -begin - dir^.clusterLow := uint16(cluster and $FFFF); - dir^.clusterHigh := uint16((cluster shr 16) and $FFFF); -end; - -{ Calculate the LBA of the first data sector on a FAT32 volume. } -function fatDataStartLBA(volume : PStorage_Volume; bootRecord : PBootRecord) : uint32; -begin - fatDataStartLBA := volume^.sectorStart + 1 + bootRecord^.rsvSectors + bootRecord^.FATSize; -end; - -{ Convert a cluster number to its starting LBA. } -function clusterToLBA(dataStart : uint32; spc : uint32; cluster : uint32) : uint32; -begin - clusterToLBA := dataStart + (cluster * spc); -end; - -{ Write an 8.3 extension into a TFATExtArray: uppercase, pad with spaces. } -procedure fillFatExt(var dest : TFATExtArray; extPart : pchar); -var - j : uint32; -begin - dest[0] := ' '; - dest[1] := ' '; - dest[2] := ' '; - if (extPart <> nil) and (extPart[0] <> char(0)) then begin - for j := 0 to 2 do begin - if extPart[j] = char(0) then break; - if (extPart[j] >= 'a') and (extPart[j] <= 'z') then - dest[j] := char(uint8(extPart[j]) - 32) - else - dest[j] := extPart[j]; - end; - end; -end; - -{ Search a single sector buffer for a directory entry matching name+ext. - Returns the index (0..entriesPerSec-1), or -1 if not found. - Sets hitEnd to true if a char(0) sentinel was reached (end of directory). } -function findEntryInSector(buffer : puint32; entriesPerSec : uint32; - name : byteArray8; ext : pchar; var hitEnd : boolean) : integer; -var - idx : uint32; - dir : PDirectory; -begin - findEntryInSector := -1; - hitEnd := false; - if entriesPerSec > 0 then - for idx := 0 to entriesPerSec - 1 do begin - dir := @PDirectory(buffer)[idx]; - if dir^.fileName[0] = char(0) then begin - hitEnd := true; - exit; - end; - if dir^.fileName[0] = char($E5) then continue; - if compareByteArray8(dir^.fileName, name) - and matchExtension(dir^.fileExtension, ext) then begin - findEntryInSector := integer(idx); - exit; - end; - end; -end; - -{ Search a single sector buffer for a free directory entry (char(0) or $E5). - Returns the index (0..entriesPerSec-1), or -1 if sector is full. } -function findFreeEntryInSector(buffer : puint32; entriesPerSec : uint32) : integer; -var - idx : uint32; - dir : PDirectory; -begin - findFreeEntryInSector := -1; - if entriesPerSec > 0 then - for idx := 0 to entriesPerSec - 1 do begin - dir := @PDirectory(buffer)[idx]; - if (dir^.fileName[0] = char(0)) or (dir^.fileName[0] = char($E5)) then begin - findFreeEntryInSector := integer(idx); - exit; - end; - end; -end; - -{ --- Higher-level helpers --- } - -type - TDirEntryLocation = record - Found : boolean; - SectorLBA : uint32; - EntryIdx : uint32; - end; - - TFileLookup = record - BootRecord : PBootRecord; - Dirs : PLinkedListBase; - Dir : PDirectory; - Exists : boolean; - Status : puint32; - end; - - TRunInfo = record - StartCluster : uint32; - LastCluster : uint32; - RunLen : uint32; - RunSectors : uint32; - RunBytes : uint32; - LBA : uint32; - end; - -{ Locate a named entry in the parent directory's raw sectors. - Walks the full FAT chain of parentCluster, scanning each sector - with findEntryInSector. On match, fills loc and leaves buffer - containing the matching sector (caller must kfree buffer). - Returns true if found. } -function locateDirEntry( - volume : PStorage_Volume; - parentCluster : uint32; - bootRecord : PBootRecord; - cleanName : byteArray8; - ext : pchar; - var loc : TDirEntryLocation; - buffer : puint32 -) : boolean; -var - dataStart : uint32; - spc : uint32; - entriesPerSector : uint32; - dirChain : PLinkedListBase; - dc : uint32; - ds : uint32; - curCluster : uint32; - clusterLBA : uint32; - hitEnd : boolean; - entryIdx : integer; - done : boolean; -begin - locateDirEntry := false; - loc.Found := false; - - dataStart := fatDataStartLBA(volume, bootRecord); - spc := uint32(bootRecord^.spc); - entriesPerSector := uint32(bootRecord^.sectorSize) div uint32(sizeof(TDirectory)); - dirChain := getFatChain(volume, parentCluster, bootRecord); - done := false; - - dc := 0; - while (dc < LL_size(dirChain)) and (not done) do begin - curCluster := puint32(LL_Get(dirChain, dc))^; - clusterLBA := clusterToLBA(dataStart, spc, curCluster); - ds := 0; - while (ds < spc) and (not done) do begin - driver.storage.mgr.storage_read(volume^.device, clusterLBA + ds, 1, buffer); - entryIdx := findEntryInSector(buffer, entriesPerSector, cleanName, ext, hitEnd); - if hitEnd then - done := true - else if entryIdx >= 0 then begin - loc.Found := true; - loc.SectorLBA := clusterLBA + ds; - loc.EntryIdx := uint32(entryIdx); - done := true; - end; - ds := ds + 1; - end; - dc := dc + 1; - end; - - LL_Free(dirChain); - locateDirEntry := loc.Found; -end; - -{ Locate an unused (free) directory entry slot in the parent directory. - Returns true if a free slot was found, filling loc and leaving buffer - containing the sector. } -function locateFreeDirEntry( - volume : PStorage_Volume; - parentCluster : uint32; - bootRecord : PBootRecord; - var loc : TDirEntryLocation; - buffer : puint32 -) : boolean; -var - dataStart : uint32; - spc : uint32; - entriesPerSector : uint32; - dirChain : PLinkedListBase; - dc : uint32; - ds : uint32; - curCluster : uint32; - clusterLBA : uint32; - entryIdx : integer; - found : boolean; -begin - locateFreeDirEntry := false; - loc.Found := false; - - dataStart := fatDataStartLBA(volume, bootRecord); - spc := uint32(bootRecord^.spc); - entriesPerSector := uint32(bootRecord^.sectorSize) div uint32(sizeof(TDirectory)); - dirChain := getFatChain(volume, parentCluster, bootRecord); - found := false; - - if LL_size(dirChain) > 0 then begin - for dc := 0 to LL_size(dirChain) - 1 do begin - curCluster := puint32(LL_Get(dirChain, dc))^; - clusterLBA := clusterToLBA(dataStart, spc, curCluster); - for ds := 0 to spc - 1 do begin - driver.storage.mgr.storage_read(volume^.device, clusterLBA + ds, 1, buffer); - entryIdx := findFreeEntryInSector(buffer, entriesPerSector); - if entryIdx >= 0 then begin - loc.Found := true; - loc.SectorLBA := clusterLBA + ds; - loc.EntryIdx := uint32(entryIdx); - found := true; - end; - if found then break; - end; - if found then break; - end; - end; - - LL_Free(dirChain); - locateFreeDirEntry := loc.Found; -end; - -{ Build a contiguous cluster run starting at startCluster. - Scans ahead via readFat, accumulating consecutive clusters up to - maxRunSectors worth of sectors. - Returns the number of clusters in the run (runLen). - Sets lastCluster to the last cluster in the run (= startCluster + runLen - 1). } -function buildContiguousRun( - volume : PStorage_Volume; - startCluster : uint32; - spc : uint32; - maxRunSectors : uint32; - bootRecord : PBootRecord; - var runLen : uint32; - var lastCluster : uint32 -) : uint32; -var - nextFat : uint32; -begin - runLen := 1; - lastCluster := startCluster; - - while (runLen * spc) < maxRunSectors do begin - nextFat := readFat(volume, lastCluster, bootRecord); - if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then - break; - if nextFat <> lastCluster + 1 then - break; - lastCluster := nextFat; - runLen := runLen + 1; - end; - - buildContiguousRun := runLen; -end; - -procedure updateSIOC( - volume : PStorage_Volume; - fileStartCluster : uint32; - chainIdx : uint32; - chainCluster : uint32 -); forward; - -function seekToClusterIndex( - volume : PStorage_Volume; - fileStartCluster : uint32; - targetIdx : uint32; - bootRecord : PBootRecord; - var curCluster : uint32; - var chainPos : uint32 -) : boolean; forward; - -function seekToClusterIndexHinted( - volume : PStorage_Volume; - fileStartCluster : uint32; - targetIdx : uint32; - bootRecord : PBootRecord; - hintValid : boolean; - hintIdx : uint32; - hintCluster : uint32; - var curCluster : uint32; - var chainPos : uint32 -) : boolean; forward; - -procedure fat32_fileio_async_complete(ctx : PFATFileIOAsyncCtx; error : TError); -begin - if ctx = nil then exit; - if ctx^.BytesOut <> nil then - ctx^.BytesOut^ := ctx^.BytesDone; - if ctx^.Callback <> nil then - ctx^.Callback(error, ctx^.CallbackData); - fatTransferFree(ctx); -end; - -function fat32_fileio_prepare_single_run(ctx : PFATFileIOAsyncCtx; - var sectorLBA : uint32; var chunkSectors : uint32) : boolean; -const - MAX_RUN_SECTORS = 2048; -var - run : TRunInfo; - curCluster : uint32; - chainPos : uint32; - currentOffset : uint32; - bytesPerCluster : uint32; - startClusterIdx : uint32; - inClusterOffset : uint32; - remaining : uint32; - skipBytes : uint32; - copyBytes : uint32; - secSize : uint32; - spc : uint32; - lastBytePos : uint32; - lastClusterUsed : uint32; - lastClusterIdx : uint32; - maxRunClusters : uint32; - extentIdx : uint32; - extentStartIdx : uint32; -begin - fat32_fileio_prepare_single_run := false; - if ctx = nil then - exit; - - currentOffset := ctx^.Offset + ctx^.BytesDone; - remaining := ctx^.ByteCount - ctx^.BytesDone; - spc := uint32(ctx^.BootRecord^.spc); - secSize := uint32(ctx^.BootRecord^.sectorSize); - bytesPerCluster := spc * secSize; - startClusterIdx := currentOffset div bytesPerCluster; - inClusterOffset := currentOffset mod bytesPerCluster; - - maxRunClusters := MAX_RUN_SECTORS div spc; - if maxRunClusters = 0 then - maxRunClusters := 1; - - if (ctx^.OpenFile <> nil) and (ctx^.OpenFile^.Extents <> nil) and - fatLocateExtentRun(ctx^.OpenFile^.Extents, startClusterIdx, - ctx^.ExtentValid, ctx^.ExtentIdx, ctx^.ExtentStartIdx, - run.StartCluster, run.RunLen, extentIdx, extentStartIdx) then begin - if run.RunLen > maxRunClusters then - run.RunLen := maxRunClusters; - run.LastCluster := run.StartCluster + run.RunLen - 1; - ctx^.ExtentValid := true; - ctx^.ExtentIdx := extentIdx; - ctx^.ExtentStartIdx := extentStartIdx; - curCluster := run.LastCluster; - chainPos := startClusterIdx + run.RunLen - 1; - end else begin - if not seekToClusterIndexHinted( - ctx^.Volume, ctx^.Cluster, startClusterIdx, ctx^.BootRecord, - ctx^.CursorValid, ctx^.CursorIndex, ctx^.CursorCluster, - curCluster, chainPos) then - exit; - - run.StartCluster := curCluster; - buildContiguousRun(ctx^.Volume, curCluster, spc, MAX_RUN_SECTORS, ctx^.BootRecord, run.RunLen, run.LastCluster); - end; - - run.RunSectors := run.RunLen * spc; - run.RunBytes := run.RunSectors * secSize; - run.LBA := clusterToLBA(ctx^.DataStart, spc, run.StartCluster); - - skipBytes := inClusterOffset; - copyBytes := run.RunBytes - skipBytes; - if copyBytes > remaining then - copyBytes := remaining; - - if (copyBytes = 0) or ((copyBytes mod secSize) <> 0) or ((skipBytes mod secSize) <> 0) then - exit; - - sectorLBA := run.LBA + (skipBytes div secSize); - chunkSectors := copyBytes div secSize; - ctx^.ChunkBytes := copyBytes; - - lastBytePos := skipBytes + copyBytes - 1; - lastClusterIdx := startClusterIdx + (lastBytePos div bytesPerCluster); - lastClusterUsed := run.StartCluster + (lastBytePos div bytesPerCluster); - updateSIOC(ctx^.Volume, ctx^.Cluster, lastClusterIdx, lastClusterUsed); - ctx^.CursorValid := true; - ctx^.CursorIndex := lastClusterIdx; - ctx^.CursorCluster := lastClusterUsed; - - fat32_fileio_prepare_single_run := true; -end; - -procedure fat32_fileio_direct_complete(error : TError; userdata : pointer); -var - ctx : PFATFileIOAsyncCtx; -begin - ctx := PFATFileIOAsyncCtx(userdata); - if ctx = nil then exit; - - if error = eNone then begin - ctx^.BytesDone := ctx^.BytesDone + ctx^.ChunkBytes; - if (ctx^.Mode = famWrite) and (ctx^.OpenFile <> nil) then begin - if (ctx^.BytesDone > 0) and ((ctx^.Offset + ctx^.BytesDone) > ctx^.OpenFile^.ByteSize) then begin - ctx^.OpenFile^.ByteSize := ctx^.Offset + ctx^.BytesDone; - ctx^.OpenFile^.MetaDirty := true; - end; - if (ctx^.OpenFile^.FirstCluster = 0) and (ctx^.Cluster <> 0) then begin - ctx^.OpenFile^.FirstCluster := ctx^.Cluster; - ctx^.OpenFile^.Exists := true; - ctx^.OpenFile^.MetaDirty := true; - end; - end; - end; - - fat32_fileio_async_complete(ctx, error); -end; - -procedure fat32_fileio_async_step(error : TError; userdata : pointer); forward; - -procedure fat32_fileio_async_run_next(ctx : PFATFileIOAsyncCtx); -const - MAX_RUN_SECTORS = 2048; -var - run : TRunInfo; - curCluster : uint32; - chainPos : uint32; - currentOffset : uint32; - bytesPerCluster : uint32; - startClusterIdx : uint32; - inClusterOffset : uint32; - remaining : uint32; - skipBytes : uint32; - copyBytes : uint32; - secSize : uint32; - spc : uint32; - sectorLBA : uint32; - chunkSectors : uint32; - lastBytePos : uint32; - lastClusterUsed : uint32; - lastClusterIdx : uint32; - maxRunClusters : uint32; - extentIdx : uint32; - extentStartIdx : uint32; - err : TError; -begin - if ctx = nil then exit; - - if ctx^.State = ftsAwaitIO then begin - if ctx^.LastError <> eNone then begin - ctx^.State := ftsError; - fat32_fileio_async_complete(ctx, ctx^.LastError); - exit; - end; - - ctx^.State := ftsAdvance; - ctx^.BytesDone := ctx^.BytesDone + ctx^.ChunkBytes; - - if (ctx^.Mode = famWrite) and (ctx^.OpenFile <> nil) then begin - if (ctx^.BytesDone > 0) and ((ctx^.Offset + ctx^.BytesDone) > ctx^.OpenFile^.ByteSize) then begin - ctx^.OpenFile^.ByteSize := ctx^.Offset + ctx^.BytesDone; - ctx^.OpenFile^.MetaDirty := true; - end; - if (ctx^.OpenFile^.FirstCluster = 0) and (ctx^.Cluster <> 0) then begin - ctx^.OpenFile^.FirstCluster := ctx^.Cluster; - ctx^.OpenFile^.Exists := true; - ctx^.OpenFile^.MetaDirty := true; - end; - end; - end; - - if ctx^.BytesDone >= ctx^.ByteCount then begin - ctx^.State := ftsComplete; - fat32_fileio_async_complete(ctx, eNone); - exit; - end; - - ctx^.State := ftsPrepare; - - currentOffset := ctx^.Offset + ctx^.BytesDone; - remaining := ctx^.ByteCount - ctx^.BytesDone; - spc := uint32(ctx^.BootRecord^.spc); - secSize := uint32(ctx^.BootRecord^.sectorSize); - bytesPerCluster := spc * secSize; - startClusterIdx := currentOffset div bytesPerCluster; - inClusterOffset := currentOffset mod bytesPerCluster; - - maxRunClusters := MAX_RUN_SECTORS div spc; - if maxRunClusters = 0 then - maxRunClusters := 1; - - if (ctx^.OpenFile <> nil) and (ctx^.OpenFile^.Extents <> nil) and - fatLocateExtentRun(ctx^.OpenFile^.Extents, startClusterIdx, - ctx^.ExtentValid, ctx^.ExtentIdx, ctx^.ExtentStartIdx, - run.StartCluster, run.RunLen, extentIdx, extentStartIdx) then begin - if run.RunLen > maxRunClusters then - run.RunLen := maxRunClusters; - run.LastCluster := run.StartCluster + run.RunLen - 1; - ctx^.ExtentValid := true; - ctx^.ExtentIdx := extentIdx; - ctx^.ExtentStartIdx := extentStartIdx; - curCluster := run.LastCluster; - chainPos := startClusterIdx + run.RunLen - 1; - end else begin - if not seekToClusterIndexHinted( - ctx^.Volume, ctx^.Cluster, startClusterIdx, ctx^.BootRecord, - ctx^.CursorValid, ctx^.CursorIndex, ctx^.CursorCluster, - curCluster, chainPos) then begin - ctx^.State := ftsError; - fat32_fileio_async_complete(ctx, eCorruptFilesystem); - exit; - end; - - run.StartCluster := curCluster; - buildContiguousRun(ctx^.Volume, curCluster, spc, MAX_RUN_SECTORS, ctx^.BootRecord, run.RunLen, run.LastCluster); - end; - run.RunSectors := run.RunLen * spc; - run.RunBytes := run.RunSectors * secSize; - run.LBA := clusterToLBA(ctx^.DataStart, spc, run.StartCluster); - - skipBytes := inClusterOffset; - copyBytes := run.RunBytes - skipBytes; - if copyBytes > remaining then - copyBytes := remaining; - - if (copyBytes = 0) or ((copyBytes mod secSize) <> 0) or ((skipBytes mod secSize) <> 0) then begin - ctx^.State := ftsError; - fat32_fileio_async_complete(ctx, eInvalidArgument); - exit; - end; - - sectorLBA := run.LBA + (skipBytes div secSize); - chunkSectors := copyBytes div secSize; - ctx^.ChunkBytes := copyBytes; - - lastBytePos := skipBytes + copyBytes - 1; - lastClusterIdx := startClusterIdx + (lastBytePos div bytesPerCluster); - lastClusterUsed := run.StartCluster + (lastBytePos div bytesPerCluster); - updateSIOC(ctx^.Volume, ctx^.Cluster, lastClusterIdx, lastClusterUsed); - ctx^.CursorValid := true; - ctx^.CursorIndex := lastClusterIdx; - ctx^.CursorCluster := lastClusterUsed; - - ctx^.State := ftsIssueIO; - - if ctx^.Mode = famRead then - err := driver.storage.mgr.storage_read_async( - ctx^.Volume^.device, sectorLBA, chunkSectors, - puint32(uint32(ctx^.Buffer) + ctx^.BytesDone), @fat32_fileio_async_step, ctx) - else - err := driver.storage.mgr.storage_write_async( - ctx^.Volume^.device, sectorLBA, chunkSectors, - puint32(uint32(ctx^.Buffer) + ctx^.BytesDone), @fat32_fileio_async_step, ctx); - - if err = eNone then - ctx^.State := ftsAwaitIO - else - ctx^.State := ftsError; - if err <> eNone then - fat32_fileio_async_complete(ctx, err); -end; - -procedure fat32_fileio_async_step(error : TError; userdata : pointer); -var - ctx : PFATFileIOAsyncCtx; -begin - ctx := PFATFileIOAsyncCtx(userdata); - if ctx = nil then exit; - ctx^.LastError := error; - if not fatScheduleTransfer(ctx) then begin - ctx^.State := ftsError; - fat32_fileio_async_complete(ctx, eQueueFull); - end; -end; - -{ Update the SIOC chain-position cache. } -procedure updateSIOC( - volume : PStorage_Volume; - fileStartCluster : uint32; - chainIdx : uint32; - chainCluster : uint32 -); -begin - sioc_vol := volume; - sioc_fileClust := fileStartCluster; - sioc_chainIdx := chainIdx; - sioc_chainClust := chainCluster; - sioc_valid := true; -end; - -{ Seek to a specific cluster index in a file's FAT chain. - Uses the SIOC cache to resume from a previously cached position - when possible, otherwise walks from the file's start cluster. - Returns true on success, false if the chain ended prematurely. } -function seekToClusterIndex( - volume : PStorage_Volume; - fileStartCluster : uint32; - targetIdx : uint32; - bootRecord : PBootRecord; - var curCluster : uint32; - var chainPos : uint32 -) : boolean; -var - nextFat : uint32; - maxCluster : uint32; -begin - seekToClusterIndex := false; - if bootRecord^.sectorSize > 0 then - maxCluster := (bootRecord^.FATSize * bootRecord^.sectorSize) div 4 - else - maxCluster := 0; - - { Resume from SIOC cache if it covers this file at a useful position } - if sioc_valid and (sioc_vol = volume) and (sioc_fileClust = fileStartCluster) - and (sioc_chainIdx <= targetIdx) then begin - curCluster := sioc_chainClust; - chainPos := sioc_chainIdx; - end else begin - curCluster := fileStartCluster; - chainPos := 0; - end; - - { Walk forward to targetIdx } - while chainPos < targetIdx do begin - if (maxCluster > 0) and (chainPos >= maxCluster) then - exit; - nextFat := readFat(volume, curCluster, bootRecord); - if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then - exit; - curCluster := nextFat; - chainPos := chainPos + 1; - end; - - seekToClusterIndex := true; -end; - -function seekToClusterIndexHinted( - volume : PStorage_Volume; - fileStartCluster : uint32; - targetIdx : uint32; - bootRecord : PBootRecord; - hintValid : boolean; - hintIdx : uint32; - hintCluster : uint32; - var curCluster : uint32; - var chainPos : uint32 -) : boolean; -var - nextFat : uint32; - maxCluster : uint32; -begin - seekToClusterIndexHinted := false; - if bootRecord^.sectorSize > 0 then - maxCluster := (bootRecord^.FATSize * bootRecord^.sectorSize) div 4 - else - maxCluster := 0; - - if hintValid and (hintIdx <= targetIdx) then begin - curCluster := hintCluster; - chainPos := hintIdx; - end else begin - if not seekToClusterIndex(volume, fileStartCluster, targetIdx, bootRecord, curCluster, chainPos) then - exit; - seekToClusterIndexHinted := true; - exit; - end; - - while chainPos < targetIdx do begin - if (maxCluster > 0) and (chainPos >= maxCluster) then - exit; - nextFat := readFat(volume, curCluster, bootRecord); - if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then - exit; - curCluster := nextFat; - chainPos := chainPos + 1; - end; - - seekToClusterIndexHinted := true; -end; - -//TODO add optional attributes flag to refine what i return -function getDirEntries(volume : PStorage_volume; cluster : uint32; bootRecord : PBootRecord) : PLinkedListBase; -var - buffer : puint32; - bufferI : puint32; - clusters : PLinkedListBase; - directories : PLinkedListBase; - i : uint32 = 0; - maxEntries : uint32; - datastart : uint32; - sectorLocation : uint32; - dirElm : puint32; -begin - push_trace('driver.storage.fs.fat32.getDirEntries.enter'); - directories:= LL_New(sizeof(TDirectory)); - - clusters:= PLinkedListBase(getFatChain(volume, cluster, bootRecord)); - if (clusters = nil) or (LL_size(clusters) = 0) then begin - getDirEntries := directories; - if clusters <> nil then LL_Free(clusters); - push_trace('driver.storage.fs.fat32.getDirEntries.exit'); - exit; - end; - push_trace('driver.storage.fs.fat32.getDirEntries.allocBuffer'); - buffer:= puint32(kalloc( (bootRecord^.sectorSize * bootRecord^.spc) * LL_size(clusters) )); - memset(uint32(buffer), 0, (bootRecord^.sectorSize * bootRecord^.spc) * LL_size(clusters) ); - - dataStart:= fatDataStartLBA(volume, bootRecord); - - push_trace('driver.storage.fs.fat32.getDirEntries.readSectors'); - for i:=0 to LL_size(clusters) - 1 do begin - sectorLocation:= bootRecord^.spc * puint32(LL_Get(clusters, i))^; - bufferI:= puint32(uint32(buffer) + uint32(i * bootRecord^.spc * bootRecord^.sectorSize)); - driver.storage.mgr.storage_read(volume^.device, datastart + sectorLocation, bootRecord^.spc, bufferI); - end; - - i:=0; - maxEntries:= ((bootRecord^.sectorSize * bootRecord^.spc) * LL_size(clusters)) div sizeof(TDirectory); - while i < maxEntries do begin - if PDirectory(buffer)[i].fileName[0] = char(0) then break; - - { Skip deleted entries ($E5 = FAT32 deleted marker) } - if PDirectory(buffer)[i].fileName[0] = char($E5) then begin - i+=1; - continue; - end; - - dirElm:= LL_Add(directories); - PDirectory(dirElm)^:= PDirectory(buffer)[i]; - i+=1; - end; - - getDirEntries:= directories; //get last . - LL_Free(clusters); - kfree(buffer); - push_trace('driver.storage.fs.fat32.getDirEntries.exit'); -end; - -function fat2GenericEntries(list : PLinkedListBase) : PLinkedListBase; -var - i : uint32; - entry : PDirectory_Entry; - dir : PDirectory; - dirElm : puint32; - nameBuf : pchar; - nameLen : uint32; - extLen : uint32; -begin - push_trace('driver.storage.fs.fat32.fat2GenericEntries.enter'); - puint32(entry) := kalloc(sizeof(TDirectory_Entry)); - fat2GenericEntries:= LL_New(sizeof(TDirectory_Entry)); - - if LL_size(list) > 0 then begin - - for i:= 0 to LL_Size(list) - 1 do begin - dir := PDirectory(LL_get(list, i)); - - { Skip deleted entries (should already be filtered by getDirEntries, but guard here too) } - if dir^.fileName[0] = char($E5) then continue; - - { Trim trailing spaces from fileName (8 bytes) } - nameLen := 8; - while (nameLen > 0) and (dir^.fileName[nameLen - 1] = ' ') do - nameLen := nameLen - 1; - - { Trim trailing spaces from extension (3 bytes) } - extLen := 3; - while (extLen > 0) and (dir^.fileExtension[extLen - 1] = ' ') do - extLen := extLen - 1; - - { Build combined filename: NAME.EXT or just NAME } - if extLen > 0 then begin - nameBuf := pchar(kalloc(nameLen + 1 + extLen + 1)); - memcpy(uint32(@dir^.fileName[0]), uint32(nameBuf), nameLen); - nameBuf[nameLen] := '.'; - memcpy(uint32(@dir^.fileExtension[0]), uint32(@nameBuf[nameLen + 1]), extLen); - nameBuf[nameLen + 1 + extLen] := char(0); - end else begin - nameBuf := pchar(kalloc(nameLen + 1)); - memcpy(uint32(@dir^.fileName[0]), uint32(nameBuf), nameLen); - nameBuf[nameLen] := char(0); - end; - - entry^.fileName := nameBuf; - - if dir^.attributes = $10 then begin - entry^.entryType:= TDirectory_Entry_Type.directoryEntry; - end else begin - entry^.entryType:= TDirectory_Entry_Type.fileEntry; - end; - - entry^.fileSize := dir^.byteSize; - entry^.modifiedDate := dir^.modifiedDate; - entry^.modifiedTime := dir^.modifiedTime; - entry^.attributes := dir^.attributes; - - //add to list - dirElm:= LL_add(fat2GenericEntries); - PDirectory_Entry(dirElm)^:= entry^; - end; - end; - - LL_Free(list); - kfree(puint32(entry)); - push_trace('driver.storage.fs.fat32.fat2GenericEntries.exit'); -end; - -//need to find out why having multiple dir stings isn't working, maybe the ls command? did I fix this? -function readDirectory(volume : PStorage_volume; directory : pchar; statusOut : puint32) : PLinkedListBase; //statusout: 0 = success, 1 = dir not exsist, 2 = not directory, 3 = invalid name, 4= already exists -var - bootRecord : PBootRecord; - directoryStrings : PLinkedListBase; - directories : PLinkedListBase; - cluster : uint32; - i : uint32; - ii : uint32 = 0; - dirEntry : PDirectory; - status : puint32; -begin - status:= puint32(kalloc(sizeof(uint32))); - push_trace('driver.storage.fs.fat32.readDirectory.enter'); - status^:= ord(eNone); - bootRecord:= readBootRecord(volume); - directoryStrings:= LL_fromString(directory, '/'); - directories:= getDirEntries(volume, bootRecord^.rootCluster, bootRecord); - - if LL_size(directoryStrings) > 0 then begin - for i:=0 to (LL_Size(directoryStrings) ) do begin /// maybe -1 will work - ii:=0; - - while true do begin - if ii > LL_Size(directories) - 1 then begin - status^:= ord(eDirectoryDoesNotExist); - break; - end; - dirEntry:= PDirectory(LL_Get(directories, ii)); - - if compareByteArray8( dirEntry^.fileName, cleanString( pchar(puint32(LL_Get(directoryStrings, i))^), status)) then begin - cluster:= dirFirstCluster(dirEntry); - break; - end; - ii+=1; - end; - - if status^ <> 0 then break; - - LL_Free(directories); - directories:= getDirEntries(volume, cluster, bootRecord); - - if i = LL_Size(directoryStrings) - 1 then break; - end; - end else begin - while true do begin - if ii > LL_Size(directories) - 1 then break; - dirEntry:= PDirectory(LL_Get(directories, ii)); - ii+=1; - end; - end; - - readDirectory:= directories; - - if statusOut <> nil then statusOut^ := status^; - kfree(status); - LL_Free(directoryStrings); - kfree(puint32(bootRecord)); - push_trace('driver.storage.fs.fat32.readDirectory.exit'); -end; - -function readDirectoryGen(volume : PStorage_volume; directory : pchar; status : puint32) : PLinkedListBase; //returns: 0 = success, 1 = dir not exsist, 2 = not directory, 3 = invalid name, 4= already exists -begin - push_trace('driver.storage.fs.fat32.readDirectoryGen.enter'); - readDirectoryGen:= fat2GenericEntries(readDirectory(volume, directory, status)); - push_trace('driver.storage.fs.fat32.readDirectoryGen.exit'); -end; - -//need to allow for setting file extension -function writeDirectory(volume : PStorage_volume; directory : pchar; dirName : pchar; attributes : uint32; statusOut : puint32) : uint32; // need to handle parent table cluster overflow, need to take attributes -var - directories : PLinkedListBase; - parentDirectory : PDirectory; - parentCluster : uint32; - extents : PLinkedListBase; - extent : PClusterExtent; - cluster : uint32; - bootRecord : PBootRecord; - buffer : puint32; - bufferPointer : PDirectory; - dataStart : uint32; - EntriesPerSector : uint32; - sectorLocation : uint32; - dataOffset : uint32; - i : uint32; - - thisArray : byteArray8 = ('.',' ',' ',' ',' ',' ',' ',' '); - parentArray : byteArray8 = ('.','.',' ',' ',' ',' ',' ',' '); - status : puint32; - - namePart : pchar; - extPart : pchar; -begin - push_trace('driver.storage.fs.fat32.writeDirectory.enter'); - io.syslog.logln('FAT32', 'writeDirectory: enter'); - writeDirectory := 0; - status:= puint32(kalloc(sizeof(uint32))); - status^:= ord(eNone); - - { Validate the directory name as a valid FAT32 8.3 name } - if not isValidFAT32Name(dirName) then begin - io.syslog.logln('FAT32', 'writeDirectory: invalid FAT32 name, aborting'); - statusOut^:= ord(eInvalidFileName); - kfree(status); - exit; - end; - - push_trace('driver.storage.fs.fat32.writeDirectory.readDir'); - directories:= readDirectory(volume, directory, status); - - { If the parent directory does not exist, bail out } - if status^ <> ord(eNone) then begin - io.syslog.logln('FAT32', 'writeDirectory: parent directory not found'); - statusOut^:= status^; - kfree(status); - LL_Free(directories); - exit; - end; - - { Split dirName for duplicate checking } - splitFileNameParts(dirName, namePart, extPart); - - if(LL_size(directories) > 1) then begin - for i:=0 to LL_Size(directories) - 1 do begin - if compareByteArray8(PDirectory(LL_get(directories, i))^.fileName, cleanString( namePart , status)) - and matchExtension(PDirectory(LL_get(directories, i))^.fileExtension, extPart) then begin - status^:= ord(eDirectoryAlreadyExists); - io.syslog.logln('FAT32', 'writeDirectory: entry already exists'); - end; - end; - end; - - kfree(void(namePart)); - kfree(void(extPart)); - - bootRecord:= readBootRecord(volume); - datastart:= fatDataStartLBA(volume, bootRecord); - - if status^ = ord(eNone) then begin - push_trace('driver.storage.fs.fat32.writeDirectory.createEntry'); - - { Check if directory sector is full } - EntriesPerSector:= uint32(bootRecord^.sectorSize) div uint32(sizeof(TDirectory)); - if LL_size(directories) >= EntriesPerSector then begin - { Current sector is full — would need cluster chain extension (not yet supported) } - io.syslog.logln('FAT32', 'writeDirectory: directory sector full'); - statusOut^:= ord(eDirectoryFull); - kfree(status); - kfree(puint32(bootRecord)); - LL_Free(directories); - exit; - end; - - parentDirectory:= PDirectory(LL_Get(directories, 0)); - parentCluster:= dirFirstCluster(parentDirectory); - - extents := findFreeClusterExtents(volume, 1, bootRecord); - if extents = nil then begin - { No free clusters available — disk is full } - io.syslog.logln('FAT32', 'writeDirectory: disk full'); - statusOut^:= ord(eDiskFull); - kfree(status); - kfree(puint32(bootRecord)); - LL_Free(directories); - exit; - end; - extent := PClusterExtent(LL_Get(extents, 0)); - cluster := extent^.StartCluster; - io.syslog.logln('FAT32', 'writeDirectory: allocated cluster'); - LL_Free(extents); - buffer:= puint32(kalloc(bootRecord^.sectorSize)); - - if attributes = $10 then begin // if directory - - memset(uint32(buffer), 0, bootRecord^.sectorSize); - - bufferPointer:= @PDirectory(buffer)[0]; - bufferPointer^.fileName:= thisArray; //TODO implement time - bufferPointer^.attributes:= attributes; - setDirFirstCluster(bufferPointer, cluster); - - bufferPointer:= @PDirectory(buffer)[1]; - bufferPointer^.fileName:= parentArray; //TODO implement time - bufferPointer^.attributes:= attributes; - setDirFirstCluster(bufferPointer, parentCluster); - - //write to disk - driver.storage.mgr.storage_write(volume^.device, clusterToLBA(dataStart, bootRecord^.spc, cluster), 1, buffer); - - //write fat - writeFat(volume, cluster, $FFFFFFF8, bootRecord); - end; - memset(uint32(buffer), 0, bootRecord^.sectorSize); - //calculate write cluster using directories and parentCluster - sectorLocation:= LL_size(directories) * sizeof(TDirectory) div bootRecord^.sectorSize; - sectorLocation:= sectorLocation + (parentCluster * bootRecord^.spc); - - //dataOffset:= datastart + ( (LL_size(directories) * sizeof(PDirectory)) - (sizeUsed * bootRecord^.sectorSize)); - driver.storage.mgr.storage_read(volume^.device, dataStart + sectorLocation, 1, buffer); - - //construct my dir entry - EntriesPerSector:= uint32(bootRecord^.sectorSize) div uint32(sizeof(TDirectory)); - bufferPointer:= @PDirectory(buffer)[LL_size(directories) - ((LL_size(directories) div EntriesPerSector) * EntriesPerSector)]; - - { Split dirName into name + extension parts for FAT32 8.3 format } - splitFileNameParts(dirName, namePart, extPart); - bufferPointer^.fileName:= cleanString(namePart, status); - bufferPointer^.attributes:= attributes; - if attributes = 0 then begin - { Copy extension characters for files } - fillFatExt(bufferPointer^.fileExtension, extPart); - end else begin - fillFatExt(bufferPointer^.fileExtension, nil); - end; - kfree(void(namePart)); - kfree(void(extPart)); - setDirFirstCluster(bufferPointer, cluster); - - writeDirectory:= cluster; - - push_trace('driver.storage.fs.fat32.writeDirectory.writeToDisk'); - //write to disk - driver.storage.mgr.storage_write(volume^.device, dataStart + sectorLocation, 1, buffer); - io.syslog.logln('FAT32', 'writeDirectory: entry written to disk'); - kfree(buffer); - end; - - statusOut^:= status^; - kfree(status); - kfree(puint32(bootRecord)); - LL_Free(directories); - io.syslog.logln('FAT32', 'writeDirectory: exit'); - -end; - -procedure writeDirectoryGen(volume : PStorage_volume; directory : pchar; dirName : pchar; attributes : uint32; statusOut : puint32); // need to handle parent table cluster overflow, need to take attributes -begin - push_trace('driver.storage.fs.fat32.writeDirectoryGen.enter'); - writeDirectory(volume, directory, dirName, attributes, statusOut); - push_trace('driver.storage.fs.fat32.writeDirectoryGen.exit'); -end; - -{ Split a full path like 'SYSTEM/FILE.TXT' into parent dir ('SYSTEM') and name ('FILE.TXT'). - If no separator, parentOut is empty string and nameOut is the whole path. - Caller must free parentOut and nameOut. } -procedure splitPathParts(fullPath : pchar; var parentOut : pchar; var nameOut : pchar); -var - pathLen, lastSlash, i : uint32; -begin - parentOut := nil; - nameOut := nil; - if fullPath = nil then begin - parentOut := stringNew(0); - nameOut := stringNew(0); - exit; - end; - pathLen := stringSize(fullPath); - if pathLen = 0 then begin - parentOut := stringNew(0); - nameOut := stringNew(0); - exit; - end; - - { Find last '/' separator } - lastSlash := pathLen; { sentinel: no slash found } - for i := 0 to pathLen - 1 do begin - if fullPath[i] = '/' then - lastSlash := i; - end; - - if lastSlash < pathLen then begin - parentOut := stringTrim(fullPath, lastSlash); - nameOut := stringCopy(pchar(@fullPath[lastSlash + 1])); - end else begin - parentOut := stringNew(0); - nameOut := stringCopy(fullPath); - end; -end; - -{ Free a FAT chain starting from the given cluster — marks all clusters as free (0). - Includes loop protection: aborts if iteration count exceeds maxCluster or cluster - is out of range (< 2 or >= maxCluster). } -procedure freeFatChain(volume : PStorage_volume; startCluster : uint32; bootRecord : PBootRecord); -var - currentCluster : uint32; - nextCluster : uint32; - maxCluster : uint32; - iter : uint32; -begin - push_trace('driver.storage.fs.fat32.freeFatChain.enter'); - if (bootRecord^.FATSize > 0) and (bootRecord^.sectorsize > 0) then - maxCluster := (bootRecord^.FATSize * bootRecord^.sectorsize) div 4 - else - maxCluster := 0; - currentCluster := startCluster; - iter := 0; - while true do begin - if (currentCluster < 2) or (currentCluster >= maxCluster) then begin - io.syslog.logln('FAT32', 'freeFatChain: cluster out of range, aborting'); - break; - end; - if iter >= maxCluster then begin - io.syslog.logln('FAT32', 'freeFatChain: iteration limit hit, possible cycle'); - break; - end; - nextCluster := readFat(volume, currentCluster, bootRecord); - writeFat(volume, currentCluster, 0, bootRecord); - iter := iter + 1; - - if (nextCluster and $0FFFFFFF) >= $0FFFFFF8 then break; { end of chain } - if (nextCluster and $0FFFFFFF) = $0FFFFFF7 then break; { bad cluster } - if nextCluster = 0 then break; { already free } - - currentCluster := nextCluster; - end; - push_trace('driver.storage.fs.fat32.freeFatChain.exit'); -end; - -{ Resolve a full path into parentCluster + leafName. - Splits the path, reads the parent directory, and returns - the parent cluster. On failure sets statusOut and returns false. - Caller must kfree parentDir and leafName when done. - bootRecord must already be read by the caller. } -function resolveParentCluster( - volume : PStorage_Volume; - path : pchar; - bootRecord : PBootRecord; - var parentDir : pchar; - var leafName : pchar; - var parentClust : uint32; - var statusOut : uint32 -) : boolean; -var - directories : PLinkedListBase; - parentEntry : PDirectory; - status : puint32; -begin - resolveParentCluster := false; - splitPathParts(path, parentDir, leafName); - - if (leafName = nil) or (stringSize(leafName) = 0) then begin - statusOut := ord(eInvalidFileName); - exit; - end; - - if (parentDir = nil) or (stringSize(parentDir) = 0) then begin - parentClust := bootRecord^.rootCluster; - resolveParentCluster := true; - exit; - end; - - status := puint32(kalloc(4)); - status^ := ord(eNone); - directories := readDirectory(volume, parentDir, status); - if status^ <> ord(eNone) then begin - statusOut := status^; - LL_Free(directories); - kfree(status); - exit; - end; - if LL_size(directories) < 1 then begin - statusOut := ord(eDirectoryDoesNotExist); - LL_Free(directories); - kfree(status); - exit; - end; - parentEntry := PDirectory(LL_Get(directories, 0)); - parentClust := dirFirstCluster(parentEntry); - LL_Free(directories); - kfree(status); - resolveParentCluster := true; -end; - -{ Delete a file from the volume. filePath is relative to volume root, e.g. 'SYSTEM/FILE.TXT' or 'FILE.TXT' } -procedure deleteFile(volume : PStorage_Volume; filePath : pchar; statusOut : puint32); -var - parentDir : pchar; - fileName : pchar; - namePart : pchar; - extPart : pchar; - bootRecord : PBootRecord; - parentCluster : uint32; - status : puint32; - buffer : puint32; - dir : PDirectory; - cluster : uint32; - cleanFileName : byteArray8; - loc : TDirEntryLocation; - resolveStatus : uint32; -begin - push_trace('driver.storage.fs.fat32.deleteFile.enter'); - io.syslog.logln('FAT32', 'deleteFile: enter'); - status := puint32(kalloc(4)); - status^ := ord(eNone); - - bootRecord := readBootRecord(volume); - - { Resolve parent directory + leaf name from full path } - resolveStatus := ord(eNone); - if not resolveParentCluster(volume, filePath, bootRecord, parentDir, fileName, parentCluster, resolveStatus) then begin - io.syslog.logln('FAT32', 'deleteFile: resolve failed'); - if statusOut <> nil then statusOut^ := resolveStatus; - kfree(status); - kfree(puint32(bootRecord)); - if parentDir <> nil then kfree(void(parentDir)); - if fileName <> nil then kfree(void(fileName)); - exit; - end; - - { Split filename for 8.3 matching } - splitFileNameParts(fileName, namePart, extPart); - cleanFileName := cleanString(namePart, status); - - { Locate target entry in parent directory } - buffer := puint32(kalloc(bootRecord^.sectorSize)); - if not locateDirEntry(volume, parentCluster, bootRecord, cleanFileName, extPart, loc, buffer) then begin - io.syslog.logln('FAT32', 'deleteFile: file not found'); - if statusOut <> nil then statusOut^ := ord(eFileDoesNotExist); - kfree(buffer); - kfree(void(namePart)); - kfree(void(extPart)); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(fileName)); - exit; - end; - - dir := @PDirectory(buffer)[loc.EntryIdx]; - - { Don't allow deleting directories via deleteFile } - if (dir^.attributes and $10) = $10 then begin - io.syslog.logln('FAT32', 'deleteFile: target is a directory, not a file'); - if statusOut <> nil then statusOut^ := ord(eNotADirectory); - kfree(buffer); - kfree(void(namePart)); - kfree(void(extPart)); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(fileName)); - exit; - end; - - cluster := dirFirstCluster(dir); - - { Free the FAT chain for this file } - io.syslog.logln('FAT32', 'deleteFile: freeing FAT chain'); - if cluster >= 2 then - freeFatChain(volume, cluster, bootRecord); - - { Mark entry deleted and write back } - dir^.fileName[0] := char($E5); - driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, buffer); - - if statusOut <> nil then statusOut^ := ord(eNone); - - kfree(buffer); - kfree(void(namePart)); - kfree(void(extPart)); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(fileName)); - push_trace('driver.storage.fs.fat32.deleteFile.exit'); - io.syslog.logln('FAT32', 'deleteFile: done'); -end; - -{ Delete a directory from the volume. path is relative to volume root. Directory must be empty. } -procedure deleteDir(volume : PStorage_Volume; path : pchar; statusOut : puint32); -var - parentDir : pchar; - dirName : pchar; - namePart : pchar; - extPart : pchar; - bootRecord : PBootRecord; - childDirs : PLinkedListBase; - parentCluster : uint32; - status : puint32; - buffer : puint32; - dir : PDirectory; - cluster : uint32; - childCount : uint32; - cleanFileName : byteArray8; - loc : TDirEntryLocation; - resolveStatus : uint32; -begin - push_trace('driver.storage.fs.fat32.deleteDir.enter'); - io.syslog.logln('FAT32', 'deleteDir: enter'); - status := puint32(kalloc(4)); - status^ := ord(eNone); - - bootRecord := readBootRecord(volume); - - { Resolve parent directory + leaf name from full path } - resolveStatus := ord(eNone); - if not resolveParentCluster(volume, path, bootRecord, parentDir, dirName, parentCluster, resolveStatus) then begin - io.syslog.logln('FAT32', 'deleteDir: resolve failed'); - if statusOut <> nil then statusOut^ := resolveStatus; - kfree(status); - kfree(puint32(bootRecord)); - if parentDir <> nil then kfree(void(parentDir)); - if dirName <> nil then kfree(void(dirName)); - exit; - end; - - { Split dirName for 8.3 matching } - splitFileNameParts(dirName, namePart, extPart); - cleanFileName := cleanString(namePart, status); - - { Locate target entry in parent directory } - buffer := puint32(kalloc(bootRecord^.sectorSize)); - if not locateDirEntry(volume, parentCluster, bootRecord, cleanFileName, extPart, loc, buffer) then begin - io.syslog.logln('FAT32', 'deleteDir: directory not found'); - if statusOut <> nil then statusOut^ := ord(eDirectoryDoesNotExist); - kfree(buffer); - kfree(void(namePart)); - kfree(void(extPart)); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(dirName)); - exit; - end; - - dir := @PDirectory(buffer)[loc.EntryIdx]; - - { Must be a directory } - if (dir^.attributes and $10) <> $10 then begin - if statusOut <> nil then statusOut^ := ord(eNotADirectory); - kfree(buffer); - kfree(void(namePart)); - kfree(void(extPart)); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(dirName)); - exit; - end; - - cluster := dirFirstCluster(dir); - - { Check if directory is empty — should contain only '.' and '..' } - childDirs := getDirEntries(volume, cluster, bootRecord); - childCount := LL_size(childDirs); - LL_Free(childDirs); - - if childCount > 2 then begin - io.syslog.logln('FAT32', 'deleteDir: directory not empty, refusing to delete'); - if statusOut <> nil then statusOut^ := ord(eDirectoryNotEmpty); - kfree(buffer); - kfree(void(namePart)); - kfree(void(extPart)); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(dirName)); - exit; - end; - - { Free the FAT chain for this directory's data } - if cluster >= 2 then - freeFatChain(volume, cluster, bootRecord); - - { Mark entry deleted and write back } - dir^.fileName[0] := char($E5); - driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, buffer); - - if statusOut <> nil then statusOut^ := ord(eNone); - - kfree(buffer); - kfree(void(namePart)); - kfree(void(extPart)); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(dirName)); - push_trace('driver.storage.fs.fat32.deleteDir.exit'); - io.syslog.logln('FAT32', 'deleteDir: done'); -end; - -{ Rename a file or directory on disk. filePath is the current relative path, - newName is the new leaf name (e.g. 'NEWFILE.TXT'). Only changes the 8.3 - directory entry in-place — does not move between directories. } -procedure renameFile(volume : PStorage_Volume; filePath : pchar; newName : pchar; statusOut : puint32); -var - parentDir : pchar; - fileName : pchar; - namePart : pchar; - extPart : pchar; - newNamePart : pchar; - newExtPart : pchar; - bootRecord : PBootRecord; - parentCluster : uint32; - j : uint32; - status : puint32; - buffer : puint32; - dir : PDirectory; - cleanName : byteArray8; - newCleanName : byteArray8; - loc : TDirEntryLocation; - resolveStatus : uint32; -begin - push_trace('driver.storage.fs.fat32.renameFile.enter'); - io.syslog.logln('FAT32', 'renameFile: enter'); - status := puint32(kalloc(4)); - status^ := ord(eNone); - - bootRecord := readBootRecord(volume); - - { Validate new name before doing any work } - if (newName = nil) or (stringSize(newName) = 0) then begin - io.syslog.logln('FAT32', 'renameFile: invalid new name'); - if statusOut <> nil then statusOut^ := ord(eInvalidFileName); - kfree(status); - kfree(puint32(bootRecord)); - exit; - end; - - { Resolve parent directory + leaf name from full path } - resolveStatus := ord(eNone); - if not resolveParentCluster(volume, filePath, bootRecord, parentDir, fileName, parentCluster, resolveStatus) then begin - io.syslog.logln('FAT32', 'renameFile: resolve failed'); - if statusOut <> nil then statusOut^ := resolveStatus; - kfree(status); - kfree(puint32(bootRecord)); - if parentDir <> nil then kfree(void(parentDir)); - if fileName <> nil then kfree(void(fileName)); - exit; - end; - - { Split current filename for 8.3 matching } - splitFileNameParts(fileName, namePart, extPart); - cleanName := cleanString(namePart, status); - - { Locate target entry in parent directory } - buffer := puint32(kalloc(bootRecord^.sectorSize)); - if not locateDirEntry(volume, parentCluster, bootRecord, cleanName, extPart, loc, buffer) then begin - io.syslog.logln('FAT32', 'renameFile: file not found'); - if statusOut <> nil then statusOut^ := ord(eFileDoesNotExist); - kfree(buffer); - kfree(void(namePart)); - kfree(void(extPart)); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(fileName)); - exit; - end; - - dir := @PDirectory(buffer)[loc.EntryIdx]; - - { Apply new 8.3 name in-place } - splitFileNameParts(newName, newNamePart, newExtPart); - newCleanName := cleanString(newNamePart, status); - for j := 0 to 7 do - dir^.fileName[j] := newCleanName[j]; - fillFatExt(dir^.fileExtension, newExtPart); - kfree(void(newNamePart)); - kfree(void(newExtPart)); - - { Write back this one sector } - driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, buffer); - - if statusOut <> nil then statusOut^ := ord(eNone); - - kfree(buffer); - kfree(void(namePart)); - kfree(void(extPart)); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(fileName)); - push_trace('driver.storage.fs.fat32.renameFile.exit'); - io.syslog.logln('FAT32', 'renameFile: done'); -end; - -// function checkExists(volume : PStorage_Volume; directory : pchar; fileName : pchar; fileExtension : pchar; entry : PDirectory_Entry) : uint32; -// var -// bootRecord : PBootRecord; -// directories : PLinkedListBase; -// dir : PDirectory; -// genDir : PDirectory_Entry; -// data : puint32; -// statusOut : puint32; -// i : uint32; -// exists : boolean = false; -// begin -// bootRecord := readBootRecord(volume); -// directories := readDirectory(volume, directory, statusOut); -// datastart:= volume^.sectorStart + 1 + bootRecord^.FATSize + bootRecord^.rsvSectors; - -// for i:=0 to LL_Size(directories) -1 do begin -// dir:= PDirectory(LL_Get(directories, i)); -// if (dir^.fileName = entry^.fileName) and (dir^.fileExtension = entry^.extension) then begin -// exists:= true; -// break; -// end; -// end; - -// PDirectory_Entry := PDirectory_Entry(kalloc(20)); - -// PDirectory_Entry^.fileName := pchar(@dir^.fileName); -// PDirectory_Entry^.extension := pchar(@dir^.fileExtension); - -// end; - -//TODO check directory commands for errors with a clean disk - -{ ========================================================================== } -{ Async format state machine (create_volume_async) } -{ ========================================================================== } - -procedure fmt_step_complete(error : TError; userdata : pointer); forward; - -procedure fmt_run_next(ctx : PFmtContext); -var - writeCount : uint32; -begin - case ctx^.Step of - fmtBootSector: begin - io.syslog.logln('FAT32', 'fmt: fmtBootSector'); - { Write boot sector at start + 1 } - driver.storage.mgr.storage_write_async(ctx^.Disk, ctx^.SectorStart + 1, 1, - ctx^.Buffer, @fmt_step_complete, pointer(ctx)); - end; - fmtZeroFAT: begin - io.syslog.logln('FAT32', 'fmt: fmtZeroFAT batch'); - { Issue next FAT zero batch } - if ctx^.BatchPos >= ctx^.FATSize then begin - io.syslog.logln('FAT32', 'fmt: FAT zero complete, moving to fmtFATEntries'); - { FAT zeroing complete — move to FAT entries } - kfree(ctx^.ZeroBuffer); - ctx^.ZeroBuffer := nil; - kfree(ctx^.Buffer); - ctx^.Buffer := puint32(kalloc(ctx^.Disk^.sectorSize)); - memset(uint32(ctx^.Buffer), 0, ctx^.Disk^.sectorSize); - puint32(ctx^.Buffer)[0] := $0FFFFFF8; { media type (reserved entry 0) } - puint32(ctx^.Buffer)[1] := $0FFFFFFF; { clean marker (reserved entry 1) } - puint32(ctx^.Buffer)[2] := $0FFFFFF8; { root cluster EOC (entry 2) } - puint32(ctx^.Buffer)[3] := $0FFFFFF8; { SYSTEM dir cluster EOC (entry 3) } - ctx^.Step := fmtFATEntries; - fmt_run_next(ctx); - exit; - end; - if (ctx^.FATSize - ctx^.BatchPos) >= ctx^.BatchSize then - writeCount := ctx^.BatchSize - else - writeCount := ctx^.FATSize - ctx^.BatchPos; - driver.storage.mgr.storage_write_async(ctx^.Disk, - ctx^.FATStart + ctx^.BatchPos, writeCount, ctx^.ZeroBuffer, - @fmt_step_complete, pointer(ctx)); - end; - fmtFATEntries: begin - io.syslog.logln('FAT32', 'fmt: fmtFATEntries'); - { Write FAT entries sector } - driver.storage.mgr.storage_write_async(ctx^.Disk, ctx^.FATStart, 1, - ctx^.Buffer, @fmt_step_complete, pointer(ctx)); - end; - fmtRootDir: begin - io.syslog.logln('FAT32', 'fmt: fmtRootDir'); - { Build root directory: "." + ".." + "SYSTEM" entries } - memset(uint32(ctx^.Buffer), 0, ctx^.Disk^.sectorSize); - { "." entry — volume label } - PDirectory(ctx^.Buffer)[0].fileName[0] := '.'; - PDirectory(ctx^.Buffer)[0].fileName[1] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[2] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[3] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[4] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[5] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[6] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[7] := ' '; - PDirectory(ctx^.Buffer)[0].attributes := $08; - PDirectory(ctx^.Buffer)[0].clusterLow := ctx^.RootCluster; - { ".." entry } - PDirectory(ctx^.Buffer)[1].fileName[0] := '.'; - PDirectory(ctx^.Buffer)[1].fileName[1] := '.'; - PDirectory(ctx^.Buffer)[1].fileName[2] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[3] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[4] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[5] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[6] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[7] := ' '; - PDirectory(ctx^.Buffer)[1].attributes := $10; - PDirectory(ctx^.Buffer)[1].clusterLow := ctx^.RootCluster; - { "SYSTEM" directory entry pointing to cluster 3 } - PDirectory(ctx^.Buffer)[2].fileName[0] := 'S'; - PDirectory(ctx^.Buffer)[2].fileName[1] := 'Y'; - PDirectory(ctx^.Buffer)[2].fileName[2] := 'S'; - PDirectory(ctx^.Buffer)[2].fileName[3] := 'T'; - PDirectory(ctx^.Buffer)[2].fileName[4] := 'E'; - PDirectory(ctx^.Buffer)[2].fileName[5] := 'M'; - PDirectory(ctx^.Buffer)[2].fileName[6] := ' '; - PDirectory(ctx^.Buffer)[2].fileName[7] := ' '; - PDirectory(ctx^.Buffer)[2].attributes := $10; - PDirectory(ctx^.Buffer)[2].clusterLow := 3; - driver.storage.mgr.storage_write_async(ctx^.Disk, - clusterToLBA(ctx^.DataStart, ctx^.SPC, ctx^.RootCluster), 1, ctx^.Buffer, - @fmt_step_complete, pointer(ctx)); - end; - fmtSystemDir: begin - io.syslog.logln('FAT32', 'fmt: fmtSystemDir'); - { Build SYSTEM directory: "." and ".." } - memset(uint32(ctx^.Buffer), 0, ctx^.Disk^.sectorSize); - PDirectory(ctx^.Buffer)[0].fileName[0] := '.'; - PDirectory(ctx^.Buffer)[0].fileName[1] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[2] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[3] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[4] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[5] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[6] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[7] := ' '; - PDirectory(ctx^.Buffer)[0].attributes := $10; - PDirectory(ctx^.Buffer)[0].clusterLow := 3; - PDirectory(ctx^.Buffer)[1].fileName[0] := '.'; - PDirectory(ctx^.Buffer)[1].fileName[1] := '.'; - PDirectory(ctx^.Buffer)[1].fileName[2] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[3] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[4] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[5] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[6] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[7] := ' '; - PDirectory(ctx^.Buffer)[1].attributes := $10; - PDirectory(ctx^.Buffer)[1].clusterLow := ctx^.RootCluster; - driver.storage.mgr.storage_write_async(ctx^.Disk, - clusterToLBA(ctx^.DataStart, ctx^.SPC, 3), 1, ctx^.Buffer, - @fmt_step_complete, pointer(ctx)); - end; - fmtDone: begin - io.syslog.logln('FAT32', 'fmt: fmtDone — format complete'); - { All done — clean up and notify caller } - kfree(ctx^.Buffer); - ctx^.Buffer := nil; - if ctx^.Callback <> nil then - ctx^.Callback(eNone, ctx^.CallbackData); - kfree(puint32(ctx)); - end; - end; -end; - -procedure fmt_step_complete(error : TError; userdata : pointer); -var - ctx : PFmtContext; -begin - ctx := PFmtContext(userdata); - if error <> eNone then begin - io.syslog.logln('FAT32', 'fmt_step_complete: I/O error — aborting format'); - { Error — abort format, clean up, notify caller } - if ctx^.Buffer <> nil then kfree(ctx^.Buffer); - if ctx^.ZeroBuffer <> nil then kfree(ctx^.ZeroBuffer); - if ctx^.Callback <> nil then - ctx^.Callback(error, ctx^.CallbackData); - kfree(puint32(ctx)); - exit; - end; - - { Advance to next step } - case ctx^.Step of - fmtBootSector: begin - ctx^.Step := fmtZeroFAT; - ctx^.BatchPos := 0; - end; - fmtZeroFAT: begin - ctx^.BatchPos := ctx^.BatchPos + ctx^.BatchSize; - { Step stays fmtZeroFAT — fmt_run_next checks if more batches remain } - end; - fmtFATEntries: begin - ctx^.Step := fmtRootDir; - end; - fmtRootDir: begin - ctx^.Step := fmtSystemDir; - end; - fmtSystemDir: begin - ctx^.Step := fmtDone; - end; - end; - fmt_run_next(ctx); -end; - -procedure create_volume_async(volume : PStorage_Volume; sectors : uint32; start : uint32; - config : puint32; callback : TIOCallback; callbackData : pointer); -var - ctx : PFmtContext; - bootRecord : PBootRecord; - spc : uint32; -begin - push_trace('driver.storage.fs.fat32.create_volume_async'); - io.syslog.logln('FAT32', 'create_volume_async: enter'); - - ctx := PFmtContext(kalloc(sizeof(TFmtContext))); - if ctx = nil then begin - io.syslog.logln('FAT32', 'create_volume_async: OOM allocating context'); - if callback <> nil then callback(eOutOfMemory, callbackData); - exit; - end; - - ctx^.Volume := volume; - ctx^.Disk := volume^.device; - ctx^.SectorStart := start; - ctx^.RootCluster := 2; - ctx^.Callback := callback; - ctx^.CallbackData := callbackData; - ctx^.ZeroBuffer := nil; - - if config <> nil then - spc := config^ - else - spc := fatDefaultSPC(sectors, ctx^.Disk^.sectorSize); - if spc = 0 then - spc := fatDefaultSPC(sectors, ctx^.Disk^.sectorSize); - ctx^.SPC := spc; - - ctx^.FATSize := fatComputeFATSize(sectors, spc, ctx^.Disk^.sectorSize); - ctx^.FATStart := start + 1 + 32; { boot sector + 32 reserved sectors } - ctx^.DataStart := ctx^.FATStart + ctx^.FATSize; - - { Allocate boot sector buffer and build boot record } - ctx^.Buffer := puint32(kalloc(ctx^.Disk^.sectorSize + 512)); - if ctx^.Buffer = nil then begin - if callback <> nil then callback(eOutOfMemory, callbackData); - kfree(puint32(ctx)); - exit; - end; - memset(uint32(ctx^.Buffer), 0, ctx^.Disk^.sectorSize); - - bootRecord := PBootRecord(ctx^.Buffer); - bootRecord^.jmp2boot := $0; - bootRecord^.OEMName[0] := 'A'; - bootRecord^.OEMName[1] := 'S'; - bootRecord^.OEMName[2] := 'U'; - bootRecord^.OEMName[3] := 'R'; - bootRecord^.OEMName[4] := 'O'; - bootRecord^.OEMName[5] := ' '; - bootRecord^.OEMName[6] := 'V'; - bootRecord^.OEMName[7] := '1'; - bootRecord^.sectorSize := ctx^.Disk^.sectorSize; - bootRecord^.spc := uint8(spc); - bootRecord^.rsvSectors := 32; - bootRecord^.numFats := 1; - bootRecord^.mediaDescp := $F8; - bootRecord^.hiddenSectors := start; - bootRecord^.manySectors := sectors; - bootRecord^.FATSize := ctx^.FATSize; - bootRecord^.rootCluster := ctx^.RootCluster; - bootRecord^.FSInfoCluster := 0; - bootRecord^.driveNumber := $80; - bootRecord^.volumeID := 62; - bootRecord^.bsignature := $29; - bootRecord^.identString[0] := 'F'; - bootRecord^.identString[1] := 'A'; - bootRecord^.identString[2] := 'T'; - bootRecord^.identString[3] := '3'; - bootRecord^.identString[4] := '2'; - bootRecord^.identString[5] := ' '; - bootRecord^.identString[6] := ' '; - bootRecord^.identString[7] := ' '; - - { Boot sector signature at bytes 508-511 } - puint32(ctx^.Buffer)[127] := $55AA; - - { Allocate zero buffer for FAT batches } - if ctx^.FATSize > 128 then - ctx^.BatchSize := 128 - else - ctx^.BatchSize := ctx^.FATSize; - ctx^.BatchPos := 0; - ctx^.ZeroBuffer := puint32(kalloc(ctx^.Disk^.sectorSize * ctx^.BatchSize)); - if ctx^.ZeroBuffer = nil then begin - kfree(ctx^.Buffer); - if callback <> nil then callback(eOutOfMemory, callbackData); - kfree(puint32(ctx)); - exit; - end; - memset(uint32(ctx^.ZeroBuffer), 0, ctx^.Disk^.sectorSize * ctx^.BatchSize); - - { Kick off the first step: write boot sector } - io.syslog.logln('FAT32', 'create_volume_async: kicking off fmtBootSector'); - ctx^.Step := fmtBootSector; - fmt_run_next(ctx); -end; - -{ ========================================================================== } -{ Synchronous create_volume (task context only) } -{ ========================================================================== } - -procedure create_volume(volume : PStorage_Volume; sectors : uint32; start : uint32; config : puint32); -var - buffer : puint32; - zeroBuffer : puint32; - bootRecord : PBootRecord; - dataStart : uint32; - fatStart : uint32; - FATSize : uint32; - batchSize : uint32; - batchPos : uint32; - - asuroArray : byteArray8 = ('A','S','U','R','O',' ','V','1'); - fatArray : byteArray8 = ('F','A','T','3','2',' ',' ',' '); - thisArray : byteArray8 = ('.',' ',' ',' ',' ',' ',' ',' '); - parentArray : byteArray8 = ('.','.',' ',' ',' ',' ',' ',' '); - - asuroFileArray : byteArray8 = ('A','S','U','R','O',' ',' ',' '); - mountFileArray : byteArray8 = ('M','O','U','N','T',' ',' ',' '); - programFileArray : byteArray8 = ('P','R','O','G','R','A','M','S'); - rootCluster : uint32 = 2; - - sysArray : byteArray8 = ('S','Y','S','T','E','M',' ',' '); - progArray : byteArray8 = ('P','R','O','G','R','A','M','S'); - userArray : byteArray8 = ('U','S','E','R',' ',' ',' ',' '); - - status : puint32; - disk : PStorage_device; - spc : uint32; - -begin - push_trace('driver.storage.fs.fat32.create_volume()'); - io.syslog.logln('FAT32', 'create_volume (sync): enter'); - - disk := volume^.device; - - { Default sectors-per-cluster to 1 if config is nil } - if config <> nil then - spc := config^ - else - spc := fatDefaultSPC(sectors, disk^.sectorSize); - if spc = 0 then - spc := fatDefaultSPC(sectors, disk^.sectorSize); - - //driver.storage.fs.fat32 structure - (* BootRecord *) - (* reserved sectors *) - (* File Allocation Table *) - (* Data Area *) - - buffer:= puint32(kalloc(disk^.sectorSize+512)); - memset(uint32(buffer), 0, disk^.sectorSize); - - bootRecord:= PBootRecord(buffer); - - FATSize:= fatComputeFATSize(sectors, spc, disk^.sectorsize); - - bootRecord^.jmp2boot := $0; //TODO impliment boot jump - bootRecord^.OEMName := asuroArray; - bootRecord^.sectorSize := disk^.sectorsize; - bootRecord^.spc := uint8(spc); - bootRecord^.rsvSectors := 32; //32 is standard - bootRecord^.numFats := 1; - bootRecord^.mediaDescp := $F8; - bootRecord^.hiddenSectors := start; - bootRecord^.manySectors := sectors; - bootRecord^.FATSize := FATSize; - bootRecord^.rootCluster := rootCluster; - bootRecord^.FSInfoCluster := 0; - bootRecord^.driveNumber := $80; - bootRecord^.volumeID := 62; //+ puint32(@driver.timer.rtc.getDateTime())^; - bootRecord^.bsignature := $29; - bootRecord^.identString := fatArray; - - - { Write the boot sector signature marker at bytes 508-511 } - puint32(buffer)[127] := $55AA; - - driver.storage.mgr.storage_write(disk, start + 1, 1, puint32(buffer)); - io.syslog.logln('FAT32', 'create_volume (sync): boot sector written'); - - fatStart:= start + 1 + bootRecord^.rsvSectors; - dataStart:= fatStart + bootRecord^.FATSize; - - { Batch zero the FAT: write 128 sectors at a time instead of 1 } - if FATSize > 128 then - batchSize := 128 - else - batchSize := FATSize; - zeroBuffer:= puint32(kalloc( disk^.sectorSize * batchSize )); - memset(uint32(zeroBuffer), 0, disk^.sectorSize * batchSize); - - batchPos := 0; - while batchPos < FATSize do begin - if (FATSize - batchPos) >= batchSize then - driver.storage.mgr.storage_write(disk, fatStart + batchPos, batchSize, zeroBuffer) - else - driver.storage.mgr.storage_write(disk, fatStart + batchPos, FATSize - batchPos, zeroBuffer); - batchPos += batchSize; - end; - - kfree(buffer); - kfree(zeroBuffer); - io.syslog.logln('FAT32', 'create_volume (sync): FAT zeroed'); - - buffer:= puint32(kalloc(disk^.sectorSize)); - memset(uint32(buffer), 0, disk^.sectorSize); - - puint32(buffer)[0]:= $0FFFFFF8; //media type marker (reserved entry 0) - puint32(buffer)[1]:= $0FFFFFFF; //clean/dirty marker (reserved entry 1) - puint32(buffer)[2]:= $0FFFFFF8; //root cluster end-of-chain (entry 2) - - driver.storage.mgr.storage_write(disk, fatStart, 1, buffer); - io.syslog.logln('FAT32', 'create_volume (sync): FAT entries written'); - - kfree(buffer); - - buffer:= puint32(kalloc(disk^.sectorsize)); - memset(uint32(buffer), 0, disk^.sectorsize); - - PDirectory(buffer)[0].fileName := thisArray; - PDirectory(buffer)[0].attributes := $08; - PDirectory(buffer)[0].clusterLow := rootCluster; - - PDirectory(buffer)[1].fileName := parentArray; - PDirectory(buffer)[1].attributes := $10; - PDirectory(buffer)[1].clusterLow := rootCluster; - - driver.storage.mgr.storage_write(disk, clusterToLBA(dataStart, spc, rootCluster), 1, buffer); - io.syslog.logln('FAT32', 'create_volume (sync): root dir written'); - - memset(uint32(buffer), 0, disk^.sectorsize); - - status := puint32(kalloc(sizeof(uint32))); - writeDirectory(volume, '', 'SYSTEM', $10, status); - io.syslog.logln('FAT32', 'create_volume (sync): SYSTEM dir created'); - kfree(status); - - kfree(buffer); - io.syslog.logln('FAT32', 'create_volume (sync): done'); - - - -end; - -{ Count free clusters by scanning the FAT sector by sector. - Returns the number of free clusters (FAT entries == 0). } -function countFreeFATClusters(volume : PStorage_Volume; bootRecord : PBootRecord) : uint32; -var - fatStart : uint32; - maxCluster : uint32; - entriesPerSect : uint32; - sectorIdx : uint32; - entryIdx : uint32; - clusterNum : uint32; - freeCount : uint32; - fatSectors : uint32; - fatBuffer : puint32; -begin - countFreeFATClusters := 0; - if (bootRecord^.sectorsize = 0) or (bootRecord^.FATSize = 0) then exit; - - fatStart := volume^.sectorStart + 1 + bootRecord^.rsvSectors; - maxCluster := (bootRecord^.FATSize * bootRecord^.sectorsize) div 4; - entriesPerSect := bootRecord^.sectorsize div 4; - fatSectors := bootRecord^.FATSize; - - fatBuffer := puint32(kalloc(bootRecord^.sectorsize)); - freeCount := 0; - - for sectorIdx := 0 to fatSectors - 1 do begin - if sectorIdx * entriesPerSect >= maxCluster then break; - driver.storage.mgr.storage_read(volume^.device, fatStart + sectorIdx, 1, fatBuffer); - for entryIdx := 0 to entriesPerSect - 1 do begin - clusterNum := sectorIdx * entriesPerSect + entryIdx; - if clusterNum < 2 then continue; - if clusterNum >= maxCluster then break; - if fatBuffer[entryIdx] = 0 then - freeCount := freeCount + 1; - end; - end; - - kfree(fatBuffer); - countFreeFATClusters := freeCount; -end; - -function identify_volume(volume : PStorage_Volume) : boolean; -var - buffer : puint32; - bootRecord : PBootRecord; - bufSize : uint32; -begin - push_trace('driver.storage.fs.fat32.identify_volume'); - io.syslog.logln('FAT32', 'identify_volume: enter'); - identify_volume := false; - if volume^.device = nil then exit; - if volume^.device^.dispatchRead = nil then exit; - - bufSize := volume^.device^.sectorSize; - if bufSize < 512 then bufSize := 512; - buffer := puint32(kalloc(bufSize)); - memset(uint32(buffer), 0, bufSize); - - driver.storage.mgr.storage_read(volume^.device, volume^.sectorStart + 1, 1, buffer); - bootRecord := PBootRecord(buffer); - - if (bootRecord^.bsignature = $29) then begin - io.syslog.logln('FAT32', 'identify_volume: FAT32 signature matched'); - identify_volume := true; - volume^.freeSectors := countFreeFATClusters(volume, bootRecord) * bootRecord^.spc; - end else begin - io.syslog.logln('FAT32', 'identify_volume: not a FAT32 volume'); - end; - - kfree(buffer); -end; - -procedure detect_volumes(disk : PStorage_Device); -var - buffer : puint32; - bufSize : uint32; - i : uint8; - volume : PStorage_volume; -begin - push_trace('driver.storage.fs.fat32.detectVolumes()'); - - bufSize := disk^.sectorSize; - if bufSize < 512 then bufSize := 512; - buffer := puint32(kalloc(bufSize)); - memset(uint32(buffer), 0, bufSize); - - { Read from sector 2 to check for FAT32 boot record } - if disk^.dispatchRead = nil then begin - io.syslog.writestringln('FAT32: detect_volumes: device has no read dispatch.'); - kfree(buffer); - exit; - end; - - driver.storage.mgr.storage_read(disk, 2, 1, buffer); - - if (puint32(buffer)[127] = $55AA) and (PBootRecord(buffer)^.bsignature = $29) then begin - io.syslog.writestringln('FAT32: volume found!'); - volume := PStorage_volume(kalloc(sizeof(TStorage_Volume))); - memset(uint32(volume), 0, sizeof(TStorage_Volume)); - volume^.device := disk; - volume^.sectorStart := 1; - volume^.sectorSize := PBootRecord(buffer)^.sectorSize; - volume^.sectorCount := disk^.maxSectorCount; - volume^.filesystem := @filesystem; - volume^.freeSectors := countFreeFATClusters(volume, PBootRecord(buffer)) * PBootRecord(buffer)^.spc; - volume^.isBootDrive := false; - - driver.storage.vol.mgr.register_volume(disk, volume); - end; - - kfree(buffer); -end; - -{ ---- Per-file open / close for VFS metadata caching ---- } - -{ Called by VFS on OpenFile. Reads the boot record (cached per-volume), - scans the directory for the file, and returns a TFATOpenFile context - that writeFileAtOffset / readFileAtOffset can reuse on every call. - Also populates fileSize so the VFS can skip calling fileSizeCallback. } -function fat32OpenFile(volume : PStorage_Volume; directory : pchar; - fileName : pchar; var fileSize : uint32) : pointer; -var - ofi : PFATOpenFile; - bootRecord : PBootRecord; - dirs : PLinkedListBase; - status : puint32; - tempdir : PDirectory; - i : uint32; - cleanFileName : byteArray8; - otherCFN : byteArray8; - namePart : pchar; - extPart : pchar; -begin - push_trace('driver.storage.fs.fat32.openFile.enter'); - fileSize := 0; - - bootRecord := getCachedBootRecord(volume); - - ofi := PFATOpenFile(kalloc(SizeOf(TFATOpenFile))); - memset(uint32(ofi), 0, SizeOf(TFATOpenFile)); - ofi^.Volume := volume; - ofi^.BootRecord := bootRecord; - ofi^.DataStart := fatDataStartLBA(volume, bootRecord); - ofi^.ScratchSize := bootRecord^.sectorSize; - ofi^.ScratchSector := puint32(kalloc(ofi^.ScratchSize)); - if ofi^.ScratchSector <> nil then - memset(uint32(ofi^.ScratchSector), 0, ofi^.ScratchSize); - ofi^.Exists := false; - - { Read directory entries } - status := puint32(kalloc(sizeof(uint32))); - status^ := 0; - dirs := readDirectory(volume, directory, status); - - { Determine parent cluster (Lesson 18: root has no '.' entry) } - if (directory = nil) or (stringSize(directory) = 0) then - ofi^.DirCluster := bootRecord^.rootCluster - else begin - if (dirs <> nil) and (LL_size(dirs) > 0) then begin - tempdir := PDirectory(LL_get(dirs, 0)); - if tempdir^.fileName[0] = '.' then - ofi^.DirCluster := dirFirstCluster(tempdir) - else - ofi^.DirCluster := bootRecord^.rootCluster; - end else - ofi^.DirCluster := bootRecord^.rootCluster; - end; - - { Parse file name and compute cleaned 8.3 name } - splitFileNameParts(fileName, namePart, extPart); - cleanFileName := cleanString(namePart, status); - ofi^.CleanName := cleanFileName; - ofi^.ExtPart := stringCopy(extPart); - - { Search for file in directory entries } - if (dirs <> nil) and (LL_size(dirs) > 0) then begin - for i := 0 to LL_Size(dirs) - 1 do begin - tempdir := PDirectory(LL_get(dirs, i)); - otherCFN := cleanString(tempdir^.filename, status); - if compareByteArray8(cleanFileName, otherCFN) and matchExtension(tempdir^.fileExtension, extPart) then begin - ofi^.FirstCluster := dirFirstCluster(tempdir); - ofi^.ByteSize := tempdir^.byteSize; - ofi^.Exists := true; - end; - end; - end; - - ofi^.AllocClusters := fatClustersForBytes(ofi^.ByteSize, - uint32(bootRecord^.spc) * uint32(bootRecord^.sectorSize)); - if ofi^.Exists and (ofi^.FirstCluster >= 2) and (ofi^.ByteSize > 0) then - ofi^.Extents := fatBuildExtentList(volume, ofi^.FirstCluster, bootRecord); - - kfree(void(namePart)); - kfree(void(extPart)); - if dirs <> nil then LL_Free(dirs); - kfree(puint32(status)); - - fileSize := ofi^.ByteSize; - fat32OpenFile := pointer(ofi); - push_trace('driver.storage.fs.fat32.openFile.exit'); -end; - -{ Called by VFS on CloseFile to free the per-file context. } -procedure fat32CloseFile(ctx : pointer); -var - ofi : PFATOpenFile; - fatCache : PFATCache; - dirBuf : puint32; - rawDir : PDirectory; - loc : TDirEntryLocation; -begin - if ctx = nil then exit; - ofi := PFATOpenFile(ctx); - if ofi^.FatDirty and (ofi^.Volume <> nil) then begin - fatCache := fatCacheGet(ofi^.Volume); - if fatCache <> nil then - fatCacheFlush(fatCache, ofi^.Volume); - end; - if ofi^.MetaDirty and (ofi^.Volume <> nil) and (ofi^.BootRecord <> nil) then begin - dirBuf := puint32(kalloc(ofi^.BootRecord^.sectorSize)); - if (dirBuf <> nil) and locateDirEntry(ofi^.Volume, ofi^.DirCluster, ofi^.BootRecord, - ofi^.CleanName, ofi^.ExtPart, loc, dirBuf) then begin - rawDir := @PDirectory(dirBuf)[loc.EntryIdx]; - rawDir^.byteSize := ofi^.ByteSize; - if ofi^.FirstCluster <> 0 then - setDirFirstCluster(rawDir, ofi^.FirstCluster); - driver.storage.mgr.storage_write(ofi^.Volume^.device, loc.SectorLBA, 1, dirBuf); - end; - if dirBuf <> nil then - kfree(dirBuf); - end; - if ofi^.ExtPart <> nil then - kfree(void(ofi^.ExtPart)); - if ofi^.Extents <> nil then - LL_Free(ofi^.Extents); - if ofi^.ScratchSector <> nil then - kfree(ofi^.ScratchSector); - kfree(puint32(ofi)); -end; - -{ Write byteCount bytes to a file beginning at byte offset. - If offset + byteCount exceeds the current file size, the file is extended - by allocating new clusters. Returns the number of bytes actually written. } -function writeFileAtOffset(volume : PStorage_Volume; directory : pchar; - fileName : pchar; offset : uint32; - buffer : puint32; byteCount : uint32; - ctx : pointer) : uint32; -const - MAX_RUN_SECTORS = 2048; { cap per multi-sector write = 1MB } -var - lookup : TFileLookup; - run : TRunInfo; - ofi : PFATOpenFile; - i : uint32; - cluster : uint32; - dataStart : uint32; - cleanFileName : byteArray8; - otherCFN : byteArray8; - tempdir : PDirectory; - namePart : pchar; - extPart : pchar; - ioBuf : puint32; - bytesPerCluster : uint32; - startClusterIdx : uint32; - inClusterOffset : uint32; - chainPos : uint32; - curCluster : uint32; - srcPos : uint32; - remaining : uint32; - fileSize : uint32; - origByteSize : uint32; - newEnd : uint32; - oldClusters : uint32; - needClusters : uint32; - targetClusters : uint32; - extraClusters : uint32; - newExtents : PLinkedListBase; - lastCluster : uint32; - nextFat : uint32; - extent : PClusterExtent; - { dir entry update — raw sector scan (Lesson 18) } - dirCluster : uint32; - dirBuf : puint32; - rawDir : PDirectory; - loc : TDirEntryLocation; - { Multi-sector write variables } - spc : uint32; - secSize : uint32; - skipBytes : uint32; - copyBytes : uint32; - firstSectorOff : uint32; - partialBytes : uint32; - fullSectors : uint32; - batchBytes : uint32; - sectorLBA : uint32; - fatCache : PFATCache; - chainWalkLimit : uint32; - firstNewCluster : uint32; - reserveClusters : uint32; -begin - push_trace('driver.storage.fs.fat32.writeFileAtOffset.enter'); - writeFileAtOffset := 0; - ofi := PFATOpenFile(ctx); - - if ofi <> nil then begin - { Fast path: use cached open-file metadata } - lookup.BootRecord := ofi^.BootRecord; - lookup.Dirs := nil; - lookup.Dir := nil; - lookup.Status := nil; - lookup.Exists := ofi^.Exists; - dataStart := ofi^.DataStart; - dirCluster := ofi^.DirCluster; - cleanFileName := ofi^.CleanName; - extPart := ofi^.ExtPart; - namePart := nil; - if lookup.Exists then begin - origByteSize := ofi^.ByteSize; - cluster := ofi^.FirstCluster; - end else begin - origByteSize := 0; - cluster := 0; - end; - end else begin - { Slow path: rediscover everything from disk } - lookup.Status := puint32(kalloc(sizeof(uint32))); - lookup.Status^ := 0; - lookup.BootRecord := readBootRecord(volume); - lookup.Dirs := readDirectory(volume, directory, lookup.Status); - lookup.Exists := false; - dataStart := fatDataStartLBA(volume, lookup.BootRecord); - - splitFileNameParts(fileName, namePart, extPart); - cleanFileName := cleanString(namePart, lookup.Status); - - if LL_size(lookup.Dirs) > 0 then begin - for i := 0 to LL_Size(lookup.Dirs) - 1 do begin - tempdir := PDirectory(LL_get(lookup.Dirs, i)); - otherCFN := cleanString(tempdir^.filename, lookup.Status); - if compareByteArray8(cleanFileName, otherCFN) and matchExtension(tempdir^.fileExtension, extPart) then begin - lookup.Dir := tempdir; - lookup.Exists := true; - end; - end; - end; - - { Determine parent cluster (Lesson 18: root has no '.' entry) } - if (directory = nil) or (stringSize(directory) = 0) then - dirCluster := lookup.BootRecord^.rootCluster - else begin - if LL_size(lookup.Dirs) > 0 then begin - tempdir := PDirectory(LL_get(lookup.Dirs, 0)); - if tempdir^.fileName[0] = '.' then - dirCluster := dirFirstCluster(tempdir) - else - dirCluster := lookup.BootRecord^.rootCluster; - end else - dirCluster := lookup.BootRecord^.rootCluster; - end; - - if lookup.Exists then begin - origByteSize := lookup.Dir^.byteSize; - cluster := dirFirstCluster(lookup.Dir); - end else begin - origByteSize := 0; - cluster := 0; - end; - end; - fileSize := origByteSize; - if lookup.BootRecord^.sectorSize > 0 then - chainWalkLimit := (lookup.BootRecord^.FATSize * lookup.BootRecord^.sectorSize) div 4 - else - chainWalkLimit := 0; - - if not lookup.Exists then begin - { --- Create a new zero-length file entry on disk --- } - { dirCluster, cleanFileName, extPart already set above } - - dirBuf := puint32(kalloc(lookup.BootRecord^.sectorSize)); - - if not locateFreeDirEntry(volume, dirCluster, lookup.BootRecord, loc, dirBuf) then begin - kfree(dirBuf); - io.syslog.logln('FAT32', 'writeFileAtOffset: exit-A no free dir slot'); - if ofi = nil then begin - if namePart <> nil then kfree(void(namePart)); - if extPart <> nil then kfree(void(extPart)); - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); - end; - exit; - end; - - rawDir := @PDirectory(dirBuf)[loc.EntryIdx]; - memset(uint32(rawDir), 0, sizeof(TDirectory)); - rawDir^.fileName := cleanFileName; - rawDir^.attributes := 0; { regular file } - fillFatExt(rawDir^.fileExtension, extPart); - rawDir^.clusterLow := 0; - rawDir^.clusterHigh := 0; - rawDir^.byteSize := 0; - driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, dirBuf); - - kfree(dirBuf); - - { Invalidate chain cache — new file } - sioc_valid := false; - end; - newEnd := offset + byteCount; - bytesPerCluster := uint32(lookup.BootRecord^.spc) * uint32(lookup.BootRecord^.sectorSize); - - { ---- Extend file if writing past EOF ---- } - if newEnd > fileSize then begin - if ofi <> nil then - oldClusters := ofi^.AllocClusters - else - oldClusters := fatClustersForBytes(fileSize, bytesPerCluster); - needClusters := fatClustersForBytes(newEnd, bytesPerCluster); - targetClusters := needClusters; - if (ofi <> nil) and (needClusters > oldClusters) then begin - reserveClusters := fatPreallocClusterCount(bytesPerCluster); - targetClusters := oldClusters + reserveClusters; - if targetClusters < needClusters then - targetClusters := needClusters; - end; - - if targetClusters > oldClusters then begin - extraClusters := targetClusters - oldClusters; - if oldClusters > 0 then begin - { Walk to last cluster using cache if available } - if sioc_valid and (sioc_vol = volume) and (sioc_fileClust = cluster) then - lastCluster := sioc_chainClust - else - lastCluster := cluster; - nextFat := readFat(volume, lastCluster, lookup.BootRecord); - while ((nextFat and $0FFFFFFF) < $0FFFFFF8) and (nextFat <> 0) do begin - if chainWalkLimit = 0 then - break; - chainWalkLimit := chainWalkLimit - 1; - if chainWalkLimit = 0 then begin - io.syslog.logln('FAT32', 'writeFileAtOffset: FAT loop detected while finding tail'); - if ofi = nil then begin - if namePart <> nil then kfree(void(namePart)); - if extPart <> nil then kfree(void(extPart)); - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); - end; - exit; - end; - lastCluster := nextFat; - nextFat := readFat(volume, lastCluster, lookup.BootRecord); - end; - end else begin - { Zero-length file: first new cluster becomes start cluster } - lastCluster := 0; - end; - - if fatFindContiguousFreeRange(volume, extraClusters, lookup.BootRecord, firstNewCluster) then begin - fatLinkContiguousRange(volume, lookup.BootRecord, lastCluster, firstNewCluster, extraClusters, lastCluster); - if oldClusters = 0 then - cluster := firstNewCluster; - if ofi <> nil then begin - if ofi^.Extents = nil then - ofi^.Extents := LL_New(sizeof(TClusterExtent)); - if ofi^.Extents <> nil then - fatAppendExtent(ofi^.Extents, firstNewCluster, extraClusters); - end; - end else begin - newExtents := findFreeClusterExtents(volume, extraClusters, lookup.BootRecord); - if newExtents = nil then begin - io.syslog.logln('FAT32', 'writeFileAtOffset: exit-B disk full'); - { Undo just-created dir entry if file was new } - if not lookup.Exists then begin - dirBuf := puint32(kalloc(lookup.BootRecord^.sectorSize)); - driver.storage.mgr.storage_read(volume^.device, loc.SectorLBA, 1, dirBuf); - PDirectory(dirBuf)[loc.EntryIdx].fileName[0] := char($E5); - driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, dirBuf); - kfree(dirBuf); - end; - if ofi = nil then begin - if namePart <> nil then kfree(void(namePart)); - if extPart <> nil then kfree(void(extPart)); - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); - end; - exit; - end; - - { Link new extents } - if LL_Size(newExtents) > 0 then begin - for i := 0 to LL_Size(newExtents) - 1 do begin - extent := PClusterExtent(LL_Get(newExtents, i)); - fatLinkContiguousRange(volume, lookup.BootRecord, lastCluster, - extent^.StartCluster, extent^.ClusterCount, lastCluster); - if (ofi <> nil) and (extent <> nil) then begin - if ofi^.Extents = nil then - ofi^.Extents := LL_New(sizeof(TClusterExtent)); - if ofi^.Extents <> nil then - fatAppendExtent(ofi^.Extents, extent^.StartCluster, extent^.ClusterCount); - end; - if (oldClusters = 0) and (i = 0) then - cluster := extent^.StartCluster; - end; - end; - LL_Free(newExtents); - end; - - if ofi <> nil then - ofi^.FatDirty := true; - - if ofi <> nil then - ofi^.AllocClusters := targetClusters; - - { Update chain cache: last new cluster is the new tail } - updateSIOC(volume, cluster, targetClusters - 1, lastCluster); - - end; - - fileSize := newEnd; - end; - - { ---- Walk to starting cluster using cache, then write sector by sector ---- } - startClusterIdx := offset div bytesPerCluster; - inClusterOffset := offset mod bytesPerCluster; - - if not seekToClusterIndex(volume, cluster, startClusterIdx, lookup.BootRecord, curCluster, chainPos) then begin - io.syslog.logln('FAT32', 'writeFileAtOffset: exit-C chain broke seeking to start'); - if ofi = nil then begin - if namePart <> nil then kfree(void(namePart)); - if extPart <> nil then kfree(void(extPart)); - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); - end; - exit; - end; - - spc := uint32(lookup.BootRecord^.spc); - secSize := uint32(lookup.BootRecord^.sectorSize); - if (ofi <> nil) and (ofi^.ScratchSector <> nil) and (ofi^.ScratchSize >= secSize) then - ioBuf := ofi^.ScratchSector - else - ioBuf := nil; - remaining := byteCount; - srcPos := 0; - - while (remaining > 0) do begin - { Build contiguous cluster run starting at curCluster } - run.StartCluster := curCluster; - buildContiguousRun(volume, curCluster, spc, MAX_RUN_SECTORS, lookup.BootRecord, run.RunLen, run.LastCluster); - curCluster := run.LastCluster; - - run.RunSectors := run.RunLen * spc; - run.RunBytes := run.RunSectors * secSize; - run.LBA := clusterToLBA(dataStart, spc, run.StartCluster); - - if srcPos = 0 then - skipBytes := inClusterOffset - else - skipBytes := 0; - - copyBytes := run.RunBytes - skipBytes; - if copyBytes > remaining then - copyBytes := remaining; - - firstSectorOff := skipBytes mod secSize; - - { Phase 1: Partial first sector — read-modify-write } - if firstSectorOff > 0 then begin - if ioBuf = nil then - ioBuf := puint32(kalloc(secSize)); - if ioBuf = nil then - break; - sectorLBA := run.LBA + (skipBytes div secSize); - partialBytes := secSize - firstSectorOff; - if partialBytes > copyBytes then - partialBytes := copyBytes; - driver.storage.mgr.storage_read(volume^.device, sectorLBA, 1, ioBuf); - core.util.memcpy(uint32(buffer) + srcPos, uint32(ioBuf) + firstSectorOff, partialBytes); - driver.storage.mgr.storage_write(volume^.device, sectorLBA, 1, ioBuf); - srcPos := srcPos + partialBytes; - remaining := remaining - partialBytes; - copyBytes := copyBytes - partialBytes; - skipBytes := skipBytes + partialBytes; - end; - - { Phase 2: Full sectors — batch write directly from caller buffer } - fullSectors := copyBytes div secSize; - if fullSectors > 0 then begin - sectorLBA := run.LBA + (skipBytes div secSize); - batchBytes := fullSectors * secSize; - driver.storage.mgr.storage_write(volume^.device, sectorLBA, fullSectors, - puint32(uint32(buffer) + srcPos)); - srcPos := srcPos + batchBytes; - remaining := remaining - batchBytes; - copyBytes := copyBytes - batchBytes; - skipBytes := skipBytes + batchBytes; - end; - - { Phase 3: Partial last sector — read-modify-write } - if copyBytes > 0 then begin - if ioBuf = nil then - ioBuf := puint32(kalloc(secSize)); - if ioBuf = nil then - break; - sectorLBA := run.LBA + (skipBytes div secSize); - driver.storage.mgr.storage_read(volume^.device, sectorLBA, 1, ioBuf); - core.util.memcpy(uint32(buffer) + srcPos, uint32(ioBuf), copyBytes); - driver.storage.mgr.storage_write(volume^.device, sectorLBA, 1, ioBuf); - srcPos := srcPos + copyBytes; - remaining := remaining - copyBytes; - end; - - { Update chain cache to last cluster in this run } - updateSIOC(volume, cluster, chainPos + run.RunLen - 1, curCluster); - chainPos := chainPos + run.RunLen; - - if remaining > 0 then begin - nextFat := readFat(volume, curCluster, lookup.BootRecord); - if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then begin - io.syslog.log('FAT32', 'writeFileAtOffset: exit-D chain broke in write loop, srcPos='); - io.syslog.writeint(srcPos); - io.syslog.writestring(' remaining='); - io.syslog.writeint(remaining); - io.syslog.writestring(' cluster='); - io.syslog.writeint(curCluster); - io.syslog.writestring(' fat='); - io.syslog.writehexln(nextFat); - break; - end; - curCluster := nextFat; - end; - end; - - if (ioBuf <> nil) and ((ofi = nil) or (ioBuf <> ofi^.ScratchSector)) then - kfree(ioBuf); - - { Flush dirty FAT cache entries to disk only for uncached callers. - Open-file callers defer this to fat32CloseFile to avoid flushing on every chunk. } - if ofi = nil then begin - fatCache := fatCacheGet(volume); - if fatCache <> nil then - fatCacheFlush(fatCache, volume); - end; - - { ---- Update directory entry byteSize if file was extended ---- } - { Only commit actual bytes written — never advance size past what was really written (Lesson 22) } - if (srcPos > 0) and ((offset + srcPos) > origByteSize) then begin - if ofi <> nil then begin - ofi^.MetaDirty := true; - end else begin - { Lesson 18: search raw directory sectors, don't use LL indices. - dirCluster, cleanFileName, extPart were set once in the discovery phase. } - dirBuf := puint32(kalloc(lookup.BootRecord^.sectorSize)); - - if locateDirEntry(volume, dirCluster, lookup.BootRecord, cleanFileName, extPart, loc, dirBuf) then begin - rawDir := @PDirectory(dirBuf)[loc.EntryIdx]; - rawDir^.byteSize := offset + srcPos; - { If file was zero-length, update cluster pointers too } - if origByteSize = 0 then - setDirFirstCluster(rawDir, cluster); - driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, dirBuf); - end; - - kfree(dirBuf); - end; - end; - - { Update cached open-file metadata so the next read/write sees the new state } - if ofi <> nil then begin - if (srcPos > 0) and ((offset + srcPos) > ofi^.ByteSize) then - ofi^.ByteSize := offset + srcPos; - if (ofi^.FirstCluster = 0) and (cluster <> 0) then begin - ofi^.FirstCluster := cluster; - ofi^.Exists := true; - ofi^.MetaDirty := true; - end; - end; - - if ofi = nil then begin - if namePart <> nil then kfree(void(namePart)); - if extPart <> nil then kfree(void(extPart)); - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); - end; - writeFileAtOffset := srcPos; - push_trace('driver.storage.fs.fat32.writeFileAtOffset.exit'); -end; - -{ Read byteCount bytes from a file beginning at byte offset 'offset'. - Returns the number of bytes actually read (may be less than byteCount if EOF reached). - - NOTE: This implementation reads sectors sequentially from the first cluster of the - file, consistent with the existing readFile behaviour. It does NOT follow the FAT - chain between clusters \u2014 it assumes contiguous allocation. This is acceptable for - the initial streaming implementation. A future improvement should cache the last - visited (offset, cluster) pair in the open-file entry to make sequential chunked - reads O(1) per call instead of O(n) in the FAT chain length. } - -function getFileSize(volume : PStorage_Volume; directory : pchar; fileName : pchar) : uint32; -var - bootRecord : PBootRecord; - dirs : PLinkedListBase; - tempdir : PDirectory; - statusOut : puint32; - i : uint32; - cleanFileName : byteArray8; - otherCFN : byteArray8; - namePart : pchar; - extPart : pchar; - fsize : uint32; -begin - push_trace('driver.storage.fs.fat32.getFileSize.enter'); - getFileSize := 0; - - statusOut := puint32(kalloc(sizeof(uint32))); - statusOut^ := 0; - bootRecord := readBootRecord(volume); - dirs := readDirectory(volume, directory, statusOut); - - splitFileNameParts(fileName, namePart, extPart); - - fsize := 0; - if LL_size(dirs) > 0 then begin - cleanFileName := cleanString(namePart, statusOut); - for i := 0 to LL_Size(dirs) - 1 do begin - tempdir := PDirectory(LL_get(dirs, i)); - otherCFN := cleanString(tempdir^.filename, statusOut); - if compareByteArray8(cleanFileName, otherCFN) and matchExtension(tempdir^.fileExtension, extPart) then begin - fsize := tempdir^.byteSize; - break; - end; - end; - end; - - kfree(void(namePart)); - kfree(void(extPart)); - LL_Free(dirs); - kfree(puint32(bootRecord)); - kfree(puint32(statusOut)); - getFileSize := fsize; - push_trace('driver.storage.fs.fat32.getFileSize.exit'); -end; - -function readFileAtOffset(volume : PStorage_Volume; directory : pchar; fileName : pchar; offset : uint32; buffer : puint32; byteCount : uint32; ctx : pointer) : uint32; -const - MAX_RUN_SECTORS = 2048; { cap per multi-sector read = 1 MB } -var - lookup : TFileLookup; - run : TRunInfo; - ofi : PFATOpenFile; - i : uint32; - cluster : uint32; - dataStart : uint32; - cleanFileName : byteArray8; - otherCFN : byteArray8; - tempdir : PDirectory; - namePart : pchar; - extPart : pchar; - bytesPerCluster : uint32; - startClusterIdx : uint32; - inClusterOffset : uint32; - chainPos : uint32; - curCluster : uint32; - destPos : uint32; - remaining : uint32; - fileSize : uint32; - nextFat : uint32; - spc : uint32; - secSize : uint32; - { Multi-sector run variables } - runBuf : puint32; - skipBytes : uint32; { bytes to skip at start of run buffer } - copyBytes : uint32; - chunkSectors : uint32; - chunkBytes : uint32; -begin - push_trace('driver.storage.fs.fat32.readFileAtOffset.enter'); - readFileAtOffset := 0; - ofi := PFATOpenFile(ctx); - - if ofi <> nil then begin - { Fast path: use cached open-file metadata } - lookup.BootRecord := ofi^.BootRecord; - lookup.Dirs := nil; - lookup.Dir := nil; - lookup.Status := nil; - lookup.Exists := ofi^.Exists; - dataStart := ofi^.DataStart; - if lookup.Exists then begin - fileSize := ofi^.ByteSize; - cluster := ofi^.FirstCluster; - end else begin - fileSize := 0; - cluster := 0; - end; - end else begin - { Slow path: rediscover everything from disk } - lookup.Status := puint32(kalloc(sizeof(uint32))); - lookup.Status^ := 0; - lookup.BootRecord := readBootRecord(volume); - lookup.Dirs := readDirectory(volume, directory, lookup.Status); - lookup.Exists := false; - dataStart := fatDataStartLBA(volume, lookup.BootRecord); - - splitFileNameParts(fileName, namePart, extPart); - - if LL_size(lookup.Dirs) > 0 then begin - cleanFileName := cleanString(namePart, lookup.Status); - for i := 0 to LL_Size(lookup.Dirs) - 1 do begin - tempdir := PDirectory(LL_get(lookup.Dirs, i)); - otherCFN := cleanString(tempdir^.filename, lookup.Status); - if compareByteArray8(cleanFileName, otherCFN) and matchExtension(tempdir^.fileExtension, extPart) then begin - lookup.Dir := tempdir; - lookup.Exists := true; - end; - end; - end; - - kfree(void(namePart)); - kfree(void(extPart)); - - if lookup.Exists then begin - fileSize := lookup.Dir^.byteSize; - cluster := dirFirstCluster(lookup.Dir); - end else begin - fileSize := 0; - cluster := 0; - end; - end; - - if not lookup.Exists then begin - if ofi = nil then begin - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); - end; - push_trace('driver.storage.fs.fat32.readFileAtOffset.notFound'); - exit; - end; - - if offset >= fileSize then begin - if ofi = nil then begin - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); - end; - exit; - end; - - { Clamp read length to not exceed file size } - remaining := byteCount; - if offset + remaining > fileSize then - remaining := fileSize - offset; - - spc := uint32(lookup.BootRecord^.spc); - secSize := uint32(lookup.BootRecord^.sectorSize); - bytesPerCluster := spc * secSize; - startClusterIdx := offset div bytesPerCluster; - inClusterOffset := offset mod bytesPerCluster; - - { Determine curCluster at startClusterIdx using SIOC cache } - if not seekToClusterIndex(volume, cluster, startClusterIdx, lookup.BootRecord, curCluster, chainPos) then begin - if ofi = nil then begin - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); - end; - exit; - end; - - destPos := 0; - - while (remaining > 0) do begin - { Build a contiguous cluster run starting at curCluster } - run.StartCluster := curCluster; - buildContiguousRun(volume, curCluster, spc, MAX_RUN_SECTORS, lookup.BootRecord, run.RunLen, curCluster); - - run.RunSectors := run.RunLen * spc; - run.RunBytes := run.RunSectors * secSize; - run.LBA := clusterToLBA(dataStart, spc, run.StartCluster); - - { Calculate how many bytes from this run we actually need } - if destPos = 0 then - skipBytes := inClusterOffset - else - skipBytes := 0; - - copyBytes := run.RunBytes - skipBytes; - if copyBytes > remaining then - copyBytes := remaining; - - { Determine if we can read directly into caller's buffer (aligned, full run) } - if (skipBytes = 0) and (copyBytes >= run.RunBytes) then begin - { Direct read into output buffer — no temp allocation } - driver.storage.mgr.storage_read(volume^.device, run.LBA, run.RunSectors, - puint32(uint32(buffer) + destPos)); - destPos := destPos + copyBytes; - remaining := remaining - copyBytes; - end else if (skipBytes = 0) and ((copyBytes mod secSize) = 0) then begin - { Partial run but sector-aligned — read only needed sectors } - chunkSectors := copyBytes div secSize; - driver.storage.mgr.storage_read(volume^.device, run.LBA, chunkSectors, - puint32(uint32(buffer) + destPos)); - destPos := destPos + copyBytes; - remaining := remaining - copyBytes; - end else begin - { Unaligned — need temp buffer for partial first/last sectors } - { Only allocate for sectors we actually touch } - chunkSectors := (skipBytes + copyBytes + secSize - 1) div secSize; - if chunkSectors > run.RunSectors then chunkSectors := run.RunSectors; - chunkBytes := chunkSectors * secSize; - runBuf := puint32(kalloc(chunkBytes)); - run.LBA := clusterToLBA(dataStart, spc, run.StartCluster) + (skipBytes div secSize); - - { Adjust skipBytes for sectors we're skipping entirely } - skipBytes := skipBytes mod secSize; - driver.storage.mgr.storage_read(volume^.device, run.LBA, chunkSectors, runBuf); - core.util.memcpy(uint32(runBuf) + skipBytes, uint32(buffer) + destPos, copyBytes); - kfree(runBuf); - destPos := destPos + copyBytes; - remaining := remaining - copyBytes; - end; - - { Update SIOC chain cache to last cluster in this run } - updateSIOC(volume, cluster, chainPos + run.RunLen - 1, curCluster); - chainPos := chainPos + run.RunLen; - - { Advance to next cluster beyond this run } - if remaining > 0 then begin - nextFat := readFat(volume, curCluster, lookup.BootRecord); - if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then - break; - curCluster := nextFat; - end; - end; - - if ofi = nil then begin - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); - end; - readFileAtOffset := destPos; - push_trace('driver.storage.fs.fat32.readFileAtOffset.exit'); -end; - -procedure readFileAtOffsetAsync(volume : PStorage_Volume; directory : pchar; fileName : pchar; - offset : uint32; buffer : puint32; byteCount : uint32; - ctx : pointer; bytesRead : puint32; - callback : TIOCallback; callbackData : pointer); -var - ofi : PFATOpenFile; - asyncCtx : PFATFileIOAsyncCtx; - sectorLBA : uint32; - chunkSectors : uint32; - err : TError; -begin - if bytesRead <> nil then bytesRead^ := 0; - ofi := PFATOpenFile(ctx); - - if (ofi = nil) or (buffer = nil) or (byteCount = 0) then begin - if callback <> nil then callback(eInvalidArgument, callbackData); - exit; - end; - - if not ofi^.Exists then begin - if callback <> nil then callback(eFileDoesNotExist, callbackData); - exit; - end; - - if offset >= ofi^.ByteSize then begin - if callback <> nil then callback(eNone, callbackData); - exit; - end; - - if offset + byteCount > ofi^.ByteSize then - byteCount := ofi^.ByteSize - offset; - - if ((offset mod ofi^.BootRecord^.sectorSize) <> 0) or ((byteCount mod ofi^.BootRecord^.sectorSize) <> 0) then begin - if bytesRead <> nil then - bytesRead^ := readFileAtOffset(volume, directory, fileName, offset, buffer, byteCount, ctx); - if callback <> nil then callback(eNone, callbackData); - exit; - end; - - asyncCtx := fatTransferAlloc(volume); - if asyncCtx = nil then begin - if callback <> nil then callback(eOutOfMemory, callbackData); - exit; - end; - asyncCtx^.Mode := famRead; - asyncCtx^.State := ftsPrepare; - asyncCtx^.Volume := volume; - asyncCtx^.OpenFile := ofi; - asyncCtx^.Buffer := buffer; - asyncCtx^.Offset := offset; - asyncCtx^.ByteCount := byteCount; - asyncCtx^.BytesOut := bytesRead; - asyncCtx^.Callback := callback; - asyncCtx^.CallbackData := callbackData; - asyncCtx^.Cluster := ofi^.FirstCluster; - asyncCtx^.DataStart := ofi^.DataStart; - asyncCtx^.BootRecord := ofi^.BootRecord; - asyncCtx^.LastError := eNone; - asyncCtx^.CursorValid := false; - asyncCtx^.ExtentValid := false; - if fat32_fileio_prepare_single_run(asyncCtx, sectorLBA, chunkSectors) and - (asyncCtx^.ChunkBytes = asyncCtx^.ByteCount) then begin - err := driver.storage.mgr.storage_read_async( - volume^.device, sectorLBA, chunkSectors, buffer, - @fat32_fileio_direct_complete, asyncCtx); - if err <> eNone then begin - fatTransferFree(asyncCtx); - if callback <> nil then callback(err, callbackData); - end; - exit; - end; - fatWorkerEnsure(volume); - if not fatScheduleTransfer(asyncCtx) then begin - fatTransferFree(asyncCtx); - if callback <> nil then callback(eQueueFull, callbackData); - end; -end; - -procedure writeFileAtOffsetAsync(volume : PStorage_Volume; directory : pchar; fileName : pchar; - offset : uint32; buffer : puint32; byteCount : uint32; - ctx : pointer; bytesWritten : puint32; - callback : TIOCallback; callbackData : pointer); -var - ofi : PFATOpenFile; - asyncCtx : PFATFileIOAsyncCtx; - cluster : uint32; - dataStart : uint32; - origByteSize : uint32; - newEnd : uint32; - bytesPerCluster : uint32; - oldClusters : uint32; - needClusters : uint32; - targetClusters : uint32; - extraClusters : uint32; - newExtents : PLinkedListBase; - lastCluster : uint32; - nextFat : uint32; - i : uint32; - extent : PClusterExtent; - dirBuf : puint32; - rawDir : PDirectory; - loc : TDirEntryLocation; - chainWalkLimit : uint32; - firstNewCluster : uint32; - reserveClusters : uint32; - sectorLBA : uint32; - chunkSectors : uint32; - err : TError; -begin - if bytesWritten <> nil then bytesWritten^ := 0; - ofi := PFATOpenFile(ctx); - - if (ofi = nil) or (buffer = nil) or (byteCount = 0) then begin - if callback <> nil then callback(eInvalidArgument, callbackData); - exit; - end; - - if ((offset mod ofi^.BootRecord^.sectorSize) <> 0) or ((byteCount mod ofi^.BootRecord^.sectorSize) <> 0) then begin - if bytesWritten <> nil then - bytesWritten^ := writeFileAtOffset(volume, directory, fileName, offset, buffer, byteCount, ctx); - if callback <> nil then callback(eNone, callbackData); - exit; - end; - - dataStart := ofi^.DataStart; - origByteSize := ofi^.ByteSize; - if ofi^.BootRecord^.sectorSize > 0 then - chainWalkLimit := (ofi^.BootRecord^.FATSize * ofi^.BootRecord^.sectorSize) div 4 - else - chainWalkLimit := 0; - if ofi^.Exists then - cluster := ofi^.FirstCluster - else - cluster := 0; - - if not ofi^.Exists then begin - dirBuf := puint32(kalloc(ofi^.BootRecord^.sectorSize)); - if dirBuf = nil then begin - if callback <> nil then callback(eOutOfMemory, callbackData); - exit; - end; - if not locateFreeDirEntry(volume, ofi^.DirCluster, ofi^.BootRecord, loc, dirBuf) then begin - kfree(dirBuf); - if callback <> nil then callback(eDirectoryFull, callbackData); - exit; - end; - rawDir := @PDirectory(dirBuf)[loc.EntryIdx]; - memset(uint32(rawDir), 0, sizeof(TDirectory)); - rawDir^.fileName := ofi^.CleanName; - rawDir^.attributes := 0; - fillFatExt(rawDir^.fileExtension, ofi^.ExtPart); - rawDir^.clusterLow := 0; - rawDir^.clusterHigh := 0; - rawDir^.byteSize := 0; - driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, dirBuf); - kfree(dirBuf); - ofi^.Exists := true; - sioc_valid := false; - end; - - newEnd := offset + byteCount; - bytesPerCluster := uint32(ofi^.BootRecord^.spc) * uint32(ofi^.BootRecord^.sectorSize); - - if newEnd > origByteSize then begin - oldClusters := ofi^.AllocClusters; - needClusters := fatClustersForBytes(newEnd, bytesPerCluster); - targetClusters := needClusters; - if needClusters > oldClusters then begin - reserveClusters := fatPreallocClusterCount(bytesPerCluster); - targetClusters := oldClusters + reserveClusters; - if targetClusters < needClusters then - targetClusters := needClusters; - end; - - if targetClusters > oldClusters then begin - extraClusters := targetClusters - oldClusters; - if oldClusters > 0 then begin - if sioc_valid and (sioc_vol = volume) and (sioc_fileClust = cluster) then - lastCluster := sioc_chainClust - else - lastCluster := cluster; - nextFat := readFat(volume, lastCluster, ofi^.BootRecord); - while ((nextFat and $0FFFFFFF) < $0FFFFFF8) and (nextFat <> 0) do begin - if chainWalkLimit = 0 then begin - if callback <> nil then callback(eCorruptFilesystem, callbackData); - exit; - end; - chainWalkLimit := chainWalkLimit - 1; - lastCluster := nextFat; - nextFat := readFat(volume, lastCluster, ofi^.BootRecord); - end; - end else - lastCluster := 0; - - if fatFindContiguousFreeRange(volume, extraClusters, ofi^.BootRecord, firstNewCluster) then begin - fatLinkContiguousRange(volume, ofi^.BootRecord, lastCluster, firstNewCluster, extraClusters, lastCluster); - if oldClusters = 0 then - cluster := firstNewCluster; - if ofi^.Extents = nil then - ofi^.Extents := LL_New(sizeof(TClusterExtent)); - if ofi^.Extents <> nil then - fatAppendExtent(ofi^.Extents, firstNewCluster, extraClusters); - end else begin - newExtents := findFreeClusterExtents(volume, extraClusters, ofi^.BootRecord); - if newExtents = nil then begin - if callback <> nil then callback(eDiskFull, callbackData); - exit; - end; - - if LL_Size(newExtents) > 0 then begin - for i := 0 to LL_Size(newExtents) - 1 do begin - extent := PClusterExtent(LL_Get(newExtents, i)); - fatLinkContiguousRange(volume, ofi^.BootRecord, lastCluster, - extent^.StartCluster, extent^.ClusterCount, lastCluster); - if extent <> nil then begin - if ofi^.Extents = nil then - ofi^.Extents := LL_New(sizeof(TClusterExtent)); - if ofi^.Extents <> nil then - fatAppendExtent(ofi^.Extents, extent^.StartCluster, extent^.ClusterCount); - end; - if (oldClusters = 0) and (i = 0) then - cluster := extent^.StartCluster; - end; - end; - LL_Free(newExtents); - end; - - ofi^.FatDirty := true; - ofi^.AllocClusters := targetClusters; - updateSIOC(volume, cluster, targetClusters - 1, lastCluster); - end; - end; - - asyncCtx := fatTransferAlloc(volume); - if asyncCtx = nil then begin - if callback <> nil then callback(eOutOfMemory, callbackData); - exit; - end; - asyncCtx^.Mode := famWrite; - asyncCtx^.State := ftsPrepare; - asyncCtx^.Volume := volume; - asyncCtx^.OpenFile := ofi; - asyncCtx^.Buffer := buffer; - asyncCtx^.Offset := offset; - asyncCtx^.ByteCount := byteCount; - asyncCtx^.BytesOut := bytesWritten; - asyncCtx^.Callback := callback; - asyncCtx^.CallbackData := callbackData; - asyncCtx^.Cluster := cluster; - asyncCtx^.OrigByteSize := origByteSize; - asyncCtx^.DataStart := dataStart; - asyncCtx^.BootRecord := ofi^.BootRecord; - - if (cluster <> 0) and ((offset + byteCount) > ofi^.ByteSize) then - ofi^.MetaDirty := true; - - asyncCtx^.LastError := eNone; - asyncCtx^.CursorValid := false; - asyncCtx^.ExtentValid := false; - if fat32_fileio_prepare_single_run(asyncCtx, sectorLBA, chunkSectors) and - (asyncCtx^.ChunkBytes = asyncCtx^.ByteCount) then begin - err := driver.storage.mgr.storage_write_async( - volume^.device, sectorLBA, chunkSectors, buffer, - @fat32_fileio_direct_complete, asyncCtx); - if err <> eNone then begin - fatTransferFree(asyncCtx); - if callback <> nil then callback(err, callbackData); - end; - exit; - end; - fatWorkerEnsure(volume); - if not fatScheduleTransfer(asyncCtx) then begin - fatTransferFree(asyncCtx); - if callback <> nil then callback(eQueueFull, callbackData); - end; -end; - -procedure init(); -begin - push_trace('driver.storage.fs.fat32.init()'); io.syslog.logln('FAT32', 'init: registering FAT32 filesystem'); - filesystem.sName:= 'FAT32'; - filesystem.system_id:= $01; - filesystem.readDirCallback:= @readDirectoryGen; - filesystem.createDirCallback:= @writeDirectoryGen; - filesystem.createcallback:= @create_volume; + filesystem.sName := 'FAT32'; + filesystem.system_id := $0B; + filesystem.readDirCallback := @FAT32ReadDirectory; + filesystem.createDirCallback := @FAT32CreateDirectory; + filesystem.createCallback := @create_volume; filesystem.createAsyncCallback := @create_volume_async; - filesystem.detectcallback:= @detect_volumes; - filesystem.readOffsetCallback := @readFileAtOffset; - filesystem.writeOffsetCallback := @writeFileAtOffset; - filesystem.readOffsetAsyncCallback := @readFileAtOffsetAsync; - filesystem.writeOffsetAsyncCallback := @writeFileAtOffsetAsync; - filesystem.fileSizeCallback := @getFileSize; + filesystem.detectCallback := @detect_volumes; + filesystem.readOffsetCallback := @FAT32ReadFileAtOffset; + filesystem.writeOffsetCallback := @FAT32WriteFileAtOffset; + filesystem.readOffsetAsyncCallback := @FAT32ReadFileAtOffsetAsync; + filesystem.writeOffsetAsyncCallback := @FAT32WriteFileAtOffsetAsync; + filesystem.fileSizeCallback := @FAT32GetFileSize; filesystem.identifyCallback := @identify_volume; - filesystem.deleteFileCallback := @deleteFile; - filesystem.deleteDirCallback := @deleteDir; - filesystem.renameFileCallback := @renameFile; - filesystem.openFileCallback := @fat32OpenFile; - filesystem.closeFileCallback := @fat32CloseFile; - { Async callbacks: nil - VFS falls through to the sync callbacks above. - All disk I/O uses psAwaiting in submit_io_wait: the calling process is - parked cleanly (CPU-free) while the driver.storage.ctl.ahci ISR completes the request. } - filesystem.createDirAsyncCallback := nil; - filesystem.readDirAsyncCallback := nil; + filesystem.deleteFileCallback := @FAT32DeleteFile; + filesystem.deleteDirCallback := @FAT32DeleteDir; + filesystem.renameFileCallback := @FAT32RenameFile; + filesystem.openFileCallback := @FAT32OpenFile; + filesystem.closeFileCallback := @FAT32CloseFile; + filesystem.createDirAsyncCallback := nil; + filesystem.readDirAsyncCallback := nil; driver.storage.fs.mgr.register_filesystem(@filesystem); - io.syslog.logln('FAT32', 'init: done'); end; initialization - boot.mgr.registerBoot('driver.storage.fs.fat32', @Init, 'FAT32 Filesystem', 'driver.storage.fs.mgr'); + boot.mgr.registerBoot('driver.storage.fs.fat32', @init, 'FAT32 Filesystem', 'driver.storage.fs.mgr'); end. diff --git a/src/driver/storage/fs/driver.storage.fs.flatfs.pas b/src/driver/storage/fs/driver.storage.fs.flatfs.pas index d679c5ba..133c3a30 100644 --- a/src/driver/storage/fs/driver.storage.fs.flatfs.pas +++ b/src/driver/storage/fs/driver.storage.fs.flatfs.pas @@ -34,6 +34,7 @@ uses io.syslog, debug.tracer, core.util, arch.x86.util, + driver.storage.vol.mbr, driver.storage.vol.mgr; type @@ -763,13 +764,38 @@ end; function identify_volume(volume : PStorage_Volume) : boolean; var - buffer : puint32; - bufSize : uint32; + buffer : puint32; + bufSize : uint32; + mbr : PMaster_Boot_Record; + freeMbr : boolean; + slot : sint32; + i : uint32; begin push_trace('driver.storage.fs.flatfs.identify_volume'); identify_volume := false; if volume^.device^.dispatchRead = nil then exit; + mbr := driver.storage.mgr.get_cached_mbr(volume^.device); + freeMbr := false; + if mbr = nil then begin + mbr := driver.storage.mgr.read_mbr(volume^.device); + freeMbr := mbr <> nil; + end; + + slot := -1; + if mbr <> nil then begin + for i := 0 to 3 do begin + if mbr^.partition[i].LBA_start = volume^.sectorStart then begin + slot := sint32(i); + break; + end; + end; + if (slot >= 0) and (mbr^.partition[slot].system_id <> filesystem.system_id) then begin + if freeMbr then kfree(void(mbr)); + exit; + end; + end; + bufSize := volume^.device^.sectorSize; if bufSize < 512 then bufSize := 512; buffer := puint32(kalloc(bufSize)); @@ -783,6 +809,7 @@ begin end; kfree(buffer); + if freeMbr then kfree(void(mbr)); end; function readDirectoryEntries(volume : PStorage_Volume; directory : pchar; status : PuInt32) : PLinkedListBase; diff --git a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.core.pas b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.core.pas new file mode 100644 index 00000000..712081d7 --- /dev/null +++ b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.core.pas @@ -0,0 +1,2403 @@ +unit driver.storage.fs.fat32.core; + +interface + +uses + core.ds.lists, + memory.heap, + driver.storage.mgr, + driver.storage.types, + core.strings, + core.util, arch.x86.util, + debug.tracer, + io.syslog, + driver.storage.fs.fat32.types; + +function FAT32GetVolumeCtx(volume : PStorage_Volume) : PFATVolumeCtx; +function FAT32GetVolumeInfo(volume : PStorage_Volume) : PFATVolumeInfo; +procedure FAT32Flush(volume : PStorage_Volume); +procedure FAT32ReleaseVolumeCtx(volume : PStorage_Volume); + +function FAT32OpenFile(volume : PStorage_Volume; directory : pchar; fileName : pchar; var fileSize : uint32) : pointer; +procedure FAT32CloseFile(ctx : pointer); +function FAT32GetFileSize(volume : PStorage_Volume; directory : pchar; fileName : pchar) : uint32; +function FAT32ReadDirectory(volume : PStorage_Volume; directory : pchar; statusOut : puint32) : PLinkedListBase; +procedure FAT32CreateDirectory(volume : PStorage_Volume; directory : pchar; dirName : pchar; attributes : uint32; statusOut : puint32); +procedure FAT32DeleteFile(volume : PStorage_Volume; filePath : pchar; statusOut : puint32); +procedure FAT32DeleteDir(volume : PStorage_Volume; path : pchar; statusOut : puint32); +procedure FAT32RenameFile(volume : PStorage_Volume; filePath : pchar; newName : pchar; statusOut : puint32); + +function FAT32TransferAlloc(volume : PStorage_Volume) : PFATTransferCtx; +procedure FAT32TransferFree(ctx : PFATTransferCtx); + +function FAT32FindRunForOffset(ofi : PFATOpenFile; fileOffset : uint32; + hintValid : boolean; hintRunIdx : uint32; hintFileCluster : uint32; + var outRunIdx : uint32; var outRunFileCluster : uint32; + var outStartCluster : uint32; var outClusterOffset : uint32; + var outAvailableBytes : uint32) : boolean; + +function FAT32EnsureCapacityForWrite(ofi : PFATOpenFile; offset : uint32; byteCount : uint32) : TError; +function FAT32EnsureDirEntry(ofi : PFATOpenFile) : TError; +function FAT32GetReadLimit(ofi : PFATOpenFile; offset : uint32; byteCount : uint32) : uint32; +function FAT32ClusterToLBA(info : PFATVolumeInfo; cluster : uint32) : uint32; + +implementation + +type + PFATTempRun = ^TFATRun; + PFATVolumeCtxNode = ^TFATVolumeCtxNode; + TFATVolumeCtxNode = record + Volume : PStorage_Volume; + Ctx : PFATVolumeCtx; + Next : PFATVolumeCtxNode; + end; + +var + sioc_allocHint : uint32 = 2; + sioc_allocVol : PStorage_Volume = nil; + fatCtxListHead : PFATVolumeCtxNode = nil; + +function readFat(volume : PStorage_Volume; cluster : uint32; bootRecord : PBootRecord) : uint32; forward; +procedure writeFat(volume : PStorage_Volume; cluster : uint32; value : uint32; bootRecord : PBootRecord); forward; + +function isEndOfChain(value : uint32) : boolean; +begin + isEndOfChain := (value and $0FFFFFFF) >= $0FFFFFF8; +end; + +function isBadCluster(value : uint32) : boolean; +begin + isBadCluster := (value and $0FFFFFFF) = $0FFFFFF7; +end; + +function FAT32ClusterToLBA(info : PFATVolumeInfo; cluster : uint32) : uint32; +begin + if cluster < 2 then + FAT32ClusterToLBA := info^.DataStart + else + FAT32ClusterToLBA := info^.DataStart + ((cluster - 2) * info^.BootRecord.spc); +end; + +function dirFirstCluster(dir : PDirectory) : uint32; +begin + dirFirstCluster := uint32(dir^.clusterLow) or (uint32(dir^.clusterHigh) shl 16); +end; + +procedure setDirFirstCluster(dir : PDirectory; cluster : uint32); +begin + dir^.clusterLow := uint16(cluster and $FFFF); + dir^.clusterHigh := uint16((cluster shr 16) and $FFFF); +end; + +function fatMaxClusterCount(bootRecord : PBootRecord) : uint32; +begin + if (bootRecord = nil) or (bootRecord^.sectorSize = 0) then + fatMaxClusterCount := 0 + else + fatMaxClusterCount := (bootRecord^.FATSize * bootRecord^.sectorSize) div 4; +end; + +function fatClustersForBytes(byteCount : uint32; bytesPerCluster : uint32) : uint32; +begin + if (byteCount = 0) or (bytesPerCluster = 0) then + fatClustersForBytes := 0 + else + fatClustersForBytes := (byteCount + bytesPerCluster - 1) div bytesPerCluster; +end; + +function fatPreallocClusterCount(bytesPerCluster : uint32) : uint32; +begin + if bytesPerCluster = 0 then begin + fatPreallocClusterCount := 1; + exit; + end; + + fatPreallocClusterCount := FAT_WRITE_PREALLOC_BYTES div bytesPerCluster; + if fatPreallocClusterCount = 0 then + fatPreallocClusterCount := 1; + if fatPreallocClusterCount < FAT_WRITE_PREALLOC_MIN_CLUSTERS then + fatPreallocClusterCount := FAT_WRITE_PREALLOC_MIN_CLUSTERS; + if fatPreallocClusterCount > FAT_WRITE_PREALLOC_MAX_CLUSTERS then + fatPreallocClusterCount := FAT_WRITE_PREALLOC_MAX_CLUSTERS; +end; + +function isValidFAT32Name(fullName : pchar) : boolean; +var + i, fnLen, dotPos, nameLen, extLen : uint32; + c : char; +begin + isValidFAT32Name := false; + if fullName = nil then exit; + fnLen := stringSize(fullName); + if fnLen = 0 then exit; + + dotPos := fnLen; + if fnLen > 0 then + for i := 0 to fnLen - 1 do + if fullName[i] = '.' then + dotPos := i; + + nameLen := dotPos; + if dotPos < fnLen then + extLen := fnLen - dotPos - 1 + else + extLen := 0; + + if (nameLen < 1) or (nameLen > 8) then exit; + if extLen > 3 then exit; + + if fnLen > 0 then + for i := 0 to fnLen - 1 do begin + c := fullName[i]; + if i = dotPos then continue; + if uint8(c) < $20 then exit; + case c of + '"', '*', '/', ':', '<', '>', '?', '\', '|': exit; + end; + end; + + isValidFAT32Name := true; +end; + +function matchExtension(dirExt : TFATExtArray; ext : pchar) : boolean; +var + j : uint32; + c : char; + extDone : boolean; +begin + matchExtension := true; + extDone := false; + for j := 0 to 2 do begin + if (not extDone) and (ext <> nil) and (ext[j] <> char(0)) then begin + c := ext[j]; + if (c >= 'a') and (c <= 'z') then + c := char(uint8(c) - 32); + end else begin + c := ' '; + extDone := true; + end; + if dirExt[j] <> c then begin + matchExtension := false; + exit; + end; + end; +end; + +function compareByteArray8(str1 : byteArray8; str2 : byteArray8) : boolean; +var + i : uint32; +begin + compareByteArray8 := true; + for i := 0 to 7 do + if str1[i] <> str2[i] then begin + compareByteArray8 := false; + exit; + end; +end; + +function cleanString(str : pchar) : byteArray8; +var + i : uint32; +begin + for i := 0 to 7 do + cleanString[i] := ' '; + if str = nil then exit; + for i := 0 to 7 do begin + if str[i] = char(0) then + exit; + if (str[i] >= 'a') and (str[i] <= 'z') then + cleanString[i] := char(uint8(str[i]) - 32) + else + cleanString[i] := str[i]; + end; +end; + +procedure splitFileNameParts(fullName : pchar; var nameOut : pchar; var extOut : pchar); +var + fnLen, dotPos, i : uint32; +begin + nameOut := nil; + extOut := nil; + if fullName = nil then begin + nameOut := stringNew(0); + extOut := stringNew(0); + exit; + end; + fnLen := stringSize(fullName); + dotPos := fnLen; + if fnLen > 0 then + for i := 0 to fnLen - 1 do + if fullName[i] = '.' then + dotPos := i; + nameOut := stringTrim(fullName, dotPos); + if dotPos < fnLen then + extOut := stringCopy(pchar(@fullName[dotPos + 1])) + else + extOut := stringNew(0); +end; + +procedure fillFatExt(var dest : TFATExtArray; extPart : pchar); +var + j : uint32; +begin + dest[0] := ' '; + dest[1] := ' '; + dest[2] := ' '; + if extPart = nil then exit; + for j := 0 to 2 do begin + if extPart[j] = char(0) then exit; + if (extPart[j] >= 'a') and (extPart[j] <= 'z') then + dest[j] := char(uint8(extPart[j]) - 32) + else + dest[j] := extPart[j]; + end; +end; + +procedure splitPathParts(fullPath : pchar; var parentOut : pchar; var nameOut : pchar); +var + pathLen : uint32; + lastSlash : uint32; + i : uint32; +begin + parentOut := nil; + nameOut := nil; + if fullPath = nil then begin + parentOut := stringNew(0); + nameOut := stringNew(0); + exit; + end; + + pathLen := stringSize(fullPath); + if pathLen = 0 then begin + parentOut := stringNew(0); + nameOut := stringNew(0); + exit; + end; + + lastSlash := pathLen; + for i := 0 to pathLen - 1 do + if fullPath[i] = '/' then + lastSlash := i; + + if lastSlash < pathLen then begin + parentOut := stringTrim(fullPath, lastSlash); + nameOut := stringCopy(pchar(@fullPath[lastSlash + 1])); + end else begin + parentOut := stringNew(0); + nameOut := stringCopy(fullPath); + end; +end; + +function fatEntryName(dir : PDirectory) : pchar; +var + nameLen : uint32; + extLen : uint32; + nameBuf : pchar; +begin + fatEntryName := nil; + if dir = nil then exit; + + nameLen := 8; + while (nameLen > 0) and (dir^.fileName[nameLen - 1] = ' ') do + nameLen := nameLen - 1; + + extLen := 3; + while (extLen > 0) and (dir^.fileExtension[extLen - 1] = ' ') do + extLen := extLen - 1; + + if extLen > 0 then begin + nameBuf := pchar(kalloc(nameLen + extLen + 2)); + if nameBuf = nil then exit; + memcpy(uint32(@dir^.fileName[0]), uint32(nameBuf), nameLen); + nameBuf[nameLen] := '.'; + memcpy(uint32(@dir^.fileExtension[0]), uint32(@nameBuf[nameLen + 1]), extLen); + nameBuf[nameLen + extLen + 1] := char(0); + end else begin + nameBuf := pchar(kalloc(nameLen + 1)); + if nameBuf = nil then exit; + memcpy(uint32(@dir^.fileName[0]), uint32(nameBuf), nameLen); + nameBuf[nameLen] := char(0); + end; + + fatEntryName := nameBuf; +end; + +function fatIsDotEntry(dir : PDirectory) : boolean; +begin + fatIsDotEntry := false; + if dir = nil then exit; + if dir^.fileName[0] <> '.' then exit; + if (dir^.fileName[1] = ' ') or (dir^.fileName[1] = '.') then + fatIsDotEntry := true; +end; + +function readBootRecordRaw(volume : PStorage_Volume; var bootRecord : TBootRecord) : boolean; +var + sectorBuf : puint32; + bufSize : uint32; +begin + readBootRecordRaw := false; + bufSize := volume^.device^.sectorSize; + if bufSize < 512 then bufSize := 512; + sectorBuf := puint32(kalloc(bufSize)); + if sectorBuf = nil then exit; + memset(uint32(sectorBuf), 0, bufSize); + driver.storage.mgr.storage_read(volume^.device, volume^.sectorStart, 1, sectorBuf); + bootRecord := PBootRecord(sectorBuf)^; + kfree(sectorBuf); + readBootRecordRaw := true; +end; + +function fatNextHintStamp(ctx : PFATVolumeCtx) : uint32; +begin + fatNextHintStamp := 0; + if ctx = nil then exit; + ctx^.HintStamp := ctx^.HintStamp + 1; + if ctx^.HintStamp = 0 then + ctx^.HintStamp := 1; + fatNextHintStamp := ctx^.HintStamp; +end; + +procedure fatRememberAllocHint(volume : PStorage_Volume; startCluster : uint32; clusterCount : uint32); +var + ctx : PFATVolumeCtx; + slot : uint32; + replace : uint32; + bestStamp : uint32; + stamp : uint32; +begin + if (volume = nil) or (startCluster < 2) or (clusterCount = 0) then exit; + ctx := FAT32GetVolumeCtx(volume); + if ctx = nil then exit; + + stamp := fatNextHintStamp(ctx); + replace := 0; + bestStamp := $FFFFFFFF; + for slot := 0 to FAT_ALLOC_HINT_SLOTS - 1 do begin + if ctx^.AllocHints[slot].Valid and (ctx^.AllocHints[slot].StartCluster = startCluster) then begin + ctx^.AllocHints[slot].ClusterCount := clusterCount; + ctx^.AllocHints[slot].Stamp := stamp; + exit; + end; + if not ctx^.AllocHints[slot].Valid then begin + replace := slot; + bestStamp := 0; + break; + end; + if ctx^.AllocHints[slot].Stamp < bestStamp then begin + bestStamp := ctx^.AllocHints[slot].Stamp; + replace := slot; + end; + end; + + ctx^.AllocHints[replace].Valid := true; + ctx^.AllocHints[replace].StartCluster := startCluster; + ctx^.AllocHints[replace].ClusterCount := clusterCount; + ctx^.AllocHints[replace].Stamp := stamp; +end; + +procedure fatInvalidateAllocRange(volume : PStorage_Volume; startCluster : uint32; clusterCount : uint32); +var + ctx : PFATVolumeCtx; + slot : uint32; + endCluster : uint32; + hintEnd : uint32; + overlapStart : uint32; +begin + if (volume = nil) or (startCluster < 2) or (clusterCount = 0) then exit; + ctx := FAT32GetVolumeCtx(volume); + if ctx = nil then exit; + endCluster := startCluster + clusterCount; + + for slot := 0 to FAT_ALLOC_HINT_SLOTS - 1 do begin + if not ctx^.AllocHints[slot].Valid then + continue; + hintEnd := ctx^.AllocHints[slot].StartCluster + ctx^.AllocHints[slot].ClusterCount; + if (endCluster <= ctx^.AllocHints[slot].StartCluster) or (startCluster >= hintEnd) then + continue; + if (startCluster <= ctx^.AllocHints[slot].StartCluster) and (endCluster >= hintEnd) then begin + ctx^.AllocHints[slot].Valid := false; + continue; + end; + if startCluster <= ctx^.AllocHints[slot].StartCluster then begin + ctx^.AllocHints[slot].StartCluster := endCluster; + ctx^.AllocHints[slot].ClusterCount := hintEnd - endCluster; + continue; + end; + overlapStart := startCluster - ctx^.AllocHints[slot].StartCluster; + ctx^.AllocHints[slot].ClusterCount := overlapStart; + end; +end; + +function fatRangeIsFree(volume : PStorage_Volume; info : PFATVolumeInfo; startCluster : uint32; clusterCount : uint32) : boolean; +var + cluster : uint32; +begin + fatRangeIsFree := false; + if (volume = nil) or (info = nil) or (clusterCount = 0) then exit; + if (startCluster < 2) or ((startCluster + clusterCount) > info^.MaxCluster) then exit; + for cluster := 0 to clusterCount - 1 do + if readFat(volume, startCluster + cluster, @info^.BootRecord) <> 0 then + exit; + fatRangeIsFree := true; +end; + +function fatTakeAllocHint(volume : PStorage_Volume; info : PFATVolumeInfo; amount : uint32; var runStart : uint32) : boolean; +var + ctx : PFATVolumeCtx; + slot : uint32; + bestSlot : sint32; + bestStamp : uint32; + stamp : uint32; +begin + fatTakeAllocHint := false; + if (volume = nil) or (info = nil) or (amount = 0) then exit; + ctx := FAT32GetVolumeCtx(volume); + if ctx = nil then exit; + + bestSlot := -1; + bestStamp := 0; + for slot := 0 to FAT_ALLOC_HINT_SLOTS - 1 do begin + if not ctx^.AllocHints[slot].Valid then + continue; + if ctx^.AllocHints[slot].ClusterCount < amount then + continue; + if (bestSlot < 0) or (ctx^.AllocHints[slot].Stamp > bestStamp) then begin + bestSlot := sint32(slot); + bestStamp := ctx^.AllocHints[slot].Stamp; + end; + end; + + while bestSlot >= 0 do begin + if fatRangeIsFree(volume, info, ctx^.AllocHints[uint32(bestSlot)].StartCluster, amount) then begin + runStart := ctx^.AllocHints[uint32(bestSlot)].StartCluster; + ctx^.AllocHints[uint32(bestSlot)].StartCluster := runStart + amount; + ctx^.AllocHints[uint32(bestSlot)].ClusterCount := ctx^.AllocHints[uint32(bestSlot)].ClusterCount - amount; + if ctx^.AllocHints[uint32(bestSlot)].ClusterCount = 0 then + ctx^.AllocHints[uint32(bestSlot)].Valid := false + else begin + stamp := fatNextHintStamp(ctx); + ctx^.AllocHints[uint32(bestSlot)].Stamp := stamp; + end; + fatTakeAllocHint := true; + exit; + end; + ctx^.AllocHints[uint32(bestSlot)].Valid := false; + bestSlot := -1; + bestStamp := 0; + for slot := 0 to FAT_ALLOC_HINT_SLOTS - 1 do begin + if not ctx^.AllocHints[slot].Valid then + continue; + if ctx^.AllocHints[slot].ClusterCount < amount then + continue; + if (bestSlot < 0) or (ctx^.AllocHints[slot].Stamp > bestStamp) then begin + bestSlot := sint32(slot); + bestStamp := ctx^.AllocHints[slot].Stamp; + end; + end; + end; +end; + +procedure fatRememberDirHint(volume : PStorage_Volume; parentCluster : uint32; sectorLBA : uint32; entryIdx : uint32); +var + ctx : PFATVolumeCtx; + slot : uint32; + replace : uint32; + bestStamp : uint32; + stamp : uint32; +begin + if (volume = nil) or (parentCluster < 2) then exit; + ctx := FAT32GetVolumeCtx(volume); + if ctx = nil then exit; + + stamp := fatNextHintStamp(ctx); + replace := 0; + bestStamp := $FFFFFFFF; + for slot := 0 to FAT_DIR_HINT_SLOTS - 1 do begin + if ctx^.DirHints[slot].Valid + and (ctx^.DirHints[slot].ParentCluster = parentCluster) + and (ctx^.DirHints[slot].SectorLBA = sectorLBA) + and (ctx^.DirHints[slot].EntryIdx = entryIdx) then begin + ctx^.DirHints[slot].Stamp := stamp; + exit; + end; + if not ctx^.DirHints[slot].Valid then begin + replace := slot; + bestStamp := 0; + break; + end; + if ctx^.DirHints[slot].Stamp < bestStamp then begin + bestStamp := ctx^.DirHints[slot].Stamp; + replace := slot; + end; + end; + + ctx^.DirHints[replace].Valid := true; + ctx^.DirHints[replace].ParentCluster := parentCluster; + ctx^.DirHints[replace].SectorLBA := sectorLBA; + ctx^.DirHints[replace].EntryIdx := entryIdx; + ctx^.DirHints[replace].Stamp := stamp; +end; + +procedure fatForgetDirHintsForParent(volume : PStorage_Volume; parentCluster : uint32); +var + ctx : PFATVolumeCtx; + slot : uint32; +begin + if (volume = nil) or (parentCluster < 2) then exit; + ctx := FAT32GetVolumeCtx(volume); + if ctx = nil then exit; + for slot := 0 to FAT_DIR_HINT_SLOTS - 1 do + if ctx^.DirHints[slot].Valid and (ctx^.DirHints[slot].ParentCluster = parentCluster) then + ctx^.DirHints[slot].Valid := false; +end; + +function fatCtxLookup(volume : PStorage_Volume) : PFATVolumeCtx; +var + node : PFATVolumeCtxNode; +begin + fatCtxLookup := nil; + node := fatCtxListHead; + while node <> nil do begin + if node^.Volume = volume then begin + fatCtxLookup := node^.Ctx; + exit; + end; + node := node^.Next; + end; +end; + +function fatCtxRegister(volume : PStorage_Volume; ctx : PFATVolumeCtx) : boolean; +var + node : PFATVolumeCtxNode; +begin + fatCtxRegister := false; + node := PFATVolumeCtxNode(kalloc(sizeof(TFATVolumeCtxNode))); + if node = nil then exit; + memset(uint32(node), 0, sizeof(TFATVolumeCtxNode)); + node^.Volume := volume; + node^.Ctx := ctx; + node^.Next := fatCtxListHead; + fatCtxListHead := node; + fatCtxRegister := true; +end; + +function fatCtxDetach(volume : PStorage_Volume) : PFATVolumeCtx; +var + prev : PFATVolumeCtxNode; + node : PFATVolumeCtxNode; +begin + fatCtxDetach := nil; + prev := nil; + asm pushf; cli end; + node := fatCtxListHead; + while node <> nil do begin + if node^.Volume = volume then begin + fatCtxDetach := node^.Ctx; + if prev = nil then + fatCtxListHead := node^.Next + else + prev^.Next := node^.Next; + kfree(void(node)); + asm popf end; + exit; + end; + prev := node; + node := node^.Next; + end; + asm popf end; +end; + +procedure fatCtxFree(ctx : PFATVolumeCtx); +begin + if ctx = nil then exit; + if ctx^.Cache.Data <> nil then + kfree(ctx^.Cache.Data); + if ctx^.Cache.EvictBuf <> nil then + kfree(ctx^.Cache.EvictBuf); + if ctx^.TransferPoolBuf <> nil then + kfree(ctx^.TransferPoolBuf); + if ctx^.TransferScratchBuf <> nil then + kfree(ctx^.TransferScratchBuf); + kfree(void(ctx)); +end; + +function FAT32GetVolumeCtx(volume : PStorage_Volume) : PFATVolumeCtx; +var + ctx : PFATVolumeCtx; + i : uint32; + transfer : PFATTransferCtx; + poolBase : uint32; + scratchBase: uint32; +begin + FAT32GetVolumeCtx := nil; + if volume = nil then exit; + + ctx := fatCtxLookup(volume); + if ctx <> nil then begin + FAT32GetVolumeCtx := ctx; + exit; + end; + + ctx := PFATVolumeCtx(kalloc(sizeof(TFATVolumeCtx))); + if ctx = nil then exit; + memset(uint32(ctx), 0, sizeof(TFATVolumeCtx)); + + if not readBootRecordRaw(volume, ctx^.Info.BootRecord) then begin + fatCtxFree(ctx); + exit; + end; + + { Keep runtime addressing compatible with the legacy formatter and metadata paths. } + ctx^.Info.DataStart := volume^.sectorStart + ctx^.Info.BootRecord.rsvSectors + + ctx^.Info.BootRecord.FATSize; + ctx^.Info.BytesPerCluster := uint32(ctx^.Info.BootRecord.spc) * uint32(ctx^.Info.BootRecord.sectorSize); + ctx^.Info.MaxCluster := fatMaxClusterCount(@ctx^.Info.BootRecord); + ctx^.InfoValid := true; + + ctx^.Cache.Data := puint32(kalloc(FAT_CACHE_LINES * 512)); + ctx^.Cache.EvictBuf := puint32(kalloc(512)); + if (ctx^.Cache.Data = nil) or (ctx^.Cache.EvictBuf = nil) then begin + fatCtxFree(ctx); + exit; + end; + memset(uint32(ctx^.Cache.Data), 0, FAT_CACHE_LINES * 512); + memset(uint32(ctx^.Cache.EvictBuf), 0, 512); + ctx^.Cache.Busy := false; + ctx^.Cache.FatStart := volume^.sectorStart + ctx^.Info.BootRecord.rsvSectors; + ctx^.Cache.Device := volume^.device; + for i := 0 to FAT_CACHE_LINES - 1 do begin + ctx^.Cache.Lines[i].Valid := false; + ctx^.Cache.Lines[i].Dirty := false; + ctx^.Cache.Lines[i].LRU := uint8(i mod FAT_CACHE_WAYS); + end; + + ctx^.TransferPoolBuf := kalloc(FAT_TRANSFER_POOL_CAPACITY * sizeof(TFATTransferCtx)); + ctx^.TransferScratchBuf := kalloc(FAT_TRANSFER_POOL_CAPACITY * ctx^.Info.BootRecord.sectorSize); + if (ctx^.TransferPoolBuf <> nil) and (ctx^.TransferScratchBuf <> nil) then begin + memset(uint32(ctx^.TransferPoolBuf), 0, FAT_TRANSFER_POOL_CAPACITY * sizeof(TFATTransferCtx)); + memset(uint32(ctx^.TransferScratchBuf), 0, FAT_TRANSFER_POOL_CAPACITY * ctx^.Info.BootRecord.sectorSize); + poolBase := uint32(ctx^.TransferPoolBuf); + scratchBase := uint32(ctx^.TransferScratchBuf); + if FAT_TRANSFER_POOL_CAPACITY > 0 then + for i := 0 to FAT_TRANSFER_POOL_CAPACITY - 1 do begin + transfer := PFATTransferCtx(poolBase + (i * sizeof(TFATTransferCtx))); + transfer^.Next := PFATTransferCtx(ctx^.TransferFreeList); + transfer^.VolumeCtx := ctx; + transfer^.ScratchSector := puint32(scratchBase + (i * ctx^.Info.BootRecord.sectorSize)); + transfer^.ScratchSize := ctx^.Info.BootRecord.sectorSize; + ctx^.TransferFreeList := pointer(transfer); + end; + end; + + if not fatCtxRegister(volume, ctx) then begin + fatCtxFree(ctx); + exit; + end; + FAT32GetVolumeCtx := ctx; +end; + +function FAT32GetVolumeInfo(volume : PStorage_Volume) : PFATVolumeInfo; +var + ctx : PFATVolumeCtx; +begin + ctx := FAT32GetVolumeCtx(volume); + if ctx = nil then + FAT32GetVolumeInfo := nil + else + FAT32GetVolumeInfo := @ctx^.Info; +end; + +procedure fatCacheFlush(cache : PFATCache; volume : PStorage_Volume); +var + i : uint32; + lba : uint32; + doFlush : boolean; +begin + if cache = nil then exit; + + while true do begin + asm pushf; cli end; + if not cache^.Busy then begin + cache^.Busy := true; + asm popf end; + break; + end; + asm popf end; + asm hlt end; + end; + + for i := 0 to FAT_CACHE_LINES - 1 do begin + asm pushf; cli end; + doFlush := cache^.Lines[i].Valid and cache^.Lines[i].Dirty; + if doFlush then begin + lba := cache^.FatStart + cache^.Lines[i].SectorIdx; + cache^.Lines[i].Dirty := false; + memcpy(uint32(cache^.Data) + (i * 512), uint32(cache^.EvictBuf), 512); + end; + asm popf end; + if doFlush then + driver.storage.mgr.storage_write(volume^.device, lba, 1, cache^.EvictBuf); + end; + + asm pushf; cli end; + cache^.Busy := false; + asm popf end; +end; + +procedure FAT32Flush(volume : PStorage_Volume); +var + ctx : PFATVolumeCtx; +begin + ctx := FAT32GetVolumeCtx(volume); + if ctx = nil then exit; + fatCacheFlush(@ctx^.Cache, volume); +end; + +procedure FAT32ReleaseVolumeCtx(volume : PStorage_Volume); +var + ctx : PFATVolumeCtx; +begin + if volume = nil then exit; + ctx := fatCtxDetach(volume); + if ctx = nil then exit; + fatCacheFlush(@ctx^.Cache, volume); + fatCtxFree(ctx); +end; + +function readFat(volume : PStorage_Volume; cluster : uint32; bootRecord : PBootRecord) : uint32; +var + ctx : PFATVolumeCtx; + cache : PFATCache; + fatSecIdx : uint32; + setIdx : uint32; + baseSlot : uint32; + entryOff : uint32; + dataPtr : puint32; + lba : uint32; + doEvict : boolean; + evictLBA : uint32; + victim : uint32; + w : uint32; +begin + ctx := FAT32GetVolumeCtx(volume); + cache := @ctx^.Cache; + fatSecIdx := cluster div 128; + setIdx := fatSecIdx mod FAT_CACHE_SETS; + baseSlot := setIdx * FAT_CACHE_WAYS; + entryOff := cluster mod 128; + + asm pushf; cli end; + for w := 0 to FAT_CACHE_WAYS - 1 do + if cache^.Lines[baseSlot + w].Valid and (cache^.Lines[baseSlot + w].SectorIdx = fatSecIdx) then begin + dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); + readFat := dataPtr[entryOff] and $0FFFFFFF; + cache^.Lines[baseSlot + w].LRU := 0; + cache^.Lines[baseSlot + (1 - w)].LRU := 1; + asm popf end; + exit; + end; + asm popf end; + + while true do begin + asm pushf; cli end; + if not cache^.Busy then begin + for w := 0 to FAT_CACHE_WAYS - 1 do + if cache^.Lines[baseSlot + w].Valid and (cache^.Lines[baseSlot + w].SectorIdx = fatSecIdx) then begin + dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); + readFat := dataPtr[entryOff] and $0FFFFFFF; + cache^.Lines[baseSlot + w].LRU := 0; + cache^.Lines[baseSlot + (1 - w)].LRU := 1; + asm popf end; + exit; + end; + cache^.Busy := true; + victim := baseSlot; + if not cache^.Lines[baseSlot].Valid then + victim := baseSlot + else if not cache^.Lines[baseSlot + 1].Valid then + victim := baseSlot + 1 + else if cache^.Lines[baseSlot + 1].LRU >= cache^.Lines[baseSlot].LRU then + victim := baseSlot + 1 + else + victim := baseSlot; + dataPtr := puint32(uint32(cache^.Data) + (victim * 512)); + doEvict := cache^.Lines[victim].Valid and cache^.Lines[victim].Dirty; + if doEvict then begin + evictLBA := cache^.FatStart + cache^.Lines[victim].SectorIdx; + memcpy(uint32(dataPtr), uint32(cache^.EvictBuf), 512); + end; + cache^.Lines[victim].Valid := false; + asm popf end; + break; + end; + asm popf end; + asm hlt end; + end; + + if doEvict then + driver.storage.mgr.storage_write(cache^.Device, evictLBA, 1, cache^.EvictBuf); + lba := cache^.FatStart + fatSecIdx; + driver.storage.mgr.storage_read(cache^.Device, lba, 1, dataPtr); + + asm pushf; cli end; + cache^.Lines[victim].SectorIdx := fatSecIdx; + cache^.Lines[victim].Valid := true; + cache^.Lines[victim].Dirty := false; + cache^.Lines[victim].LRU := 0; + if victim = baseSlot then + cache^.Lines[baseSlot + 1].LRU := 1 + else + cache^.Lines[baseSlot].LRU := 1; + readFat := dataPtr[entryOff] and $0FFFFFFF; + cache^.Busy := false; + asm popf end; +end; + +procedure writeFat(volume : PStorage_Volume; cluster : uint32; value : uint32; bootRecord : PBootRecord); +var + ctx : PFATVolumeCtx; + cache : PFATCache; + fatSecIdx : uint32; + setIdx : uint32; + baseSlot : uint32; + entryOff : uint32; + dataPtr : puint32; + lba : uint32; + doEvict : boolean; + evictLBA : uint32; + victim : uint32; + w : uint32; +begin + ctx := FAT32GetVolumeCtx(volume); + cache := @ctx^.Cache; + fatSecIdx := cluster div 128; + setIdx := fatSecIdx mod FAT_CACHE_SETS; + baseSlot := setIdx * FAT_CACHE_WAYS; + entryOff := cluster mod 128; + + asm pushf; cli end; + for w := 0 to FAT_CACHE_WAYS - 1 do + if cache^.Lines[baseSlot + w].Valid and (cache^.Lines[baseSlot + w].SectorIdx = fatSecIdx) then begin + dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); + if value = 0 then + dataPtr[entryOff] := 0 + else + dataPtr[entryOff] := (dataPtr[entryOff] and $F0000000) or (value and $0FFFFFFF); + cache^.Lines[baseSlot + w].Dirty := true; + cache^.Lines[baseSlot + w].LRU := 0; + cache^.Lines[baseSlot + (1 - w)].LRU := 1; + asm popf end; + exit; + end; + asm popf end; + + while true do begin + asm pushf; cli end; + if not cache^.Busy then begin + for w := 0 to FAT_CACHE_WAYS - 1 do + if cache^.Lines[baseSlot + w].Valid and (cache^.Lines[baseSlot + w].SectorIdx = fatSecIdx) then begin + dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); + if value = 0 then + dataPtr[entryOff] := 0 + else + dataPtr[entryOff] := (dataPtr[entryOff] and $F0000000) or (value and $0FFFFFFF); + cache^.Lines[baseSlot + w].Dirty := true; + cache^.Lines[baseSlot + w].LRU := 0; + cache^.Lines[baseSlot + (1 - w)].LRU := 1; + asm popf end; + exit; + end; + cache^.Busy := true; + victim := baseSlot; + if not cache^.Lines[baseSlot].Valid then + victim := baseSlot + else if not cache^.Lines[baseSlot + 1].Valid then + victim := baseSlot + 1 + else if cache^.Lines[baseSlot + 1].LRU >= cache^.Lines[baseSlot].LRU then + victim := baseSlot + 1 + else + victim := baseSlot; + dataPtr := puint32(uint32(cache^.Data) + (victim * 512)); + doEvict := cache^.Lines[victim].Valid and cache^.Lines[victim].Dirty; + if doEvict then begin + evictLBA := cache^.FatStart + cache^.Lines[victim].SectorIdx; + memcpy(uint32(dataPtr), uint32(cache^.EvictBuf), 512); + end; + cache^.Lines[victim].Valid := false; + asm popf end; + break; + end; + asm popf end; + asm hlt end; + end; + + if doEvict then + driver.storage.mgr.storage_write(cache^.Device, evictLBA, 1, cache^.EvictBuf); + lba := cache^.FatStart + fatSecIdx; + driver.storage.mgr.storage_read(cache^.Device, lba, 1, dataPtr); + + asm pushf; cli end; + cache^.Lines[victim].SectorIdx := fatSecIdx; + cache^.Lines[victim].Valid := true; + cache^.Lines[victim].Dirty := false; + if value = 0 then + dataPtr[entryOff] := 0 + else + dataPtr[entryOff] := (dataPtr[entryOff] and $F0000000) or (value and $0FFFFFFF); + cache^.Lines[victim].Dirty := true; + cache^.Lines[victim].LRU := 0; + if victim = baseSlot then + cache^.Lines[baseSlot + 1].LRU := 1 + else + cache^.Lines[baseSlot].LRU := 1; + cache^.Busy := false; + asm popf end; +end; + +function runMapEnsureCapacity(var runMap : TFATRunMap; needed : uint32) : boolean; +var + newCapacity : uint32; + newRuns : PFATRun; +begin + runMapEnsureCapacity := true; + if needed <= runMap.Capacity then exit; + newCapacity := runMap.Capacity; + if newCapacity = 0 then + newCapacity := 4; + while newCapacity < needed do + newCapacity := newCapacity * 2; + newRuns := PFATRun(kalloc(newCapacity * sizeof(TFATRun))); + if newRuns = nil then begin + runMapEnsureCapacity := false; + exit; + end; + memset(uint32(newRuns), 0, newCapacity * sizeof(TFATRun)); + if (runMap.Runs <> nil) and (runMap.Count > 0) then + memcpy(uint32(runMap.Runs), uint32(newRuns), runMap.Count * sizeof(TFATRun)); + if runMap.Runs <> nil then + kfree(void(runMap.Runs)); + runMap.Runs := newRuns; + runMap.Capacity := newCapacity; +end; + +procedure runMapFree(var runMap : TFATRunMap); +begin + if runMap.Runs <> nil then + kfree(void(runMap.Runs)); + runMap.Runs := nil; + runMap.Count := 0; + runMap.Capacity := 0; + runMap.TotalClusters := 0; +end; + +function runMapAppend(var runMap : TFATRunMap; startCluster : uint32; clusterCount : uint32) : boolean; +var + lastRun : PFATRun; +begin + runMapAppend := false; + if clusterCount = 0 then exit; + if runMap.Count > 0 then begin + lastRun := @runMap.Runs[runMap.Count - 1]; + if (lastRun^.StartCluster + lastRun^.ClusterCount) = startCluster then begin + lastRun^.ClusterCount := lastRun^.ClusterCount + clusterCount; + runMap.TotalClusters := runMap.TotalClusters + clusterCount; + runMapAppend := true; + exit; + end; + end; + if not runMapEnsureCapacity(runMap, runMap.Count + 1) then exit; + runMap.Runs[runMap.Count].StartCluster := startCluster; + runMap.Runs[runMap.Count].ClusterCount := clusterCount; + runMap.Count := runMap.Count + 1; + runMap.TotalClusters := runMap.TotalClusters + clusterCount; + runMapAppend := true; +end; + +function buildRunMap(volume : PStorage_Volume; info : PFATVolumeInfo; firstCluster : uint32; var runMap : TFATRunMap) : boolean; +var + currentCluster : uint32; + nextCluster : uint32; + runStart : uint32; + runCount : uint32; + iterCount : uint32; +begin + buildRunMap := false; + runMapFree(runMap); + if (info = nil) or (firstCluster < 2) then begin + buildRunMap := true; + exit; + end; + + currentCluster := firstCluster; + runStart := firstCluster; + runCount := 1; + iterCount := 0; + + while true do begin + if (info^.MaxCluster > 0) and (iterCount >= info^.MaxCluster) then + exit; + if (currentCluster < 2) or ((info^.MaxCluster > 0) and (currentCluster >= info^.MaxCluster)) then + exit; + + nextCluster := readFat(volume, currentCluster, @info^.BootRecord); + if isBadCluster(nextCluster) then exit; + + if isEndOfChain(nextCluster) then begin + if not runMapAppend(runMap, runStart, runCount) then exit; + buildRunMap := true; + exit; + end; + + if nextCluster = 0 then exit; + + if nextCluster = (currentCluster + 1) then + runCount := runCount + 1 + else begin + if not runMapAppend(runMap, runStart, runCount) then exit; + runStart := nextCluster; + runCount := 1; + end; + + currentCluster := nextCluster; + iterCount := iterCount + 1; + end; +end; + +function findEntryInSector(buffer : puint32; entriesPerSector : uint32; + cleanName : byteArray8; ext : pchar; var hitEnd : boolean) : integer; +var + idx : uint32; + dir : PDirectory; +begin + findEntryInSector := -1; + hitEnd := false; + if entriesPerSector = 0 then exit; + for idx := 0 to entriesPerSector - 1 do begin + dir := @PDirectory(buffer)[idx]; + if dir^.fileName[0] = char(0) then begin + hitEnd := true; + exit; + end; + if dir^.fileName[0] = char($E5) then + continue; + if compareByteArray8(dir^.fileName, cleanName) and matchExtension(dir^.fileExtension, ext) then begin + findEntryInSector := integer(idx); + exit; + end; + end; +end; + +function findFreeEntryInSector(buffer : puint32; entriesPerSector : uint32) : integer; +var + idx : uint32; + dir : PDirectory; +begin + findFreeEntryInSector := -1; + if entriesPerSector = 0 then exit; + for idx := 0 to entriesPerSector - 1 do begin + dir := @PDirectory(buffer)[idx]; + if (dir^.fileName[0] = char(0)) or (dir^.fileName[0] = char($E5)) then begin + findFreeEntryInSector := integer(idx); + exit; + end; + end; +end; + +function locateFreeDirEntryFromHint(volume : PStorage_Volume; info : PFATVolumeInfo; parentCluster : uint32; + var loc : TFATDirEntryLocation; buffer : puint32) : boolean; +var + ctx : PFATVolumeCtx; + slot : uint32; + bestSlot : sint32; + bestStamp : uint32; + entryIdx : integer; +begin + locateFreeDirEntryFromHint := false; + if (volume = nil) or (info = nil) or (parentCluster < 2) or (buffer = nil) then exit; + ctx := FAT32GetVolumeCtx(volume); + if ctx = nil then exit; + + bestSlot := -1; + bestStamp := 0; + for slot := 0 to FAT_DIR_HINT_SLOTS - 1 do begin + if not ctx^.DirHints[slot].Valid then + continue; + if ctx^.DirHints[slot].ParentCluster <> parentCluster then + continue; + if (bestSlot < 0) or (ctx^.DirHints[slot].Stamp > bestStamp) then begin + bestSlot := sint32(slot); + bestStamp := ctx^.DirHints[slot].Stamp; + end; + end; + + while bestSlot >= 0 do begin + driver.storage.mgr.storage_read(volume^.device, ctx^.DirHints[uint32(bestSlot)].SectorLBA, 1, buffer); + entryIdx := findFreeEntryInSector(buffer, info^.BootRecord.sectorSize div sizeof(TDirectory)); + if (entryIdx >= 0) and (uint32(entryIdx) = ctx^.DirHints[uint32(bestSlot)].EntryIdx) then begin + loc.Valid := true; + loc.ParentCluster := parentCluster; + loc.SectorLBA := ctx^.DirHints[uint32(bestSlot)].SectorLBA; + loc.EntryIdx := uint32(entryIdx); + ctx^.DirHints[uint32(bestSlot)].Stamp := fatNextHintStamp(ctx); + locateFreeDirEntryFromHint := true; + exit; + end; + + ctx^.DirHints[uint32(bestSlot)].Valid := false; + bestSlot := -1; + bestStamp := 0; + for slot := 0 to FAT_DIR_HINT_SLOTS - 1 do begin + if not ctx^.DirHints[slot].Valid then + continue; + if ctx^.DirHints[slot].ParentCluster <> parentCluster then + continue; + if (bestSlot < 0) or (ctx^.DirHints[slot].Stamp > bestStamp) then begin + bestSlot := sint32(slot); + bestStamp := ctx^.DirHints[slot].Stamp; + end; + end; + end; +end; + +procedure fatRememberFreeEntryFromBuffer(volume : PStorage_Volume; parentCluster : uint32; sectorLBA : uint32; + buffer : puint32; entriesPerSec : uint32); +var + entryIdx : integer; +begin + if buffer = nil then exit; + entryIdx := findFreeEntryInSector(buffer, entriesPerSec); + if entryIdx >= 0 then + fatRememberDirHint(volume, parentCluster, sectorLBA, uint32(entryIdx)) + else + fatForgetDirHintsForParent(volume, parentCluster); +end; + +function locateDirEntry(volume : PStorage_Volume; info : PFATVolumeInfo; parentCluster : uint32; + cleanName : byteArray8; ext : pchar; var loc : TFATDirEntryLocation; buffer : puint32) : boolean; +var + currentCluster : uint32; + nextCluster : uint32; + ds : uint32; + entryIdx : integer; + hitEnd : boolean; + entriesPerSec : uint32; + clusterLBA : uint32; + iterCount : uint32; +begin + locateDirEntry := false; + loc.Valid := false; + if (info = nil) or (parentCluster < 2) then exit; + + currentCluster := parentCluster; + entriesPerSec := info^.BootRecord.sectorSize div sizeof(TDirectory); + iterCount := 0; + + while currentCluster >= 2 do begin + if (info^.MaxCluster > 0) and (iterCount >= info^.MaxCluster) then + exit; + if (info^.MaxCluster > 0) and (currentCluster >= info^.MaxCluster) then + exit; + + clusterLBA := FAT32ClusterToLBA(info, currentCluster); + for ds := 0 to info^.BootRecord.spc - 1 do begin + driver.storage.mgr.storage_read(volume^.device, clusterLBA + ds, 1, buffer); + entryIdx := findEntryInSector(buffer, entriesPerSec, cleanName, ext, hitEnd); + if entryIdx >= 0 then begin + loc.Valid := true; + loc.ParentCluster := parentCluster; + loc.SectorLBA := clusterLBA + ds; + loc.EntryIdx := uint32(entryIdx); + locateDirEntry := true; + exit; + end; + if hitEnd then + exit; + end; + + nextCluster := readFat(volume, currentCluster, @info^.BootRecord); + if isEndOfChain(nextCluster) or isBadCluster(nextCluster) or (nextCluster = 0) then + exit; + currentCluster := nextCluster; + iterCount := iterCount + 1; + end; +end; + +function locateFreeDirEntry(volume : PStorage_Volume; info : PFATVolumeInfo; parentCluster : uint32; + var loc : TFATDirEntryLocation; buffer : puint32) : boolean; +var + currentCluster : uint32; + nextCluster : uint32; + ds : uint32; + entryIdx : integer; + entriesPerSec : uint32; + clusterLBA : uint32; + iterCount : uint32; +begin + locateFreeDirEntry := false; + loc.Valid := false; + if (info = nil) or (parentCluster < 2) then exit; + if locateFreeDirEntryFromHint(volume, info, parentCluster, loc, buffer) then begin + locateFreeDirEntry := true; + exit; + end; + + currentCluster := parentCluster; + entriesPerSec := info^.BootRecord.sectorSize div sizeof(TDirectory); + iterCount := 0; + + while currentCluster >= 2 do begin + if (info^.MaxCluster > 0) and (iterCount >= info^.MaxCluster) then + exit; + if (info^.MaxCluster > 0) and (currentCluster >= info^.MaxCluster) then + exit; + + clusterLBA := FAT32ClusterToLBA(info, currentCluster); + for ds := 0 to info^.BootRecord.spc - 1 do begin + driver.storage.mgr.storage_read(volume^.device, clusterLBA + ds, 1, buffer); + entryIdx := findFreeEntryInSector(buffer, entriesPerSec); + if entryIdx >= 0 then begin + loc.Valid := true; + loc.ParentCluster := parentCluster; + loc.SectorLBA := clusterLBA + ds; + loc.EntryIdx := uint32(entryIdx); + fatRememberDirHint(volume, parentCluster, loc.SectorLBA, loc.EntryIdx); + locateFreeDirEntry := true; + exit; + end; + end; + + nextCluster := readFat(volume, currentCluster, @info^.BootRecord); + if isEndOfChain(nextCluster) or isBadCluster(nextCluster) or (nextCluster = 0) then + exit; + currentCluster := nextCluster; + iterCount := iterCount + 1; + end; +end; + +function resolveDirectoryCluster(volume : PStorage_Volume; info : PFATVolumeInfo; + directory : pchar; var parentCluster : uint32) : boolean; +var + parts : PLinkedListBase; + partName : pchar; + namePart : pchar; + extPart : pchar; + cleanName : byteArray8; + sectorBuf : puint32; + loc : TFATDirEntryLocation; + rawDir : PDirectory; + i : uint32; +begin + resolveDirectoryCluster := false; + parentCluster := info^.BootRecord.rootCluster; + if (directory = nil) or (stringSize(directory) = 0) then begin + resolveDirectoryCluster := true; + exit; + end; + + sectorBuf := puint32(kalloc(info^.BootRecord.sectorSize)); + if sectorBuf = nil then exit; + parts := LL_fromString(directory, '/'); + + if (parts <> nil) and (LL_Size(parts) > 0) then + for i := 0 to LL_Size(parts) - 1 do begin + partName := pchar(puint32(LL_Get(parts, i))^); + if (partName = nil) or (partName[0] = char(0)) then + continue; + splitFileNameParts(partName, namePart, extPart); + cleanName := cleanString(namePart); + if not locateDirEntry(volume, info, parentCluster, cleanName, extPart, loc, sectorBuf) then begin + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + LL_Free(parts); + kfree(sectorBuf); + exit; + end; + rawDir := @PDirectory(sectorBuf)[loc.EntryIdx]; + if (rawDir^.attributes and $10) <> $10 then begin + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + LL_Free(parts); + kfree(sectorBuf); + exit; + end; + parentCluster := dirFirstCluster(rawDir); + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + end; + + if parts <> nil then LL_Free(parts); + kfree(sectorBuf); + resolveDirectoryCluster := true; +end; + +function FAT32EnsureDirEntry(ofi : PFATOpenFile) : TError; +var + sectorBuf : puint32; + rawDir : PDirectory; +begin + FAT32EnsureDirEntry := eNone; + if ofi = nil then begin + FAT32EnsureDirEntry := eInvalidArgument; + exit; + end; + if ofi^.DirLoc.Valid then exit; + + sectorBuf := puint32(kalloc(ofi^.VolumeInfo^.BootRecord.sectorSize)); + if sectorBuf = nil then begin + FAT32EnsureDirEntry := eOutOfMemory; + exit; + end; + + if not locateFreeDirEntry(ofi^.Volume, ofi^.VolumeInfo, ofi^.DirLoc.ParentCluster, ofi^.DirLoc, sectorBuf) then begin + kfree(sectorBuf); + FAT32EnsureDirEntry := eDirectoryFull; + exit; + end; + + rawDir := @PDirectory(sectorBuf)[ofi^.DirLoc.EntryIdx]; + memset(uint32(rawDir), 0, sizeof(TDirectory)); + rawDir^.fileName := ofi^.CleanName; + rawDir^.attributes := 0; + fillFatExt(rawDir^.fileExtension, ofi^.ExtPart); + rawDir^.byteSize := 0; + setDirFirstCluster(rawDir, 0); + fatRememberFreeEntryFromBuffer(ofi^.Volume, ofi^.DirLoc.ParentCluster, ofi^.DirLoc.SectorLBA, + sectorBuf, ofi^.VolumeInfo^.BootRecord.sectorSize div sizeof(TDirectory)); + driver.storage.mgr.storage_write(ofi^.Volume^.device, ofi^.DirLoc.SectorLBA, 1, sectorBuf); + kfree(sectorBuf); + ofi^.Exists := true; +end; + +procedure fatUpdateAllocHint(volume : PStorage_Volume; info : PFATVolumeInfo; nextCluster : uint32); +begin + if info^.MaxCluster <= 2 then begin + sioc_allocHint := 2; + sioc_allocVol := volume; + exit; + end; + if nextCluster < 2 then + nextCluster := 2; + if nextCluster >= info^.MaxCluster then + nextCluster := 2; + sioc_allocHint := nextCluster; + sioc_allocVol := volume; +end; + +function fatTryContiguousRange(volume : PStorage_Volume; info : PFATVolumeInfo; + rangeStart : uint32; rangeEnd : uint32; needed : uint32; var foundStart : uint32) : boolean; +var + scanPos : uint32; + runLen : uint32; + runPos : uint32; + fatVal : uint32; +begin + fatTryContiguousRange := false; + if (needed = 0) or (rangeStart >= rangeEnd) then exit; + scanPos := rangeStart; + runLen := 0; + runPos := 0; + while scanPos < rangeEnd do begin + fatVal := readFat(volume, scanPos, @info^.BootRecord); + if fatVal = 0 then begin + if runLen = 0 then + runPos := scanPos; + runLen := runLen + 1; + if runLen >= needed then begin + fatRememberAllocHint(volume, runPos, runLen); + foundStart := runPos; + fatTryContiguousRange := true; + exit; + end; + end else begin + if runLen > 0 then + fatRememberAllocHint(volume, runPos, runLen); + runLen := 0; + end; + scanPos := scanPos + 1; + end; + if runLen > 0 then + fatRememberAllocHint(volume, runPos, runLen); +end; + +function fatFindContiguousFreeRange(volume : PStorage_Volume; info : PFATVolumeInfo; + amount : uint32; var runStart : uint32) : boolean; +var + scanStart : uint32; +begin + fatFindContiguousFreeRange := false; + if amount = 0 then exit; + if info^.MaxCluster <= 2 then exit; + if fatTakeAllocHint(volume, info, amount, runStart) then begin + fatFindContiguousFreeRange := true; + exit; + end; + + if (sioc_allocVol = volume) and (sioc_allocHint >= 2) and (sioc_allocHint < info^.MaxCluster) then + scanStart := sioc_allocHint + else + scanStart := 2; + + if fatTryContiguousRange(volume, info, scanStart, info^.MaxCluster, amount, runStart) or + ((scanStart > 2) and fatTryContiguousRange(volume, info, 2, scanStart, amount, runStart)) then + fatFindContiguousFreeRange := true; +end; + +procedure fatLinkContiguousRange(volume : PStorage_Volume; info : PFATVolumeInfo; + previousTail : uint32; firstCluster : uint32; clusterCount : uint32; var lastCluster : uint32); +var + cur : uint32; + i : uint32; +begin + lastCluster := previousTail; + if clusterCount = 0 then exit; + fatInvalidateAllocRange(volume, firstCluster, clusterCount); + if previousTail <> 0 then + writeFat(volume, previousTail, firstCluster, @info^.BootRecord); + cur := firstCluster; + i := 1; + while i < clusterCount do begin + writeFat(volume, cur, cur + 1, @info^.BootRecord); + cur := cur + 1; + i := i + 1; + end; + writeFat(volume, cur, $0FFFFFF8, @info^.BootRecord); + lastCluster := cur; + fatUpdateAllocHint(volume, info, lastCluster + 1); +end; + +function findFreeClusterRuns(volume : PStorage_Volume; info : PFATVolumeInfo; amount : uint32) : PLinkedListBase; +var + runs : PLinkedListBase; + currentAmount : uint32; + lastAllocated : uint32; + scanStart : uint32; + + procedure appendRun(firstCluster : uint32; clusterCount : uint32); + var + tempRun : PFATTempRun; + begin + fatRememberAllocHint(volume, firstCluster, clusterCount); + tempRun := PFATTempRun(LL_Add(runs)); + tempRun^.StartCluster := firstCluster; + tempRun^.ClusterCount := clusterCount; + currentAmount := currentAmount + clusterCount; + lastAllocated := firstCluster + clusterCount - 1; + end; + + procedure scanRange(rangeStart : uint32; rangeEnd : uint32); + var + scanPos : uint32; + runStart : uint32; + runLen : uint32; + takeCount : uint32; + fatVal : uint32; + begin + scanPos := rangeStart; + while (scanPos < rangeEnd) and (currentAmount < amount) do begin + fatVal := readFat(volume, scanPos, @info^.BootRecord); + if fatVal <> 0 then begin + scanPos := scanPos + 1; + continue; + end; + runStart := scanPos; + runLen := 0; + while scanPos < rangeEnd do begin + fatVal := readFat(volume, scanPos, @info^.BootRecord); + if fatVal <> 0 then break; + runLen := runLen + 1; + scanPos := scanPos + 1; + end; + takeCount := amount - currentAmount; + if runLen < takeCount then + takeCount := runLen; + appendRun(runStart, takeCount); + end; + end; +begin + runs := LL_New(sizeof(TFATRun)); + currentAmount := 0; + lastAllocated := 0; + if amount = 0 then begin + findFreeClusterRuns := runs; + exit; + end; + if info^.MaxCluster <= 2 then begin + LL_Free(runs); + findFreeClusterRuns := nil; + exit; + end; + + if (sioc_allocVol = volume) and (sioc_allocHint >= 2) and (sioc_allocHint < info^.MaxCluster) then + scanStart := sioc_allocHint + else + scanStart := 2; + + scanRange(scanStart, info^.MaxCluster); + if (currentAmount < amount) and (scanStart > 2) then + scanRange(2, scanStart); + + if currentAmount < amount then begin + LL_Free(runs); + findFreeClusterRuns := nil; + exit; + end; + fatUpdateAllocHint(volume, info, lastAllocated + 1); + findFreeClusterRuns := runs; +end; + +function FAT32FindRunForOffset(ofi : PFATOpenFile; fileOffset : uint32; + hintValid : boolean; hintRunIdx : uint32; hintFileCluster : uint32; + var outRunIdx : uint32; var outRunFileCluster : uint32; + var outStartCluster : uint32; var outClusterOffset : uint32; + var outAvailableBytes : uint32) : boolean; +var + targetCluster : uint32; + clusterBase : uint32; + i : uint32; + bytesPerCluster : uint32; + byteOffsetInCl : uint32; +begin + FAT32FindRunForOffset := false; + if (ofi = nil) or (ofi^.VolumeInfo = nil) or (ofi^.RunMap.Count = 0) then + exit; + + bytesPerCluster := ofi^.VolumeInfo^.BytesPerCluster; + if bytesPerCluster = 0 then exit; + + targetCluster := fileOffset div bytesPerCluster; + byteOffsetInCl := fileOffset mod bytesPerCluster; + + if hintValid and (hintRunIdx < ofi^.RunMap.Count) and (hintFileCluster <= targetCluster) then begin + i := hintRunIdx; + clusterBase := hintFileCluster; + end else begin + i := 0; + clusterBase := 0; + end; + + while i < ofi^.RunMap.Count do begin + if targetCluster < (clusterBase + ofi^.RunMap.Runs[i].ClusterCount) then begin + outRunIdx := i; + outRunFileCluster := clusterBase; + outClusterOffset := targetCluster - clusterBase; + outStartCluster := ofi^.RunMap.Runs[i].StartCluster + outClusterOffset; + outAvailableBytes := ((ofi^.RunMap.Runs[i].ClusterCount - outClusterOffset) * bytesPerCluster) - byteOffsetInCl; + FAT32FindRunForOffset := true; + exit; + end; + clusterBase := clusterBase + ofi^.RunMap.Runs[i].ClusterCount; + i := i + 1; + end; +end; + +function FAT32EnsureCapacityForWrite(ofi : PFATOpenFile; offset : uint32; byteCount : uint32) : TError; +var + newEnd : uint32; + needClusters : uint32; + targetClusters : uint32; + reserve : uint32; + extraClusters : uint32; + lastCluster : uint32; + firstNew : uint32; + tempRuns : PLinkedListBase; + tempRun : PFATTempRun; + i : uint32; + lastTail : uint32; +begin + FAT32EnsureCapacityForWrite := eNone; + if (ofi = nil) or (ofi^.VolumeInfo = nil) then begin + FAT32EnsureCapacityForWrite := eInvalidArgument; + exit; + end; + + if byteCount = 0 then exit; + + FAT32EnsureCapacityForWrite := FAT32EnsureDirEntry(ofi); + if FAT32EnsureCapacityForWrite <> eNone then + exit; + + newEnd := offset + byteCount; + needClusters := fatClustersForBytes(newEnd, ofi^.VolumeInfo^.BytesPerCluster); + targetClusters := needClusters; + if needClusters > ofi^.AllocClusters then begin + reserve := fatPreallocClusterCount(ofi^.VolumeInfo^.BytesPerCluster); + targetClusters := ofi^.AllocClusters + reserve; + if targetClusters < needClusters then + targetClusters := needClusters; + end; + + if targetClusters <= ofi^.AllocClusters then exit; + + extraClusters := targetClusters - ofi^.AllocClusters; + if ofi^.RunMap.Count > 0 then + lastCluster := ofi^.RunMap.Runs[ofi^.RunMap.Count - 1].StartCluster + + ofi^.RunMap.Runs[ofi^.RunMap.Count - 1].ClusterCount - 1 + else + lastCluster := 0; + + if fatFindContiguousFreeRange(ofi^.Volume, ofi^.VolumeInfo, extraClusters, firstNew) then begin + fatLinkContiguousRange(ofi^.Volume, ofi^.VolumeInfo, lastCluster, firstNew, extraClusters, lastTail); + if not runMapAppend(ofi^.RunMap, firstNew, extraClusters) then begin + FAT32EnsureCapacityForWrite := eOutOfMemory; + exit; + end; + if ofi^.FirstCluster = 0 then + ofi^.FirstCluster := firstNew; + end else begin + tempRuns := findFreeClusterRuns(ofi^.Volume, ofi^.VolumeInfo, extraClusters); + if tempRuns = nil then begin + FAT32EnsureCapacityForWrite := eDiskFull; + exit; + end; + for i := 0 to LL_Size(tempRuns) - 1 do begin + tempRun := PFATTempRun(LL_Get(tempRuns, i)); + fatLinkContiguousRange(ofi^.Volume, ofi^.VolumeInfo, lastCluster, + tempRun^.StartCluster, tempRun^.ClusterCount, lastTail); + lastCluster := lastTail; + if not runMapAppend(ofi^.RunMap, tempRun^.StartCluster, tempRun^.ClusterCount) then begin + LL_Free(tempRuns); + FAT32EnsureCapacityForWrite := eOutOfMemory; + exit; + end; + if ofi^.FirstCluster = 0 then + ofi^.FirstCluster := tempRun^.StartCluster; + end; + LL_Free(tempRuns); + end; + + ofi^.AllocClusters := ofi^.RunMap.TotalClusters; + ofi^.FatDirty := true; + ofi^.MetaDirty := true; +end; + +function FAT32TransferAlloc(volume : PStorage_Volume) : PFATTransferCtx; +var + volCtx : PFATVolumeCtx; + ctx : PFATTransferCtx; + idx : uint32; +begin + FAT32TransferAlloc := nil; + volCtx := FAT32GetVolumeCtx(volume); + if volCtx = nil then exit; + + asm pushf; cli end; + ctx := PFATTransferCtx(volCtx^.TransferFreeList); + if ctx <> nil then + volCtx^.TransferFreeList := pointer(ctx^.Next); + asm popf end; + + if ctx = nil then exit; + idx := (uint32(ctx) - uint32(volCtx^.TransferPoolBuf)) div sizeof(TFATTransferCtx); + memset(uint32(ctx), 0, sizeof(TFATTransferCtx)); + ctx^.VolumeCtx := volCtx; + ctx^.ScratchSector := puint32(uint32(volCtx^.TransferScratchBuf) + (idx * volCtx^.Info.BootRecord.sectorSize)); + ctx^.ScratchSize := volCtx^.Info.BootRecord.sectorSize; + FAT32TransferAlloc := ctx; +end; + +procedure FAT32TransferFree(ctx : PFATTransferCtx); +var + volCtx : PFATVolumeCtx; +begin + if ctx = nil then exit; + volCtx := ctx^.VolumeCtx; + if volCtx = nil then exit; + asm pushf; cli end; + ctx^.Next := PFATTransferCtx(volCtx^.TransferFreeList); + volCtx^.TransferFreeList := pointer(ctx); + asm popf end; +end; + +function FAT32OpenFile(volume : PStorage_Volume; directory : pchar; fileName : pchar; var fileSize : uint32) : pointer; +var + ofi : PFATOpenFile; + info : PFATVolumeInfo; + sectorBuf : puint32; + namePart : pchar; + extPart : pchar; + cleanName : byteArray8; + parentCluster : uint32; +begin + fileSize := 0; + FAT32OpenFile := nil; + info := FAT32GetVolumeInfo(volume); + if info = nil then exit; + + if not isValidFAT32Name(fileName) then + exit; + + ofi := PFATOpenFile(kalloc(sizeof(TFATOpenFile))); + if ofi = nil then exit; + memset(uint32(ofi), 0, sizeof(TFATOpenFile)); + ofi^.Volume := volume; + ofi^.VolumeInfo := info; + ofi^.ScratchSize := info^.BootRecord.sectorSize; + if ofi^.ScratchSize > 0 then begin + ofi^.ScratchSector := puint32(kalloc(ofi^.ScratchSize)); + if ofi^.ScratchSector <> nil then + memset(uint32(ofi^.ScratchSector), 0, ofi^.ScratchSize); + end; + + splitFileNameParts(fileName, namePart, extPart); + cleanName := cleanString(namePart); + ofi^.CleanName := cleanName; + ofi^.ExtPart := stringCopy(extPart); + + if not resolveDirectoryCluster(volume, info, directory, parentCluster) then begin + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + FAT32CloseFile(ofi); + exit; + end; + ofi^.DirLoc.ParentCluster := parentCluster; + + sectorBuf := puint32(kalloc(info^.BootRecord.sectorSize)); + if sectorBuf <> nil then begin + if locateDirEntry(volume, info, parentCluster, cleanName, extPart, ofi^.DirLoc, sectorBuf) then begin + ofi^.Exists := true; + ofi^.FirstCluster := dirFirstCluster(@PDirectory(sectorBuf)[ofi^.DirLoc.EntryIdx]); + ofi^.ByteSize := PDirectory(sectorBuf)[ofi^.DirLoc.EntryIdx].byteSize; + if (ofi^.FirstCluster >= 2) and not buildRunMap(volume, info, ofi^.FirstCluster, ofi^.RunMap) then begin + kfree(sectorBuf); + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + FAT32CloseFile(ofi); + exit; + end; + ofi^.AllocClusters := ofi^.RunMap.TotalClusters; + end; + kfree(sectorBuf); + end; + + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + fileSize := ofi^.ByteSize; + FAT32OpenFile := ofi; +end; + +procedure FAT32CloseFile(ctx : pointer); +var + ofi : PFATOpenFile; + sectorBuf : puint32; + rawDir : PDirectory; +begin + if ctx = nil then exit; + ofi := PFATOpenFile(ctx); + + if ofi^.FatDirty and (ofi^.Volume <> nil) then + FAT32Flush(ofi^.Volume); + + if ofi^.MetaDirty and ofi^.DirLoc.Valid then begin + sectorBuf := puint32(kalloc(ofi^.VolumeInfo^.BootRecord.sectorSize)); + if sectorBuf <> nil then begin + driver.storage.mgr.storage_read(ofi^.Volume^.device, ofi^.DirLoc.SectorLBA, 1, sectorBuf); + rawDir := @PDirectory(sectorBuf)[ofi^.DirLoc.EntryIdx]; + rawDir^.byteSize := ofi^.ByteSize; + setDirFirstCluster(rawDir, ofi^.FirstCluster); + driver.storage.mgr.storage_write(ofi^.Volume^.device, ofi^.DirLoc.SectorLBA, 1, sectorBuf); + kfree(sectorBuf); + end; + end; + + runMapFree(ofi^.RunMap); + if ofi^.ExtPart <> nil then + kfree(void(ofi^.ExtPart)); + if ofi^.ScratchSector <> nil then + kfree(ofi^.ScratchSector); + kfree(void(ofi)); +end; + +procedure fatFreeGenericEntries(entries : PLinkedListBase); +var + i : uint32; + entry : PDirectory_Entry; +begin + if entries = nil then exit; + for i := 0 to LL_Size(entries) - 1 do begin + entry := PDirectory_Entry(LL_Get(entries, i)); + if (entry <> nil) and (entry^.fileName <> nil) then + kfree(void(entry^.fileName)); + end; + LL_Free(entries); +end; + +procedure fatFreeClusterChain(volume : PStorage_Volume; info : PFATVolumeInfo; startCluster : uint32); +var + currentCluster : uint32; + nextCluster : uint32; + runStart : uint32; + runCount : uint32; + iterCount : uint32; +begin + if (volume = nil) or (info = nil) or (startCluster < 2) then exit; + currentCluster := startCluster; + runStart := 0; + runCount := 0; + iterCount := 0; + + while true do begin + if (currentCluster < 2) or (currentCluster >= info^.MaxCluster) then + break; + if iterCount >= info^.MaxCluster then + break; + + nextCluster := readFat(volume, currentCluster, @info^.BootRecord); + writeFat(volume, currentCluster, 0, @info^.BootRecord); + + if runCount = 0 then begin + runStart := currentCluster; + runCount := 1; + end else if currentCluster = (runStart + runCount) then + runCount := runCount + 1 + else begin + fatRememberAllocHint(volume, runStart, runCount); + runStart := currentCluster; + runCount := 1; + end; + + iterCount := iterCount + 1; + if isEndOfChain(nextCluster) or isBadCluster(nextCluster) or (nextCluster = 0) then + break; + currentCluster := nextCluster; + end; + + if runCount > 0 then + fatRememberAllocHint(volume, runStart, runCount); +end; + +function fatDirectoryHasChildren(volume : PStorage_Volume; info : PFATVolumeInfo; firstCluster : uint32) : boolean; +var + buffer : puint32; + currentCluster : uint32; + nextCluster : uint32; + clusterLBA : uint32; + ds : uint32; + idx : uint32; + iterCount : uint32; + entriesPerSec: uint32; + dir : PDirectory; +begin + fatDirectoryHasChildren := false; + if (volume = nil) or (info = nil) or (firstCluster < 2) then exit; + + buffer := puint32(kalloc(info^.BootRecord.sectorSize)); + if buffer = nil then begin + fatDirectoryHasChildren := true; + exit; + end; + + entriesPerSec := info^.BootRecord.sectorSize div sizeof(TDirectory); + currentCluster := firstCluster; + iterCount := 0; + + while currentCluster >= 2 do begin + if (currentCluster >= info^.MaxCluster) or (iterCount >= info^.MaxCluster) then + break; + clusterLBA := FAT32ClusterToLBA(info, currentCluster); + for ds := 0 to info^.BootRecord.spc - 1 do begin + driver.storage.mgr.storage_read(volume^.device, clusterLBA + ds, 1, buffer); + for idx := 0 to entriesPerSec - 1 do begin + dir := @PDirectory(buffer)[idx]; + if dir^.fileName[0] = char(0) then begin + kfree(buffer); + exit; + end; + if dir^.fileName[0] = char($E5) then + continue; + if dir^.attributes = $0F then + continue; + if fatIsDotEntry(dir) then + continue; + fatDirectoryHasChildren := true; + kfree(buffer); + exit; + end; + end; + nextCluster := readFat(volume, currentCluster, @info^.BootRecord); + if isEndOfChain(nextCluster) or isBadCluster(nextCluster) or (nextCluster = 0) then + break; + currentCluster := nextCluster; + iterCount := iterCount + 1; + end; + + kfree(buffer); +end; + +function fatResolveLeafPath(volume : PStorage_Volume; info : PFATVolumeInfo; fullPath : pchar; + var parentDir : pchar; var leafName : pchar; var parentCluster : uint32; var statusOut : TError) : boolean; +begin + fatResolveLeafPath := false; + parentDir := nil; + leafName := nil; + parentCluster := 0; + statusOut := eDirectoryDoesNotExist; + + splitPathParts(fullPath, parentDir, leafName); + if (leafName = nil) or (leafName[0] = char(0)) then begin + statusOut := eInvalidFileName; + exit; + end; + + if not resolveDirectoryCluster(volume, info, parentDir, parentCluster) then begin + statusOut := eDirectoryDoesNotExist; + exit; + end; + + fatResolveLeafPath := true; + statusOut := eNone; +end; + +procedure fatInitDirectoryCluster(volume : PStorage_Volume; info : PFATVolumeInfo; cluster : uint32; parentCluster : uint32); +var + buffer : puint32; + ds : uint32; + clusterLBA : uint32; + dir : PDirectory; +begin + if (volume = nil) or (info = nil) or (cluster < 2) then exit; + buffer := puint32(kalloc(info^.BootRecord.sectorSize)); + if buffer = nil then exit; + clusterLBA := FAT32ClusterToLBA(info, cluster); + + for ds := 0 to info^.BootRecord.spc - 1 do begin + memset(uint32(buffer), 0, info^.BootRecord.sectorSize); + if ds = 0 then begin + dir := @PDirectory(buffer)[0]; + dir^.fileName[0] := '.'; + dir^.fileName[1] := ' '; + dir^.fileName[2] := ' '; + dir^.fileName[3] := ' '; + dir^.fileName[4] := ' '; + dir^.fileName[5] := ' '; + dir^.fileName[6] := ' '; + dir^.fileName[7] := ' '; + dir^.attributes := $10; + setDirFirstCluster(dir, cluster); + + dir := @PDirectory(buffer)[1]; + dir^.fileName[0] := '.'; + dir^.fileName[1] := '.'; + dir^.fileName[2] := ' '; + dir^.fileName[3] := ' '; + dir^.fileName[4] := ' '; + dir^.fileName[5] := ' '; + dir^.fileName[6] := ' '; + dir^.fileName[7] := ' '; + dir^.attributes := $10; + setDirFirstCluster(dir, parentCluster); + end; + driver.storage.mgr.storage_write(volume^.device, clusterLBA + ds, 1, buffer); + end; + + kfree(buffer); +end; + +function FAT32ReadDirectory(volume : PStorage_Volume; directory : pchar; statusOut : puint32) : PLinkedListBase; +var + info : PFATVolumeInfo; + entries : PLinkedListBase; + buffer : puint32; + parentCluster : uint32; + currentCluster: uint32; + nextCluster : uint32; + clusterLBA : uint32; + entriesPerSec : uint32; + iterCount : uint32; + ds : uint32; + idx : uint32; + rawDir : PDirectory; + outEntry : PDirectory_Entry; + slot : puint32; + stopScan : boolean; +begin + FAT32ReadDirectory := nil; + if statusOut <> nil then statusOut^ := ord(eUnknown); + info := FAT32GetVolumeInfo(volume); + if info = nil then exit; + if not resolveDirectoryCluster(volume, info, directory, parentCluster) then begin + if statusOut <> nil then statusOut^ := ord(eDirectoryDoesNotExist); + exit; + end; + + entries := LL_New(sizeof(TDirectory_Entry)); + if entries = nil then begin + if statusOut <> nil then statusOut^ := ord(eOutOfMemory); + exit; + end; + + buffer := puint32(kalloc(info^.BootRecord.sectorSize)); + if buffer = nil then begin + LL_Free(entries); + if statusOut <> nil then statusOut^ := ord(eOutOfMemory); + exit; + end; + + currentCluster := parentCluster; + entriesPerSec := info^.BootRecord.sectorSize div sizeof(TDirectory); + iterCount := 0; + stopScan := false; + + while (currentCluster >= 2) and not stopScan do begin + if (currentCluster >= info^.MaxCluster) or (iterCount >= info^.MaxCluster) then begin + fatFreeGenericEntries(entries); + kfree(buffer); + if statusOut <> nil then statusOut^ := ord(eCorruptFilesystem); + exit; + end; + + clusterLBA := FAT32ClusterToLBA(info, currentCluster); + for ds := 0 to info^.BootRecord.spc - 1 do begin + driver.storage.mgr.storage_read(volume^.device, clusterLBA + ds, 1, buffer); + for idx := 0 to entriesPerSec - 1 do begin + rawDir := @PDirectory(buffer)[idx]; + if rawDir^.fileName[0] = char(0) then begin + stopScan := true; + break; + end; + if rawDir^.fileName[0] = char($E5) then + continue; + if rawDir^.attributes = $0F then + continue; + if (rawDir^.attributes and $08) = $08 then + continue; + + slot := LL_Add(entries); + if slot = nil then begin + fatFreeGenericEntries(entries); + kfree(buffer); + if statusOut <> nil then statusOut^ := ord(eOutOfMemory); + exit; + end; + outEntry := PDirectory_Entry(slot); + memset(uint32(outEntry), 0, sizeof(TDirectory_Entry)); + outEntry^.fileName := fatEntryName(rawDir); + if outEntry^.fileName = nil then begin + fatFreeGenericEntries(entries); + kfree(buffer); + if statusOut <> nil then statusOut^ := ord(eOutOfMemory); + exit; + end; + if (rawDir^.attributes and $10) = $10 then + outEntry^.entryType := directoryEntry + else + outEntry^.entryType := fileEntry; + outEntry^.fileSize := rawDir^.byteSize; + outEntry^.modifiedDate := rawDir^.modifiedDate; + outEntry^.modifiedTime := rawDir^.modifiedTime; + outEntry^.attributes := rawDir^.attributes; + end; + if stopScan then + break; + end; + + nextCluster := readFat(volume, currentCluster, @info^.BootRecord); + if isEndOfChain(nextCluster) or isBadCluster(nextCluster) or (nextCluster = 0) then + break; + currentCluster := nextCluster; + iterCount := iterCount + 1; + end; + + kfree(buffer); + if statusOut <> nil then statusOut^ := ord(eNone); + FAT32ReadDirectory := entries; +end; + +procedure FAT32CreateDirectory(volume : PStorage_Volume; directory : pchar; dirName : pchar; attributes : uint32; statusOut : puint32); +var + info : PFATVolumeInfo; + parentCluster : uint32; + namePart : pchar; + extPart : pchar; + cleanName : byteArray8; + sectorBuf : puint32; + loc : TFATDirEntryLocation; + rawDir : PDirectory; + cluster : uint32; + lastTail : uint32; + tempRuns : PLinkedListBase; + tempRun : PFATTempRun; +begin + if statusOut <> nil then statusOut^ := ord(eUnknown); + info := FAT32GetVolumeInfo(volume); + if info = nil then exit; + + if (dirName = nil) or not isValidFAT32Name(dirName) then begin + if statusOut <> nil then statusOut^ := ord(eInvalidFileName); + exit; + end; + + if not resolveDirectoryCluster(volume, info, directory, parentCluster) then begin + if statusOut <> nil then statusOut^ := ord(eDirectoryDoesNotExist); + exit; + end; + + splitFileNameParts(dirName, namePart, extPart); + cleanName := cleanString(namePart); + + sectorBuf := puint32(kalloc(info^.BootRecord.sectorSize)); + if sectorBuf = nil then begin + if statusOut <> nil then statusOut^ := ord(eOutOfMemory); + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + exit; + end; + + if locateDirEntry(volume, info, parentCluster, cleanName, extPart, loc, sectorBuf) then begin + if statusOut <> nil then statusOut^ := ord(eDirectoryAlreadyExists); + kfree(sectorBuf); + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + exit; + end; + + if not locateFreeDirEntry(volume, info, parentCluster, loc, sectorBuf) then begin + if statusOut <> nil then statusOut^ := ord(eDirectoryFull); + kfree(sectorBuf); + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + exit; + end; + + cluster := 0; + if fatFindContiguousFreeRange(volume, info, 1, cluster) then + fatLinkContiguousRange(volume, info, 0, cluster, 1, lastTail) + else begin + tempRuns := findFreeClusterRuns(volume, info, 1); + if (tempRuns = nil) or (LL_Size(tempRuns) = 0) then begin + if tempRuns <> nil then LL_Free(tempRuns); + if statusOut <> nil then statusOut^ := ord(eDiskFull); + kfree(sectorBuf); + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + exit; + end; + tempRun := PFATTempRun(LL_Get(tempRuns, 0)); + cluster := tempRun^.StartCluster; + fatLinkContiguousRange(volume, info, 0, cluster, 1, lastTail); + LL_Free(tempRuns); + end; + + fatInitDirectoryCluster(volume, info, cluster, parentCluster); + + rawDir := @PDirectory(sectorBuf)[loc.EntryIdx]; + memset(uint32(rawDir), 0, sizeof(TDirectory)); + rawDir^.fileName := cleanName; + rawDir^.attributes := attributes; + fillFatExt(rawDir^.fileExtension, extPart); + rawDir^.byteSize := 0; + setDirFirstCluster(rawDir, cluster); + fatRememberFreeEntryFromBuffer(volume, parentCluster, loc.SectorLBA, sectorBuf, + info^.BootRecord.sectorSize div sizeof(TDirectory)); + driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, sectorBuf); + FAT32Flush(volume); + + if statusOut <> nil then statusOut^ := ord(eNone); + kfree(sectorBuf); + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); +end; + +procedure FAT32DeleteFile(volume : PStorage_Volume; filePath : pchar; statusOut : puint32); +var + info : PFATVolumeInfo; + parentDir : pchar = nil; + fileName : pchar = nil; + namePart : pchar = nil; + extPart : pchar = nil; + parentCluster : uint32; + err : TError; + cleanName : byteArray8; + sectorBuf : puint32 = nil; + loc : TFATDirEntryLocation; + rawDir : PDirectory; + firstCluster : uint32; + resultErr : TError; +begin + resultErr := eUnknown; + if statusOut <> nil then statusOut^ := ord(resultErr); + info := FAT32GetVolumeInfo(volume); + if info = nil then begin + if statusOut <> nil then statusOut^ := ord(resultErr); + exit; + end; + + if not fatResolveLeafPath(volume, info, filePath, parentDir, fileName, parentCluster, err) then begin + resultErr := err; + end else begin + splitFileNameParts(fileName, namePart, extPart); + cleanName := cleanString(namePart); + sectorBuf := puint32(kalloc(info^.BootRecord.sectorSize)); + if sectorBuf = nil then + resultErr := eOutOfMemory + else if not locateDirEntry(volume, info, parentCluster, cleanName, extPart, loc, sectorBuf) then + resultErr := eFileDoesNotExist + else begin + rawDir := @PDirectory(sectorBuf)[loc.EntryIdx]; + if (rawDir^.attributes and $10) = $10 then + resultErr := eNotADirectory + else begin + firstCluster := dirFirstCluster(rawDir); + if firstCluster >= 2 then + fatFreeClusterChain(volume, info, firstCluster); + rawDir^.fileName[0] := char($E5); + fatRememberDirHint(volume, parentCluster, loc.SectorLBA, loc.EntryIdx); + driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, sectorBuf); + if firstCluster >= 2 then + FAT32Flush(volume); + resultErr := eNone; + end; + end; + end; + if sectorBuf <> nil then kfree(sectorBuf); + if parentDir <> nil then kfree(void(parentDir)); + if fileName <> nil then kfree(void(fileName)); + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + if statusOut <> nil then statusOut^ := ord(resultErr); +end; + +procedure FAT32DeleteDir(volume : PStorage_Volume; path : pchar; statusOut : puint32); +var + info : PFATVolumeInfo; + parentDir : pchar = nil; + dirName : pchar = nil; + namePart : pchar = nil; + extPart : pchar = nil; + parentCluster : uint32; + err : TError; + cleanName : byteArray8; + sectorBuf : puint32 = nil; + loc : TFATDirEntryLocation; + rawDir : PDirectory; + firstCluster : uint32; + resultErr : TError; +begin + resultErr := eUnknown; + if statusOut <> nil then statusOut^ := ord(resultErr); + info := FAT32GetVolumeInfo(volume); + if info = nil then begin + if statusOut <> nil then statusOut^ := ord(resultErr); + exit; + end; + + if not fatResolveLeafPath(volume, info, path, parentDir, dirName, parentCluster, err) then begin + resultErr := err; + end else begin + splitFileNameParts(dirName, namePart, extPart); + cleanName := cleanString(namePart); + sectorBuf := puint32(kalloc(info^.BootRecord.sectorSize)); + if sectorBuf = nil then + resultErr := eOutOfMemory + else if not locateDirEntry(volume, info, parentCluster, cleanName, extPart, loc, sectorBuf) then + resultErr := eDirectoryDoesNotExist + else begin + rawDir := @PDirectory(sectorBuf)[loc.EntryIdx]; + if (rawDir^.attributes and $10) <> $10 then + resultErr := eNotADirectory + else begin + firstCluster := dirFirstCluster(rawDir); + if fatDirectoryHasChildren(volume, info, firstCluster) then + resultErr := eDirectoryNotEmpty + else begin + if firstCluster >= 2 then + fatFreeClusterChain(volume, info, firstCluster); + rawDir^.fileName[0] := char($E5); + fatRememberDirHint(volume, parentCluster, loc.SectorLBA, loc.EntryIdx); + driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, sectorBuf); + if firstCluster >= 2 then + FAT32Flush(volume); + resultErr := eNone; + end; + end; + end; + end; + if sectorBuf <> nil then kfree(sectorBuf); + if parentDir <> nil then kfree(void(parentDir)); + if dirName <> nil then kfree(void(dirName)); + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + if statusOut <> nil then statusOut^ := ord(resultErr); +end; + +procedure FAT32RenameFile(volume : PStorage_Volume; filePath : pchar; newName : pchar; statusOut : puint32); +var + info : PFATVolumeInfo; + parentDir : pchar = nil; + fileName : pchar = nil; + namePart : pchar = nil; + extPart : pchar = nil; + newNamePart : pchar = nil; + newExtPart : pchar = nil; + parentCluster : uint32; + err : TError; + cleanName : byteArray8; + newCleanName : byteArray8; + sectorBuf : puint32 = nil; + loc : TFATDirEntryLocation; + rawDir : PDirectory; + resultErr : TError; +begin + resultErr := eUnknown; + if statusOut <> nil then statusOut^ := ord(resultErr); + info := FAT32GetVolumeInfo(volume); + if info = nil then begin + if statusOut <> nil then statusOut^ := ord(resultErr); + exit; + end; + + if (newName = nil) or not isValidFAT32Name(newName) then begin + if statusOut <> nil then statusOut^ := ord(eInvalidFileName); + exit; + end; + + if not fatResolveLeafPath(volume, info, filePath, parentDir, fileName, parentCluster, err) then begin + resultErr := err; + end else begin + splitFileNameParts(fileName, namePart, extPart); + cleanName := cleanString(namePart); + sectorBuf := puint32(kalloc(info^.BootRecord.sectorSize)); + if sectorBuf = nil then + resultErr := eOutOfMemory + else if not locateDirEntry(volume, info, parentCluster, cleanName, extPart, loc, sectorBuf) then + resultErr := eFileDoesNotExist + else begin + rawDir := @PDirectory(sectorBuf)[loc.EntryIdx]; + splitFileNameParts(newName, newNamePart, newExtPart); + newCleanName := cleanString(newNamePart); + rawDir^.fileName := newCleanName; + fillFatExt(rawDir^.fileExtension, newExtPart); + driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, sectorBuf); + resultErr := eNone; + end; + end; + if sectorBuf <> nil then kfree(sectorBuf); + if parentDir <> nil then kfree(void(parentDir)); + if fileName <> nil then kfree(void(fileName)); + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + if newNamePart <> nil then kfree(void(newNamePart)); + if newExtPart <> nil then kfree(void(newExtPart)); + if statusOut <> nil then statusOut^ := ord(resultErr); +end; + +function FAT32GetFileSize(volume : PStorage_Volume; directory : pchar; fileName : pchar) : uint32; +var + ctx : pointer; +begin + FAT32GetFileSize := 0; + ctx := FAT32OpenFile(volume, directory, fileName, FAT32GetFileSize); + if ctx <> nil then + FAT32CloseFile(ctx); +end; + +function FAT32GetReadLimit(ofi : PFATOpenFile; offset : uint32; byteCount : uint32) : uint32; +begin + FAT32GetReadLimit := 0; + if (ofi = nil) or (offset >= ofi^.ByteSize) then exit; + FAT32GetReadLimit := ofi^.ByteSize - offset; + if byteCount < FAT32GetReadLimit then + FAT32GetReadLimit := byteCount; +end; + +end. diff --git a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.transfer.pas b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.transfer.pas new file mode 100644 index 00000000..a164c2fb --- /dev/null +++ b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.transfer.pas @@ -0,0 +1,410 @@ +unit driver.storage.fs.fat32.transfer; + +interface + +uses + driver.storage.types; + +function FAT32ReadFileAtOffset(volume : PStorage_Volume; directory : pchar; fileName : pchar; + offset : uint32; buffer : puint32; byteCount : uint32; ctx : pointer) : uint32; +function FAT32WriteFileAtOffset(volume : PStorage_Volume; directory : pchar; fileName : pchar; + offset : uint32; buffer : puint32; byteCount : uint32; ctx : pointer) : uint32; +procedure FAT32ReadFileAtOffsetAsync(volume : PStorage_Volume; directory : pchar; fileName : pchar; + offset : uint32; buffer : puint32; byteCount : uint32; ctx : pointer; bytesRead : puint32; + callback : TIOCallback; callbackData : pointer); +procedure FAT32WriteFileAtOffsetAsync(volume : PStorage_Volume; directory : pchar; fileName : pchar; + offset : uint32; buffer : puint32; byteCount : uint32; ctx : pointer; bytesWritten : puint32; + callback : TIOCallback; callbackData : pointer); + +implementation + +uses + driver.storage.mgr, + core.util, arch.x86.util, + memory.heap, + driver.storage.fs.fat32.types, + driver.storage.fs.fat32.core; + +procedure noteWriteProgress(ofi : PFATOpenFile; completedEnd : uint32); +begin + if ofi = nil then exit; + if (ofi^.FirstCluster = 0) and (ofi^.RunMap.Count > 0) then + ofi^.FirstCluster := ofi^.RunMap.Runs[0].StartCluster; + if completedEnd > ofi^.ByteSize then begin + ofi^.ByteSize := completedEnd; + ofi^.MetaDirty := true; + end; +end; + +function transferSync(ofi : PFATOpenFile; buffer : puint32; offset : uint32; byteCount : uint32; + mode : TFATTransferMode; scratch : puint32; scratchSize : uint32) : uint32; +var + remaining : uint32; + bytesDone : uint32; + runIdx : uint32; + runFileCluster : uint32; + startCluster : uint32; + clusterOffset : uint32; + availableBytes : uint32; + hintValid : boolean; + hintRunIdx : uint32; + hintFileCluster : uint32; + bytesPerCluster : uint32; + sectorSize : uint32; + currentOffset : uint32; + runByteOffset : uint32; + sectorOffset : uint32; + chunkBytes : uint32; + chunkSectors : uint32; + sectorLBA : uint32; +begin + transferSync := 0; + if (ofi = nil) or (buffer = nil) or (byteCount = 0) then exit; + + remaining := byteCount; + bytesDone := 0; + hintValid := false; + hintRunIdx := 0; + hintFileCluster := 0; + bytesPerCluster := ofi^.VolumeInfo^.BytesPerCluster; + sectorSize := ofi^.VolumeInfo^.BootRecord.sectorSize; + + while remaining > 0 do begin + currentOffset := offset + bytesDone; + if not FAT32FindRunForOffset(ofi, currentOffset, hintValid, hintRunIdx, hintFileCluster, + runIdx, runFileCluster, startCluster, clusterOffset, availableBytes) then + break; + + hintValid := true; + hintRunIdx := runIdx; + hintFileCluster := runFileCluster; + + runByteOffset := currentOffset - (runFileCluster * bytesPerCluster); + sectorOffset := runByteOffset mod sectorSize; + sectorLBA := FAT32ClusterToLBA(ofi^.VolumeInfo, startCluster) + (runByteOffset div sectorSize); + + if (sectorOffset <> 0) or (remaining < sectorSize) then begin + if scratch = nil then break; + chunkBytes := sectorSize - sectorOffset; + if chunkBytes > remaining then + chunkBytes := remaining; + driver.storage.mgr.storage_read(ofi^.Volume^.device, sectorLBA, 1, scratch); + if mode = ftmRead then + memcpy(uint32(scratch) + sectorOffset, uint32(buffer) + bytesDone, chunkBytes) + else begin + memcpy(uint32(buffer) + bytesDone, uint32(scratch) + sectorOffset, chunkBytes); + driver.storage.mgr.storage_write(ofi^.Volume^.device, sectorLBA, 1, scratch); + end; + end else begin + chunkBytes := remaining; + if chunkBytes > availableBytes then + chunkBytes := availableBytes; + chunkBytes := (chunkBytes div sectorSize) * sectorSize; + if chunkBytes = 0 then break; + chunkSectors := chunkBytes div sectorSize; + if chunkSectors > FAT_MAX_IO_SECTORS then + chunkSectors := FAT_MAX_IO_SECTORS; + chunkBytes := chunkSectors * sectorSize; + if mode = ftmRead then + driver.storage.mgr.storage_read(ofi^.Volume^.device, sectorLBA, chunkSectors, + puint32(uint32(buffer) + bytesDone)) + else + driver.storage.mgr.storage_write(ofi^.Volume^.device, sectorLBA, chunkSectors, + puint32(uint32(buffer) + bytesDone)); + end; + + bytesDone := bytesDone + chunkBytes; + remaining := remaining - chunkBytes; + if mode = ftmWrite then + noteWriteProgress(ofi, offset + bytesDone); + end; + + transferSync := bytesDone; +end; + +procedure asyncComplete(ctx : PFATTransferCtx; error : TError); +begin + if ctx = nil then exit; + if ctx^.BytesOut <> nil then + ctx^.BytesOut^ := ctx^.BytesDone; + if ctx^.Callback <> nil then + ctx^.Callback(error, ctx^.CallbackData); + FAT32TransferFree(ctx); +end; + +procedure asyncContinue(ctx : PFATTransferCtx); forward; + +procedure asyncStep(error : TError; userdata : pointer); +var + ctx : PFATTransferCtx; +begin + ctx := PFATTransferCtx(userdata); + if ctx = nil then exit; + + if error <> eNone then begin + asyncComplete(ctx, error); + exit; + end; + + case ctx^.Phase of + ftpAwaitDirect: begin + ctx^.BytesDone := ctx^.BytesDone + ctx^.PendingAdvanceBytes; + if ctx^.Mode = ftmWrite then + noteWriteProgress(ctx^.OpenFile, ctx^.Offset + ctx^.BytesDone); + asyncContinue(ctx); + end; + ftpAwaitScratchRead: begin + if ctx^.Mode = ftmRead then begin + memcpy(uint32(ctx^.ScratchSector) + ctx^.PendingScratchOffset, + uint32(ctx^.Buffer) + ctx^.PendingBufferOffset, ctx^.PendingScratchBytes); + ctx^.BytesDone := ctx^.BytesDone + ctx^.PendingScratchBytes; + asyncContinue(ctx); + end else begin + memcpy(uint32(ctx^.Buffer) + ctx^.PendingBufferOffset, + uint32(ctx^.ScratchSector) + ctx^.PendingScratchOffset, ctx^.PendingScratchBytes); + ctx^.Phase := ftpAwaitScratchWrite; + if driver.storage.mgr.storage_write_async( + ctx^.Volume^.device, ctx^.PendingLBA, 1, ctx^.ScratchSector, @asyncStep, ctx) <> eNone then + asyncComplete(ctx, eIOError); + end; + end; + ftpAwaitScratchWrite: begin + ctx^.BytesDone := ctx^.BytesDone + ctx^.PendingScratchBytes; + noteWriteProgress(ctx^.OpenFile, ctx^.Offset + ctx^.BytesDone); + asyncContinue(ctx); + end; + else + asyncComplete(ctx, eUnknown); + end; +end; + +procedure asyncContinue(ctx : PFATTransferCtx); +var + runIdx : uint32; + runFileCluster : uint32; + startCluster : uint32; + clusterOffset : uint32; + availableBytes : uint32; + bytesPerCluster : uint32; + sectorSize : uint32; + currentOffset : uint32; + remaining : uint32; + runByteOffset : uint32; + sectorOffset : uint32; + chunkBytes : uint32; + chunkSectors : uint32; + err : TError; +begin + if ctx = nil then exit; + if ctx^.BytesDone >= ctx^.ByteCount then begin + asyncComplete(ctx, eNone); + exit; + end; + + bytesPerCluster := ctx^.OpenFile^.VolumeInfo^.BytesPerCluster; + sectorSize := ctx^.OpenFile^.VolumeInfo^.BootRecord.sectorSize; + currentOffset := ctx^.Offset + ctx^.BytesDone; + remaining := ctx^.ByteCount - ctx^.BytesDone; + + if not FAT32FindRunForOffset(ctx^.OpenFile, currentOffset, ctx^.RunHintValid, ctx^.RunHintIndex, + ctx^.RunHintFileCluster, runIdx, runFileCluster, startCluster, clusterOffset, availableBytes) then begin + asyncComplete(ctx, eCorruptFilesystem); + exit; + end; + + ctx^.RunHintValid := true; + ctx^.RunHintIndex := runIdx; + ctx^.RunHintFileCluster := runFileCluster; + + runByteOffset := currentOffset - (runFileCluster * bytesPerCluster); + sectorOffset := runByteOffset mod sectorSize; + ctx^.PendingLBA := FAT32ClusterToLBA(ctx^.OpenFile^.VolumeInfo, startCluster) + (runByteOffset div sectorSize); + ctx^.PendingBufferOffset := ctx^.BytesDone; + + if (sectorOffset <> 0) or (remaining < sectorSize) then begin + chunkBytes := sectorSize - sectorOffset; + if chunkBytes > remaining then + chunkBytes := remaining; + ctx^.PendingScratchOffset := sectorOffset; + ctx^.PendingScratchBytes := chunkBytes; + ctx^.Phase := ftpAwaitScratchRead; + err := driver.storage.mgr.storage_read_async(ctx^.Volume^.device, ctx^.PendingLBA, 1, + ctx^.ScratchSector, @asyncStep, ctx); + if err <> eNone then + asyncComplete(ctx, err); + exit; + end; + + chunkBytes := remaining; + if chunkBytes > availableBytes then + chunkBytes := availableBytes; + chunkBytes := (chunkBytes div sectorSize) * sectorSize; + if chunkBytes = 0 then begin + asyncComplete(ctx, eInvalidArgument); + exit; + end; + chunkSectors := chunkBytes div sectorSize; + if chunkSectors > FAT_MAX_IO_SECTORS then + chunkSectors := FAT_MAX_IO_SECTORS; + ctx^.PendingAdvanceBytes := chunkSectors * sectorSize; + ctx^.PendingSectors := chunkSectors; + ctx^.Phase := ftpAwaitDirect; + + if ctx^.Mode = ftmRead then + err := driver.storage.mgr.storage_read_async(ctx^.Volume^.device, ctx^.PendingLBA, chunkSectors, + puint32(uint32(ctx^.Buffer) + ctx^.PendingBufferOffset), @asyncStep, ctx) + else + err := driver.storage.mgr.storage_write_async(ctx^.Volume^.device, ctx^.PendingLBA, chunkSectors, + puint32(uint32(ctx^.Buffer) + ctx^.PendingBufferOffset), @asyncStep, ctx); + if err <> eNone then + asyncComplete(ctx, err); +end; + +function FAT32ReadFileAtOffset(volume : PStorage_Volume; directory : pchar; fileName : pchar; + offset : uint32; buffer : puint32; byteCount : uint32; ctx : pointer) : uint32; +var + ofi : PFATOpenFile; + tempCtx : pointer; + fileSize : uint32; + readLimit : uint32; +begin + FAT32ReadFileAtOffset := 0; + if (buffer = nil) or (byteCount = 0) then exit; + + ofi := PFATOpenFile(ctx); + tempCtx := nil; + if ofi = nil then begin + tempCtx := FAT32OpenFile(volume, directory, fileName, fileSize); + ofi := PFATOpenFile(tempCtx); + end; + if ofi = nil then exit; + + readLimit := FAT32GetReadLimit(ofi, offset, byteCount); + if readLimit > 0 then + FAT32ReadFileAtOffset := transferSync(ofi, buffer, offset, readLimit, ftmRead, + ofi^.ScratchSector, ofi^.ScratchSize); + + if tempCtx <> nil then + FAT32CloseFile(tempCtx); +end; + +function FAT32WriteFileAtOffset(volume : PStorage_Volume; directory : pchar; fileName : pchar; + offset : uint32; buffer : puint32; byteCount : uint32; ctx : pointer) : uint32; +var + ofi : PFATOpenFile; + tempCtx : pointer; + fileSize : uint32; + err : TError; +begin + FAT32WriteFileAtOffset := 0; + if buffer = nil then exit; + + ofi := PFATOpenFile(ctx); + tempCtx := nil; + if ofi = nil then begin + tempCtx := FAT32OpenFile(volume, directory, fileName, fileSize); + ofi := PFATOpenFile(tempCtx); + end; + if ofi = nil then exit; + + if byteCount = 0 then begin + FAT32EnsureDirEntry(ofi); + if tempCtx <> nil then + FAT32CloseFile(tempCtx); + exit; + end; + + err := FAT32EnsureCapacityForWrite(ofi, offset, byteCount); + if err = eNone then + FAT32WriteFileAtOffset := transferSync(ofi, buffer, offset, byteCount, ftmWrite, + ofi^.ScratchSector, ofi^.ScratchSize); + + if tempCtx <> nil then + FAT32CloseFile(tempCtx); +end; + +procedure FAT32ReadFileAtOffsetAsync(volume : PStorage_Volume; directory : pchar; fileName : pchar; + offset : uint32; buffer : puint32; byteCount : uint32; ctx : pointer; bytesRead : puint32; + callback : TIOCallback; callbackData : pointer); +var + ofi : PFATOpenFile; + asyncCtx : PFATTransferCtx; + readLimit : uint32; +begin + if bytesRead <> nil then bytesRead^ := 0; + ofi := PFATOpenFile(ctx); + if (ofi = nil) or (buffer = nil) or (byteCount = 0) then begin + if callback <> nil then callback(eInvalidArgument, callbackData); + exit; + end; + + readLimit := FAT32GetReadLimit(ofi, offset, byteCount); + if readLimit = 0 then begin + if callback <> nil then callback(eNone, callbackData); + exit; + end; + + asyncCtx := FAT32TransferAlloc(volume); + if asyncCtx = nil then begin + if callback <> nil then callback(eOutOfMemory, callbackData); + exit; + end; + + asyncCtx^.Mode := ftmRead; + asyncCtx^.Volume := volume; + asyncCtx^.OpenFile := ofi; + asyncCtx^.Buffer := buffer; + asyncCtx^.Offset := offset; + asyncCtx^.ByteCount := readLimit; + asyncCtx^.BytesOut := bytesRead; + asyncCtx^.Callback := callback; + asyncCtx^.CallbackData := callbackData; + asyncContinue(asyncCtx); +end; + +procedure FAT32WriteFileAtOffsetAsync(volume : PStorage_Volume; directory : pchar; fileName : pchar; + offset : uint32; buffer : puint32; byteCount : uint32; ctx : pointer; bytesWritten : puint32; + callback : TIOCallback; callbackData : pointer); +var + ofi : PFATOpenFile; + asyncCtx : PFATTransferCtx; + err : TError; +begin + if bytesWritten <> nil then bytesWritten^ := 0; + ofi := PFATOpenFile(ctx); + if (ofi = nil) or (buffer = nil) then begin + if callback <> nil then callback(eInvalidArgument, callbackData); + exit; + end; + + if byteCount = 0 then begin + err := FAT32EnsureDirEntry(ofi); + if callback <> nil then callback(err, callbackData); + exit; + end; + + err := FAT32EnsureCapacityForWrite(ofi, offset, byteCount); + if err <> eNone then begin + if callback <> nil then callback(err, callbackData); + exit; + end; + + asyncCtx := FAT32TransferAlloc(volume); + if asyncCtx = nil then begin + if callback <> nil then callback(eOutOfMemory, callbackData); + exit; + end; + + asyncCtx^.Mode := ftmWrite; + asyncCtx^.Volume := volume; + asyncCtx^.OpenFile := ofi; + asyncCtx^.Buffer := buffer; + asyncCtx^.Offset := offset; + asyncCtx^.ByteCount := byteCount; + asyncCtx^.BytesOut := bytesWritten; + asyncCtx^.Callback := callback; + asyncCtx^.CallbackData := callbackData; + asyncContinue(asyncCtx); +end; + +end. diff --git a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.types.pas b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.types.pas new file mode 100644 index 00000000..1681410a --- /dev/null +++ b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.types.pas @@ -0,0 +1,222 @@ +unit driver.storage.fs.fat32.types; + +interface + +uses + driver.storage.types; + +type + TBootRecord = bitpacked record + jmp2boot : ubit24; + OEMName : array[0..7] of char; + sectorSize : uint16; + spc : uint8; + rsvSectors : uint16; + numFats : uint8; + numDirEnt : uint16; + numSectors : uint16; + mediaDescp : uint8; + sectorsPerFat : uint16; + sectorsPerTrack : uint16; + heads : uint16; + hiddenSectors : uint32; + manySectors : uint32; + FATSize : uint32; + flags : uint16; + FATVersion : uint16; + rootCluster : uint32; + FSInfoCluster : uint16; + backupCluster : uint16; + reserved0 : array[0..11] of uint8; + driveNumber : uint8; + reserved1 : uint8; + bsignature : uint8; + volumeID : uint32; + volumeLabel : array[0..10] of uint8; + identString : array[0..7] of char; + end; + PBootRecord = ^TBootRecord; + + TFATExtArray = array[0..2] of char; + + TDirectory = packed record + fileName : array[0..7] of char; + fileExtension : TFATExtArray; + attributes : uint8; + reserved0 : uint8; + timeFine : uint8; + time : uint16; + date : uint16; + accessTime : uint16; + clusterHigh : uint16; + modifiedTime : uint16; + modifiedDate : uint16; + clusterLow : uint16; + byteSize : uint32; + end; + PDirectory = ^TDirectory; + + TFilesystemInfo = record + leadSignature : uint32; + reserved0 : array[0..479] of uint8; + structSignature : uint32; + freeSectors : uint32; + nextFreeSector : uint32; + reserved1 : array[0..11] of uint8; + trailSignature : uint32; + end; + +const + FAT_CACHE_SETS = 512; + FAT_CACHE_WAYS = 2; + FAT_CACHE_LINES = FAT_CACHE_SETS * FAT_CACHE_WAYS; + FAT_TRANSFER_POOL_CAPACITY = 64; + FAT_ALLOC_HINT_SLOTS = 8; + FAT_DIR_HINT_SLOTS = 16; + FAT_WRITE_PREALLOC_BYTES = 16 * 1024 * 1024; + FAT_WRITE_PREALLOC_MAX_CLUSTERS = 2048; + FAT_WRITE_PREALLOC_MIN_CLUSTERS = 64; + FAT_MAX_IO_SECTORS = 2048; + +type + TFATCacheLine = record + SectorIdx : uint32; + Dirty : boolean; + Valid : boolean; + LRU : uint8; + end; + + PFATCache = ^TFATCache; + TFATCache = record + Lines : array[0..FAT_CACHE_LINES - 1] of TFATCacheLine; + Data : puint32; + EvictBuf : puint32; + Busy : boolean; + FatStart : uint32; + Device : PStorage_Device; + end; + + PFATRun = ^TFATRun; + TFATRun = record + StartCluster : uint32; + ClusterCount : uint32; + end; + + TFATAllocHint = record + Valid : boolean; + StartCluster : uint32; + ClusterCount : uint32; + Stamp : uint32; + end; + + TFATDirHint = record + Valid : boolean; + ParentCluster : uint32; + SectorLBA : uint32; + EntryIdx : uint32; + Stamp : uint32; + end; + + TFATRunMap = record + Runs : PFATRun; + Count : uint32; + Capacity : uint32; + TotalClusters: uint32; + end; + + PFATVolumeInfo = ^TFATVolumeInfo; + TFATVolumeInfo = record + BootRecord : TBootRecord; + DataStart : uint32; + BytesPerCluster : uint32; + MaxCluster : uint32; + end; + + TFATDirEntryLocation = record + Valid : boolean; + ParentCluster : uint32; + SectorLBA : uint32; + EntryIdx : uint32; + end; + + TFATOpenCursor = record + Valid : boolean; + RunIdx : uint32; + FileClusterStart : uint32; + end; + + PFATOpenFile = ^TFATOpenFile; + TFATOpenFile = record + Volume : PStorage_Volume; + VolumeInfo : PFATVolumeInfo; + FirstCluster : uint32; + ByteSize : uint32; + AllocClusters : uint32; + RunMap : TFATRunMap; + Cursor : TFATOpenCursor; + ScratchSector : puint32; + ScratchSize : uint32; + CleanName : byteArray8; + ExtPart : pchar; + DirLoc : TFATDirEntryLocation; + Exists : boolean; + FatDirty : boolean; + MetaDirty : boolean; + end; + + TFATTransferMode = (ftmRead, ftmWrite); + TFATTransferPhase = ( + ftpIdle, + ftpAwaitDirect, + ftpAwaitScratchRead, + ftpAwaitScratchWrite, + ftpComplete, + ftpError + ); + + PFATVolumeCtx = ^TFATVolumeCtx; + PFATTransferCtx = ^TFATTransferCtx; + + TFATTransferCtx = record + Next : PFATTransferCtx; + VolumeCtx : PFATVolumeCtx; + Mode : TFATTransferMode; + Phase : TFATTransferPhase; + Volume : PStorage_Volume; + OpenFile : PFATOpenFile; + Buffer : puint32; + Offset : uint32; + ByteCount : uint32; + BytesDone : uint32; + BytesOut : puint32; + Callback : TIOCallback; + CallbackData : pointer; + PendingLBA : uint32; + PendingSectors : uint32; + PendingAdvanceBytes : uint32; + PendingScratchOffset : uint32; + PendingScratchBytes : uint32; + PendingBufferOffset : uint32; + ScratchSector : puint32; + ScratchSize : uint32; + RunHintValid : boolean; + RunHintIndex : uint32; + RunHintFileCluster : uint32; + LastError : TError; + end; + + TFATVolumeCtx = record + Cache : TFATCache; + Info : TFATVolumeInfo; + InfoValid : boolean; + TransferPoolBuf : pointer; + TransferScratchBuf: pointer; + TransferFreeList : pointer; + HintStamp : uint32; + AllocHints : array[0..FAT_ALLOC_HINT_SLOTS - 1] of TFATAllocHint; + DirHints : array[0..FAT_DIR_HINT_SLOTS - 1] of TFATDirHint; + end; + +implementation + +end. diff --git a/src/driver/storage/vol/driver.storage.vol.mgr.pas b/src/driver/storage/vol/driver.storage.vol.mgr.pas index 6e5c69da..673d3dae 100644 --- a/src/driver/storage/vol/driver.storage.vol.mgr.pas +++ b/src/driver/storage/vol/driver.storage.vol.mgr.pas @@ -81,10 +81,160 @@ procedure delete_volume(volume : PStorage_Volume); implementation uses - driver.storage.vfs; + driver.storage.vfs, + proc.mgr, + proc.types; + +type + PFormatVolumeAsyncCtx = ^TFormatVolumeAsyncCtx; + TFormatVolumeAsyncCtx = record + Volume : PStorage_Volume; + Filesystem : PFilesystem; + Config : puint32; + UserCallback : TIOCallback; + UserData : pointer; + end; { ===================== helpers ===================== } +function find_partition_slot_by_start(mbr : PMaster_Boot_Record; sectorStart : uint32) : sint32; +var + i : uint32; +begin + find_partition_slot_by_start := -1; + if mbr = nil then exit; + for i := 0 to 3 do begin + if mbr^.partition[i].LBA_start = sectorStart then begin + find_partition_slot_by_start := sint32(i); + exit; + end; + end; +end; + +procedure update_partition_system_id_sync(device : PStorage_Device; sectorStart : uint32; systemId : uint8); +var + srcMbr : PMaster_Boot_Record; + tempMbr : PMaster_Boot_Record; + slot : sint32; + freeSrcMbr : boolean; +begin + if device = nil then exit; + + srcMbr := driver.storage.mgr.get_cached_mbr(device); + freeSrcMbr := false; + if srcMbr = nil then begin + srcMbr := driver.storage.mgr.read_mbr(device); + freeSrcMbr := srcMbr <> nil; + end; + if srcMbr = nil then exit; + + slot := find_partition_slot_by_start(srcMbr, sectorStart); + if slot < 0 then begin + if freeSrcMbr then kfree(void(srcMbr)); + exit; + end; + + if srcMbr^.partition[slot].system_id = systemId then begin + if freeSrcMbr then kfree(void(srcMbr)); + exit; + end; + + tempMbr := PMaster_Boot_Record(kalloc(sizeof(TMaster_Boot_Record))); + if tempMbr = nil then begin + if freeSrcMbr then kfree(void(srcMbr)); + exit; + end; + memcpy(uint32(srcMbr), uint32(tempMbr), sizeof(TMaster_Boot_Record)); + tempMbr^.partition[slot].system_id := systemId; + driver.storage.mgr.write_mbr(device, tempMbr); + kfree(void(tempMbr)); + + if freeSrcMbr then kfree(void(srcMbr)); +end; + +procedure update_partition_system_id_async(device : PStorage_Device; sectorStart : uint32; systemId : uint8); +var + srcMbr : PMaster_Boot_Record; + tempMbr : PMaster_Boot_Record; + slot : sint32; + freeSrcMbr : boolean; +begin + if device = nil then exit; + + srcMbr := driver.storage.mgr.get_cached_mbr(device); + freeSrcMbr := false; + if srcMbr = nil then begin + srcMbr := driver.storage.mgr.read_mbr(device); + freeSrcMbr := srcMbr <> nil; + end; + if srcMbr = nil then exit; + + slot := find_partition_slot_by_start(srcMbr, sectorStart); + if slot < 0 then begin + if freeSrcMbr then kfree(void(srcMbr)); + exit; + end; + + if srcMbr^.partition[slot].system_id = systemId then begin + if freeSrcMbr then kfree(void(srcMbr)); + exit; + end; + + tempMbr := PMaster_Boot_Record(kalloc(sizeof(TMaster_Boot_Record))); + if tempMbr = nil then begin + if freeSrcMbr then kfree(void(srcMbr)); + exit; + end; + memcpy(uint32(srcMbr), uint32(tempMbr), sizeof(TMaster_Boot_Record)); + tempMbr^.partition[slot].system_id := systemId; + driver.storage.mgr.write_mbr_async(device, tempMbr, nil, nil); + kfree(void(tempMbr)); + + if freeSrcMbr then kfree(void(srcMbr)); +end; + +procedure format_volume_worker(pctx : proc.types.PProcessContext); +var + ctx : PFormatVolumeAsyncCtx; + err : TError; +begin + ctx := PFormatVolumeAsyncCtx(pctx^.Local); + if ctx = nil then begin + proc.mgr.proc_exit(1); + exit; + end; + + err := eNone; + if (ctx^.Volume = nil) or (ctx^.Filesystem = nil) then + err := eInvalidArgument + else if ctx^.Volume^.device = nil then + err := eDeviceNotFound + else if not ctx^.Volume^.device^.writable then + err := eReadOnly + else if ctx^.Filesystem^.createCallback = nil then + err := eInvalidArgument; + + if err = eNone then begin + update_partition_system_id_sync(ctx^.Volume^.device, ctx^.Volume^.sectorStart, ctx^.Filesystem^.system_id); + ctx^.Volume^.filesystem := ctx^.Filesystem; + ctx^.Filesystem^.createCallback(ctx^.Volume, ctx^.Volume^.sectorCount, ctx^.Volume^.sectorStart, ctx^.Config); + + { Re-probe the freshly formatted volume so the caller gets a verified + filesystem assignment and refreshed free-space metadata. } + ctx^.Volume^.filesystem := nil; + driver.storage.fs.mgr.probe_volume(ctx^.Volume); + if ctx^.Volume^.filesystem <> ctx^.Filesystem then + err := eUnsupportedFilesystem; + end; + + if ctx^.Config <> nil then + kfree(void(ctx^.Config)); + if ctx^.UserCallback <> nil then + ctx^.UserCallback(err, ctx^.UserData); + kfree(void(ctx)); + proc.mgr.proc_exit(0); +end; + { Create a TStorage_Volume from a partition entry and register it } procedure create_volume_from_partition(device : PStorage_Device; sectorStart : uint32; sectorCount : uint32); var @@ -103,6 +253,23 @@ begin register_volume(device, volume); end; +procedure remove_volume_from_device_list(device : PStorage_Device; volume : PStorage_Volume); +var + i : uint32; + v : PStorage_Volume; +begin + if (device = nil) or (device^.volumes = nil) or (volume = nil) then exit; + i := 0; + while i < DL_Size(device^.volumes) do begin + v := PStorage_Volume(Void(DL_Get(device^.volumes, i))^); + if v = volume then begin + DL_Delete(device^.volumes, i); + exit; + end; + i := i + 1; + end; +end; + { Remove from both global and device volume core.ds.lists, then free } procedure remove_volume_by_start(device : PStorage_Device; sectorStart : uint32); var @@ -113,33 +280,21 @@ begin found := nil; if device = nil then exit; - { Remove from device list first (before we free the volume) } - if device^.volumes <> nil then begin - i := 0; - while i < DL_Size(device^.volumes) do begin - v := PStorage_Volume(Void(DL_Get(device^.volumes, i))^); - if v^.sectorStart = sectorStart then begin - DL_Delete(device^.volumes, i); - break; - end else - i := i + 1; - end; - end; - - { Remove from global list and free } i := 0; while i < DL_Size(volumes) do begin v := PStorage_Volume(Void(DL_Get(volumes, i))^); if (v^.device = device) and (v^.sectorStart = sectorStart) then begin found := v; - DL_Delete(volumes, i); break; end else i := i + 1; end; if found <> nil then begin - driver.storage.vfs.InvalidateVolume(found); + if not driver.storage.vfs.InvalidateVolume(found) then + exit; + remove_volume_from_device_list(device, found); + DL_Delete(volumes, i); kfree(puint32(found)); end; end; @@ -154,15 +309,16 @@ begin while i < DL_Size(volumes) do begin v := PStorage_Volume(Void(DL_Get(volumes, i))^); if v^.device = device then begin + if not driver.storage.vfs.InvalidateVolume(v) then begin + i := i + 1; + continue; + end; DL_Delete(volumes, i); - driver.storage.vfs.InvalidateVolume(v); + remove_volume_from_device_list(device, v); kfree(puint32(v)); end else i := i + 1; end; - - if (device <> nil) and (device^.volumes <> nil) then - DL_Clear(device^.volumes); end; { ===================== Lifecycle ===================== } @@ -616,9 +772,12 @@ begin if fs = nil then exit; if fs^.createCallback = nil then exit; + update_partition_system_id_sync(device, vol^.sectorStart, fs^.system_id); vol^.filesystem := fs; fs^.createCallback(vol, vol^.sectorCount, vol^.sectorStart, config); - format_volume := true; + vol^.filesystem := nil; + driver.storage.fs.mgr.probe_volume(vol); + format_volume := vol^.filesystem = fs; end; function format_volume_async(device : PStorage_Device; volIndex : uint32; filesystemName : pchar; @@ -626,6 +785,8 @@ function format_volume_async(device : PStorage_Device; volIndex : uint32; filesy var vol : PStorage_Volume; fs : PFilesystem; + ctx : PFormatVolumeAsyncCtx; + cfgCopy : puint32; begin push_trace('driver.storage.vol.mgr.format_volume_async'); format_volume_async := false; @@ -636,10 +797,37 @@ begin fs := driver.storage.fs.mgr.find_filesystem_by_name(filesystemName); if fs = nil then exit; - if fs^.createAsyncCallback = nil then exit; + if fs^.createCallback = nil then exit; + + cfgCopy := nil; + if config <> nil then begin + cfgCopy := puint32(kalloc(sizeof(uint32))); + if cfgCopy = nil then begin + if callback <> nil then callback(eOutOfMemory, callbackData); + exit; + end; + cfgCopy^ := config^; + end; + + ctx := PFormatVolumeAsyncCtx(kalloc(sizeof(TFormatVolumeAsyncCtx))); + if ctx = nil then begin + if cfgCopy <> nil then kfree(void(cfgCopy)); + if callback <> nil then callback(eOutOfMemory, callbackData); + exit; + end; + ctx^.Volume := vol; + ctx^.Filesystem := fs; + ctx^.Config := cfgCopy; + ctx^.UserCallback := callback; + ctx^.UserData := callbackData; + + if proc.mgr.create('vol.fmt', @format_volume_worker, void(ctx), 1) = nil then begin + if cfgCopy <> nil then kfree(void(cfgCopy)); + kfree(void(ctx)); + if callback <> nil then callback(eOutOfMemory, callbackData); + exit; + end; - vol^.filesystem := fs; - fs^.createAsyncCallback(vol, vol^.sectorCount, vol^.sectorStart, config, callback, callbackData); format_volume_async := true; end; -- 2.54.0 From 663bfbb0e449b1439da8e8c55d53f3899e332980 Mon Sep 17 00:00:00 2001 From: Aaron Date: Mon, 16 Mar 2026 23:03:43 +0000 Subject: [PATCH 06/21] storage: lifecycle watch system, PFSFormatParams, vol busy/format flags, diskutil improvements - Add TStorageLifecycleWatch with watch_lifecycle/unwatch_lifecycle API (up to 32 watchers) - Add notify_lifecycle to broadcast mount/unmount/format events to registered callbacks - Replace raw puint32 config in format_volume with typed PFSFormatParams - Add volume_is_busy, volume_is_formatting, volume_set_mounted helpers - Add lifecycleFlags field to TStorage_Volume for tracking busy/formatting state - Expand app.diskutil with improved commands and volume state reporting - Minor improvements to core.util, arch.x86.util, proc.mgr, VFS, and flatfs - Remove obsolete driver.storage.fs.fat32.old.pas --- src/app/app.diskutil.pas | 214 +- src/arch/x86/arch.x86.util.pas | 14 + src/core/core.util.pas | 37 +- src/driver/storage/driver.storage.types.pas | 74 +- src/driver/storage/driver.storage.vfs.pas | 41 +- .../fs/driver.storage.fs.fat32.old.pas | 4650 ----------------- .../storage/fs/driver.storage.fs.fat32.pas | 1 + .../storage/fs/driver.storage.fs.flatfs.pas | 12 +- .../storage/vol/driver.storage.vol.mgr.pas | 247 +- src/proc/proc.mgr.pas | 51 + 10 files changed, 636 insertions(+), 4705 deletions(-) delete mode 100644 src/driver/storage/fs/driver.storage.fs.fat32.old.pas diff --git a/src/app/app.diskutil.pas b/src/app/app.diskutil.pas index 35abade3..4a96e032 100644 --- a/src/app/app.diskutil.pas +++ b/src/app/app.diskutil.pas @@ -59,6 +59,11 @@ var active_mbox : Plv_obj; { Widgets kept across dialogs } fmt_dropdown : Plv_obj; + fmt_param_area : Plv_obj; + fmt_cluster_input : Plv_obj; + fmt_filecount_input: Plv_obj; + fmt_label_input : Plv_obj; + fmt_param_error_lbl: Plv_obj; add_textarea : Plv_obj; add_err_lbl : Plv_obj; { Sidebar item tracking for highlight } @@ -67,6 +72,8 @@ var fmt_done_flag : uint32; fmt_done_error : TError; fmt_done_vol_idx : uint32; + storage_dirty_flag : uint32; + storage_watch_id : uint32; ui_session_id : uint32; type @@ -87,6 +94,8 @@ procedure showDeviceDetail(devIdx: uint32); forward; procedure showVolumeDetail(volIdx: uint32); forward; procedure clearDetail; forward; procedure fmt_poll_cb(tmr: Plv_timer); cdecl; forward; +procedure diskutil_storage_event(event : TStorageLifecycleEvent; device : PStorage_Device; volume : PStorage_Volume; error : TError; userdata : pointer); forward; +procedure rebuild_fmt_param_ui(fs : PFilesystem); forward; { ============================================================ Helpers — size formatting @@ -421,6 +430,73 @@ begin add_textarea := nil; add_err_lbl := nil; fmt_dropdown := nil; + fmt_param_area := nil; + fmt_cluster_input := nil; + fmt_filecount_input := nil; + fmt_param_error_lbl := nil; +end; + +procedure set_fmt_param_error(text : pchar); +begin + if fmt_param_area = nil then exit; + if fmt_param_error_lbl = nil then begin + fmt_param_error_lbl := lv_label_create(fmt_param_area); + lv_obj_set_style_text_color(fmt_param_error_lbl, lv_color_make(230, 80, 80), 0); + lv_obj_set_style_text_font(fmt_param_error_lbl, @lv_font_montserrat_14, 0); + end; + lv_label_set_text(fmt_param_error_lbl, text); +end; + +procedure rebuild_fmt_param_ui(fs : PFilesystem); +var + lbl : Plv_obj; +begin + if fmt_param_area = nil then exit; + lv_obj_clean(fmt_param_area); + fmt_cluster_input := nil; + fmt_filecount_input := nil; + fmt_param_error_lbl := nil; + + if fs = nil then exit; + if fs^.formatParamFlags = 0 then begin + addStatusLabel(fmt_param_area, 'No filesystem-specific format options.', 140, 150, 170); + exit; + end; + + if (fs^.formatParamFlags and FS_FORMAT_PARAM_CLUSTER_SIZE) <> 0 then begin + lbl := lv_label_create(fmt_param_area); + lv_label_set_text(lbl, 'Sectors per cluster (blank = auto, power of 2)'); + lv_obj_set_style_text_color(lbl, lv_color_make(180, 190, 210), 0); + lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0); + fmt_cluster_input := lv_textarea_create(fmt_param_area); + lv_textarea_set_one_line(fmt_cluster_input, true); + lv_textarea_set_placeholder_text(fmt_cluster_input, 'Auto'); + lv_obj_set_width(fmt_cluster_input, 220); + end; + + if (fs^.formatParamFlags and FS_FORMAT_PARAM_FILE_COUNT) <> 0 then begin + lbl := lv_label_create(fmt_param_area); + lv_label_set_text(lbl, 'Directory/file table entry count'); + lv_obj_set_style_text_color(lbl, lv_color_make(180, 190, 210), 0); + lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0); + fmt_filecount_input := lv_textarea_create(fmt_param_area); + lv_textarea_set_one_line(fmt_filecount_input, true); + lv_textarea_set_placeholder_text(fmt_filecount_input, '1000'); + lv_obj_set_width(fmt_filecount_input, 220); + end; +end; + +procedure fmt_fs_change_cb(e: Plv_event); cdecl; +var + code : uint32; + selIdx : uint32; + fs : PFilesystem; +begin + code := lv_event_get_code(e); + if code <> LV_EVENT_VALUE_CHANGED then exit; + selIdx := lv_dropdown_get_selected(fmt_dropdown); + fs := driver.storage.fs.mgr.get_filesystem(selIdx); + rebuild_fmt_param_ui(fs); end; { ============================================================ @@ -630,24 +706,51 @@ begin end; end; +procedure diskutil_storage_event(event : TStorageLifecycleEvent; device : PStorage_Device; volume : PStorage_Volume; error : TError; userdata : pointer); +begin + puint32(@storage_dirty_flag)^ := 1; +end; + procedure fmt_poll_cb(tmr: Plv_timer); cdecl; var mbox : Plv_obj; begin + if puint32(@storage_dirty_flag)^ <> 0 then begin + puint32(@storage_dirty_flag)^ := 0; + refreshSidebar; + if sel_mode = SEL_VOLUME then begin + if driver.storage.vol.mgr.get_volume(sel_vol_idx) <> nil then + showVolumeDetail(sel_vol_idx) + else + clearDetail; + end else if sel_mode = SEL_DEVICE then begin + if driver.storage.mgr.get_device(sel_dev_idx) <> nil then + showDeviceDetail(sel_dev_idx) + else + clearDetail; + end; + end; + if puint32(@fmt_done_flag)^ <> 1 then exit; puint32(@fmt_done_flag)^ := 0; + closeMsgBox; refreshSidebar; showVolumeDetail(fmt_done_vol_idx); if fmt_done_error <> eNone then begin - closeMsgBox; mbox := lv_msgbox_create(lv_layer_top); active_mbox := mbox; lv_msgbox_add_title(mbox, 'Format Failed'); lv_msgbox_add_text(mbox, 'The volume was formatted but could not be verified.'); lv_msgbox_add_text(mbox, formatErrorName(fmt_done_error)); lv_msgbox_add_footer_button(mbox, 'OK'); + end else begin + mbox := lv_msgbox_create(lv_layer_top); + active_mbox := mbox; + lv_msgbox_add_title(mbox, 'Format Complete'); + lv_msgbox_add_text(mbox, 'The volume is ready.'); + lv_msgbox_add_footer_button(mbox, 'OK'); end; end; @@ -658,17 +761,73 @@ var fs : PFilesystem; vol : PStorage_Volume; cbCtx : PFmtUICallbackCtx; + params : TFSFormatParams; + paramPtr : PFSFormatParams; + text : pchar; + value : uint32; begin code := lv_event_get_code(e); if code <> LV_EVENT_CLICKED then exit; vol := driver.storage.vol.mgr.get_volume(sel_vol_idx); if vol = nil then begin closeMsgBox; exit; end; + if driver.storage.vol.mgr.volume_is_busy(vol) then begin + closeMsgBox; + active_mbox := lv_msgbox_create(lv_layer_top); + lv_msgbox_add_title(active_mbox, 'Format Busy'); + lv_msgbox_add_text(active_mbox, 'This volume is busy. Wait for the current operation to finish.'); + lv_msgbox_add_footer_button(active_mbox, 'OK'); + exit; + end; selIdx := lv_dropdown_get_selected(fmt_dropdown); fs := driver.storage.fs.mgr.get_filesystem(selIdx); if fs = nil then begin closeMsgBox; exit; end; + memset(uint32(@params), 0, sizeof(TFSFormatParams)); + params.Version := FS_FORMAT_PARAMS_VERSION_1; + paramPtr := nil; + + if (fs^.formatParamFlags and FS_FORMAT_PARAM_CLUSTER_SIZE) <> 0 then begin + if fmt_cluster_input <> nil then begin + text := lv_textarea_get_text(fmt_cluster_input); + if (text <> nil) and (text^ <> #0) then begin + if not isNumericStr(text) then begin + set_fmt_param_error('Cluster size must be a number.'); + exit; + end; + value := stringToInt(text); + if (value = 0) or (value > 128) or ((value and (value - 1)) <> 0) then begin + set_fmt_param_error('Cluster size must be a power of two from 1 to 128.'); + exit; + end; + params.Flags := params.Flags or FS_FORMAT_PARAM_CLUSTER_SIZE; + params.ClusterSize := value; + paramPtr := @params; + end; + end; + end; + + if (fs^.formatParamFlags and FS_FORMAT_PARAM_FILE_COUNT) <> 0 then begin + if fmt_filecount_input <> nil then begin + text := lv_textarea_get_text(fmt_filecount_input); + if (text <> nil) and (text^ <> #0) then begin + if not isNumericStr(text) then begin + set_fmt_param_error('File count must be a number.'); + exit; + end; + value := stringToInt(text); + if (value = 0) or (value > 65535) then begin + set_fmt_param_error('File count must be from 1 to 65535.'); + exit; + end; + params.Flags := params.Flags or FS_FORMAT_PARAM_FILE_COUNT; + params.FileCount := value; + paramPtr := @params; + end; + end; + end; + closeMsgBox; cbCtx := PFmtUICallbackCtx(kalloc(sizeof(TFmtUICallbackCtx))); @@ -677,7 +836,7 @@ begin cbCtx^.VolumeIdx := sel_vol_idx; { Submit async format — returns immediately, fmt_done_cb only flips a flag. } - if not driver.storage.vol.mgr.format_volume_async(vol^.device, sel_vol_idx, fs^.sName, nil, + if not driver.storage.vol.mgr.format_volume_async(vol^.device, sel_vol_idx, fs^.sName, paramPtr, @fmt_done_cb, cbCtx) then begin kfree(void(cbCtx)); closeMsgBox; @@ -685,6 +844,11 @@ begin lv_msgbox_add_title(active_mbox, 'Format Failed'); lv_msgbox_add_text(active_mbox, 'Could not start the format operation.'); lv_msgbox_add_footer_button(active_mbox, 'OK'); + end else begin + active_mbox := lv_msgbox_create(lv_layer_top); + lv_msgbox_add_title(active_mbox, 'Formatting'); + lv_msgbox_add_text(active_mbox, 'Formatting is running in the background.'); + lv_msgbox_add_text(active_mbox, 'The volume will remount automatically when complete.'); end; end; @@ -813,6 +977,15 @@ begin vol := driver.storage.vol.mgr.get_volume(sel_vol_idx); if vol = nil then exit; + if driver.storage.vol.mgr.volume_is_busy(vol) then begin + mbox := lv_msgbox_create(lv_layer_top); + active_mbox := mbox; + lv_msgbox_add_title(mbox, 'Cannot Format'); + lv_msgbox_add_text(mbox, 'This volume is busy or already being formatted.'); + btn_no := lv_msgbox_add_footer_button(mbox, 'OK'); + lv_obj_add_event_cb(btn_no, @fmt_cancel_cb, LV_EVENT_CLICKED, nil); + exit; + end; { Check writable } if (vol^.device <> nil) and (not vol^.device^.writable) then begin @@ -864,6 +1037,18 @@ begin lv_obj_set_width(fmt_dropdown, 280); if opts <> nil then lv_dropdown_set_options(fmt_dropdown, opts); + lv_obj_add_event_cb(fmt_dropdown, @fmt_fs_change_cb, LV_EVENT_VALUE_CHANGED, nil); + + fmt_param_area := lv_obj_create(body); + lv_obj_remove_style_all(fmt_param_area); + lv_obj_set_width(fmt_param_area, 300); + lv_obj_set_style_layout(fmt_param_area, LV_LAYOUT_FLEX, 0); + lv_obj_set_flex_flow(fmt_param_area, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(fmt_param_area, 4, 0); + lv_obj_set_style_pad_all(fmt_param_area, 0, 0); + lv_obj_remove_flag(fmt_param_area, LV_OBJ_FLAG_SCROLLABLE); + fs := driver.storage.fs.mgr.get_filesystem(lv_dropdown_get_selected(fmt_dropdown)); + rebuild_fmt_param_ui(fs); btn_ok := lv_msgbox_add_footer_button(mbox, 'Format'); lv_obj_add_event_cb(btn_ok, @fmt_ok_cb, LV_EVENT_CLICKED, nil); @@ -1220,8 +1405,12 @@ begin lv_obj_set_flex_align(btn_row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); if (vol^.device <> nil) and vol^.device^.writable then begin - btn := makeButton(btn_row, 'Format', 100); - lv_obj_add_event_cb(btn, @btn_format_cb, LV_EVENT_CLICKED, nil); + if driver.storage.vol.mgr.volume_is_formatting(vol) then + addStatusLabel(detail, 'Format in progress. The volume will remount when complete.', 210, 180, 110) + else begin + btn := makeButton(btn_row, 'Format', 100); + lv_obj_add_event_cb(btn, @btn_format_cb, LV_EVENT_CLICKED, nil); + end; end else begin addStatusLabel(detail, 'Device is read-only. Format disabled.', 160, 150, 130); end; @@ -1398,10 +1587,15 @@ begin closeMsgBox; ui_session_id := ui_session_id + 1; puint32(@fmt_done_flag)^ := 0; + puint32(@storage_dirty_flag)^ := 0; if fmt_poll_timer <> nil then begin lv_timer_delete(fmt_poll_timer); fmt_poll_timer := nil; end; + if storage_watch_id <> 0 then begin + driver.storage.vol.mgr.unwatch_lifecycle(storage_watch_id); + storage_watch_id := 0; + end; if proc_pid <> 0 then begin proc.mgr.kill(proc_pid); proc_pid := 0; @@ -1506,13 +1700,19 @@ begin pending_del_slot := -1; active_mbox := nil; fmt_dropdown := nil; + fmt_param_area := nil; + fmt_cluster_input := nil; + fmt_filecount_input := nil; + fmt_param_error_lbl := nil; add_textarea := nil; add_err_lbl := nil; active_sidebar_btn := nil; fmt_done_flag := 0; fmt_done_error := eNone; fmt_done_vol_idx := 0; + storage_dirty_flag := 0; fmt_poll_timer := lv_timer_create(@fmt_poll_cb, 50, nil); + storage_watch_id := driver.storage.vol.mgr.watch_lifecycle(@diskutil_storage_event, nil); refreshSidebar; @@ -1542,6 +1742,10 @@ begin pending_del_slot := -1; active_mbox := nil; fmt_dropdown := nil; + fmt_param_area := nil; + fmt_cluster_input := nil; + fmt_filecount_input := nil; + fmt_param_error_lbl := nil; add_textarea := nil; add_err_lbl := nil; active_sidebar_btn := nil; @@ -1549,6 +1753,8 @@ begin fmt_done_flag := 0; fmt_done_error := eNone; fmt_done_vol_idx := 0; + storage_dirty_flag := 0; + storage_watch_id := 0; ui_session_id := 1; driver.video.desktop.registerProgram('Disk Utility', @launch); debug.tracer.pop_trace; diff --git a/src/arch/x86/arch.x86.util.pas b/src/arch/x86/arch.x86.util.pas index 01d3cea8..e42fa8bc 100644 --- a/src/arch/x86/arch.x86.util.pas +++ b/src/arch/x86/arch.x86.util.pas @@ -44,6 +44,7 @@ function inl(port : uint16) : uint32; procedure io_wait; procedure __SSE_128_memcpy(source : uint32; dest : uint32); +procedure __REP_MOVSB_memcpy(source : uint32; dest : uint32; size : uint32); procedure halt_and_catch_fire(); procedure halt_and_dont_catch_fire(); @@ -113,6 +114,19 @@ asm MOVAPS [EAX], XMM1 end; +procedure __REP_MOVSB_memcpy(source : uint32; dest : uint32; size : uint32); assembler; +asm + push esi + push edi + cld + mov esi, source + mov edi, dest + mov ecx, size + rep movsb + pop edi + pop esi +end; + function getESP : uint32; var tmp: uint32; diff --git a/src/core/core.util.pas b/src/core/core.util.pas index 1ece95ea..2b1b4e73 100644 --- a/src/core/core.util.pas +++ b/src/core/core.util.pas @@ -42,6 +42,26 @@ function HexCharToDecimal(hex : char) : uint8; function abs(x : sint32) : uint32; implementation +{$ifdef CPU386} +uses + arch.x86.util; + +const + MEMCPY_X86_SMALL_COPY_THRESHOLD = 16; +{$endif} + +procedure scalar_memcpy(source : uint32; dest : uint32; size : uint32); +var + src, dst : puint8; + i : uint32; +begin + if size = 0 then exit; + for i := 0 to size - 1 do begin + src := puint8(source + i); + dst := puint8(dest + i); + dst^ := src^; + end; +end; function abs(x : sint32) : uint32; var @@ -123,17 +143,16 @@ begin end; procedure memcpy(source : uint32; dest : uint32; size : uint32); -var - src, dst : puint8; - i : uint32; - begin if size = 0 then exit; - for i:=0 to size-1 do begin - src:= puint8(source + i); - dst:= puint8(dest + i); - dst^:= src^; - end; +{$ifdef CPU386} + if size < MEMCPY_X86_SMALL_COPY_THRESHOLD then + scalar_memcpy(source, dest, size) + else + arch.x86.util.__REP_MOVSB_memcpy(source, dest, size); +{$else} + scalar_memcpy(source, dest, size); +{$endif} end; function getWord(i : uint32; hi : boolean) : uint16; diff --git a/src/driver/storage/driver.storage.types.pas b/src/driver/storage/driver.storage.types.pas index b5e0c0bc..0a71a094 100644 --- a/src/driver/storage/driver.storage.types.pas +++ b/src/driver/storage/driver.storage.types.pas @@ -53,6 +53,22 @@ type { Directory entry core.types } TDirectory_Entry_Type = (directoryEntry, fileEntry, mountEntry); + { Generic directory entry } + TDirectory_Entry = record + fileName : pchar; + entryType : TDirectory_Entry_Type; + fileSize : uint32; + modifiedDate : uint16; { FAT-style: bits 15-9=year-1980, 8-5=month, 4-0=day } + modifiedTime : uint16; { FAT-style: bits 15-11=hour, 10-5=min, 4-0=sec/2 } + attributes : uint8; { FS-specific attribute bits } + end; + + TDrive_Error = record + code : uint16; + description : pchar; + recoverable : boolean; + end; + { Storage error codes — shared across I/O layer, drivers, FS, and VFS } TError = ( { Success } @@ -126,6 +142,34 @@ type PDirectory_Entry = ^TDirectory_Entry; PDrive_Error = ^TDrive_Error; PIORequest = ^TIORequest; + PFSFormatParams = ^TFSFormatParams; + + TStorageLifecycleEvent = ( + sleVolumeAdded, + sleVolumeMounted, + sleVolumeUnmounted, + sleVolumeFormatStarted, + sleVolumeFormatCompleted, + sleVolumeInvalidating, + sleVolumeInvalidated, + sleVolumeRemoved, + sleDeviceAdded, + sleDeviceRemoved + ); + + TStorageLifecycleCallback = procedure(event : TStorageLifecycleEvent; + device : PStorage_Device; + volume : PStorage_Volume; + error : TError; + userdata : pointer); + + TFSFormatParams = record + Version : uint16; + Flags : uint16; + ClusterSize : uint32; + FileCount : uint32; + LabelText : array[0..11] of char; + end; { === Driver dispatch type (Phase 2+) === } @@ -142,8 +186,8 @@ type TIOCallback = procedure(error : TError; userdata : pointer); { Filesystem callback core.types } - PPCreateHook = procedure(volume : PStorage_volume; start : uint32; size : uint32; config : puint32); - PPCreateAsyncHook = procedure(volume : PStorage_volume; start : uint32; size : uint32; config : puint32; callback : TIOCallback; callbackData : pointer); + PPCreateHook = procedure(volume : PStorage_volume; start : uint32; size : uint32; config : PFSFormatParams); + PPCreateAsyncHook = procedure(volume : PStorage_volume; start : uint32; size : uint32; config : PFSFormatParams; callback : TIOCallback; callbackData : pointer); PPDetectHook = procedure(disk : PStorage_Device); PPCreateDirHook = procedure(volume : PStorage_volume; directory : pchar; dirname : pchar; attributes : uint32; status : puint32); PPReadDirHook = function(volume : PStorage_volume; directory : pchar; status : puint32) : PLinkedListBase; @@ -259,6 +303,7 @@ type { Per-file open/close — nil if FS does not cache per-file metadata } openFileCallback : PPOpenFileHook; closeFileCallback : PPCloseFileHook; + formatParamFlags : uint32; end; { Generic storage volume } @@ -271,24 +316,19 @@ type freeSectors : uint32; filesystem : PFilesystem; isBootDrive : boolean; + lifecycleFlags : uint32; fsPrivate : pointer; { FS-driver private data (e.g. PFATCache for FAT32) } end; - { Generic directory entry } - TDirectory_Entry = record - fileName : pchar; - entryType : TDirectory_Entry_Type; - fileSize : uint32; - modifiedDate : uint16; { FAT-style: bits 15-9=year-1980, 8-5=month, 4-0=day } - modifiedTime : uint16; { FAT-style: bits 15-11=hour, 10-5=min, 4-0=sec/2 } - attributes : uint8; { FS-specific attribute bits } - end; - - TDrive_Error = record - code : uint16; - description : pchar; - recoverable : boolean; - end; +const + FS_FORMAT_PARAM_CLUSTER_SIZE = 1; + FS_FORMAT_PARAM_FILE_COUNT = 2; + FS_FORMAT_PARAM_LABEL = 4; + FS_FORMAT_PARAMS_VERSION_1 = 1; + STORAGE_VOLUME_FLAG_MOUNTED = 1; + STORAGE_VOLUME_FLAG_FORMATTING = 2; + STORAGE_VOLUME_FLAG_INVALIDATING = 4; + STORAGE_VOLUME_FLAG_REMOVED = 8; implementation diff --git a/src/driver/storage/driver.storage.vfs.pas b/src/driver/storage/driver.storage.vfs.pas index 22c505b4..bf676d97 100644 --- a/src/driver/storage/driver.storage.vfs.pas +++ b/src/driver/storage/driver.storage.vfs.pas @@ -558,6 +558,7 @@ begin debug.tracer.push_trace('driver.storage.vfs.InvalidateVolume.busy'); exit; end; + driver.storage.vol.mgr.volume_set_mounted(vol, false); { 1. Evict all directory cache entries for this volume } DirCache_Invalidate(vol, nil); @@ -1163,6 +1164,7 @@ begin core.ds.hashmap.add(ht, stringCopy(mountName), void(mountObj)); mountVolume := pvRegistered; + driver.storage.vol.mgr.volume_set_mounted(volume, true); kfree(void(parentPath)); STRLL_Free(splitPath); @@ -2434,6 +2436,7 @@ end; procedure DeleteFileAsync(Path : pchar; Error : PError; Callback : TIOCallback; CallbackData : pointer); var ctx : PVFSPathAsyncCtx; + worker : proc.types.PProcessContext; begin debug.tracer.push_trace('driver.storage.vfs.DeleteFileAsync.enter'); if Error <> nil then Error^ := eUnknown; @@ -2462,7 +2465,15 @@ begin ctx^.IsDirectory := false; ctx^.UserCallback := Callback; ctx^.UserData := CallbackData; - proc.mgr.create('vfs.delf', @vfs_pathop_worker, void(ctx), 1); + worker := proc.mgr.create('vfs.delf', @vfs_pathop_worker, void(ctx), 1); + if worker = nil then begin + if ctx^.Path <> nil then + kfree(void(ctx^.Path)); + kfree(void(ctx)); + if Error <> nil then Error^ := eOutOfMemory; + if Callback <> nil then Callback(eOutOfMemory, CallbackData); + exit; + end; debug.tracer.push_trace('driver.storage.vfs.DeleteFileAsync.exit'); end; @@ -2470,6 +2481,7 @@ end; procedure DeleteDirectoryAsync(Path : pchar; Error : PError; Callback : TIOCallback; CallbackData : pointer); var ctx : PVFSPathAsyncCtx; + worker : proc.types.PProcessContext; begin debug.tracer.push_trace('driver.storage.vfs.DeleteDirectoryAsync.enter'); if Error <> nil then Error^ := eUnknown; @@ -2498,7 +2510,15 @@ begin ctx^.IsDirectory := true; ctx^.UserCallback := Callback; ctx^.UserData := CallbackData; - proc.mgr.create('vfs.deld', @vfs_pathop_worker, void(ctx), 1); + worker := proc.mgr.create('vfs.deld', @vfs_pathop_worker, void(ctx), 1); + if worker = nil then begin + if ctx^.Path <> nil then + kfree(void(ctx^.Path)); + kfree(void(ctx)); + if Error <> nil then Error^ := eOutOfMemory; + if Callback <> nil then Callback(eOutOfMemory, CallbackData); + exit; + end; debug.tracer.push_trace('driver.storage.vfs.DeleteDirectoryAsync.exit'); end; @@ -2561,6 +2581,7 @@ procedure GetDirectoryListingAsync(Path : pchar; ResultMap : PPHashMap; Callback var ctx : PVFSDirAsyncCtx; absPath : pchar; + worker : proc.types.PProcessContext; begin debug.tracer.push_trace('driver.storage.vfs.GetDirectoryListingAsync.enter'); if ResultMap <> nil then ResultMap^ := nil; @@ -2573,12 +2594,26 @@ begin absPath := MakeAbsolutePath(Path); ctx := PVFSDirAsyncCtx(kalloc(sizeof(TVFSDirAsyncCtx))); + if ctx = nil then begin + if absPath <> nil then + kfree(void(absPath)); + if Callback <> nil then Callback(eOutOfMemory, CallbackData); + exit; + end; + memset(uint32(ctx), 0, sizeof(TVFSDirAsyncCtx)); ctx^.Path := absPath; ctx^.ResultMap := ResultMap; ctx^.UserCallback := Callback; ctx^.UserData := CallbackData; - proc.mgr.create('vfs.dirls', @vfs_dirlist_worker, void(ctx), 1); + worker := proc.mgr.create('vfs.dirls', @vfs_dirlist_worker, void(ctx), 1); + if worker = nil then begin + if ctx^.Path <> nil then + kfree(void(ctx^.Path)); + kfree(void(ctx)); + if Callback <> nil then Callback(eOutOfMemory, CallbackData); + exit; + end; debug.tracer.push_trace('driver.storage.vfs.GetDirectoryListingAsync.exit'); end; diff --git a/src/driver/storage/fs/driver.storage.fs.fat32.old.pas b/src/driver/storage/fs/driver.storage.fs.fat32.old.pas deleted file mode 100644 index 4698394f..00000000 --- a/src/driver/storage/fs/driver.storage.fs.fat32.old.pas +++ /dev/null @@ -1,4650 +0,0 @@ -// Copyright 2021 Kieron Morris -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -{ - Driver->storage->driver.storage.fs.fat32 - driver.storage.fs.fat32 filesystem driver - - @author(Aaron Hance ) -} - -unit driver.storage.fs.fat32.old; - -interface - -uses - boot.mgr, - driver.storage.fs.mgr, - core.ds.lists, - memory.heap, - proc.mgr, - proc.types, - driver.timer.rtc, - io.stdio, - driver.storage.mgr, - driver.storage.types, - core.strings, - io.syslog, - debug.tracer, - core.util, arch.x86.util, - driver.storage.vol.mgr; - -procedure FAT32LegacyReleaseVolumeCtx(volume : PStorage_Volume); - -type - - TBootRecord = bitpacked record - jmp2boot : ubit24; - OEMName : array[0..7] of char; - sectorSize : uint16; - spc : uint8; - rsvSectors : uint16; - numFats : uint8; - numDirEnt : uint16; - numSectors : uint16; - mediaDescp : uint8; - sectorsPerFat : uint16; - sectorsPerTrack : uint16; - heads : uint16; - hiddenSectors : uint32; - manySectors : uint32; - FATSize : uint32; - flags : uint16; - FATVersion : uint16; - rootCluster : uint32; - FSInfoCluster : uint16; - backupCluster : uint16; - reserved0 : array[0..11] of uint8; - driveNumber : uint8; - reserved1 : uint8; - bsignature : uint8;// = $28; - volumeID : uint32; - volumeLabel : array[0..10] of uint8; - identString : array[0..7] of char;// = 'FAT32 '; - end; - PBootRecord = ^TBootRecord; - - TFATExtArray = array[0..2] of char; - - TDirectory = packed record - fileName : array[0..7] of char; - fileExtension : TFATExtArray; - attributes : uint8; - reserved0 : uint8; - timeFine : uint8; - time : uint16; - date : uint16; - accessTime : uint16; - clusterHigh : uint16; - modifiedTime : uint16; - modifiedDate : uint16; - clusterLow : uint16; - byteSize : uint32; - end; - PDirectory = ^TDirectory; - - TFilesystemInfo = record - leadSignature : uint32; - reserved0 : array[0..479] of uint8; - structSignature : uint32; - freeSectors : uint32; - nextFreeSector : uint32; - reserved1 : array[0..11] of uint8; - trailSignature : uint32; - end; - - TFatVolumeInfo = record - sectorsPerCluster : uint8; // must be power of 2 and mult by sectorsize to max 32k - end; - PFatVolumeInfo = ^TFatVolumeInfo; - - TClusterExtent = record - StartCluster : uint32; - ClusterCount : uint32; - end; - PClusterExtent = ^TClusterExtent; - -const - { FAT sector cache — 2-way set-associative, 256 sets, LRU replacement. - This keeps small-cluster sequential files from churning the FAT cache - once they grow beyond a few megabytes. } - FAT_CACHE_SETS = 256; - FAT_CACHE_WAYS = 2; - FAT_CACHE_LINES = 512; { SETS * WAYS } - FAT_TRANSFER_POOL_CAPACITY = 64; - FAT_WORK_QUEUE_CAPACITY = FAT_TRANSFER_POOL_CAPACITY; - FAT_WRITE_PREALLOC_BYTES = 8 * 1024 * 1024; - FAT_WRITE_PREALLOC_MAX_CLUSTERS = 2048; - FAT_WRITE_PREALLOC_MIN_CLUSTERS = 64; - -type - TFATCacheLine = record - SectorIdx : uint32; { which FAT sector is stored here } - Dirty : boolean; { true if written but not flushed to disk } - Valid : boolean; { true if line contains valid data } - LRU : uint8; { 0 = MRU, 1 = LRU, 2 = LRU (for 3-way), 3 = LRU (for 4-way) } - end; - - PFATCache = ^TFATCache; - TFATCache = record - Lines : array[0..FAT_CACHE_LINES - 1] of TFATCacheLine; - Data : puint32; { kalloc'd FAT_CACHE_LINES * 512 bytes of FAT sector data } - EvictBuf : puint32; { kalloc'd 512-byte scratch for eviction writes } - Busy : boolean; { spinlock: serializes miss-path eviction+load } - FatStart : uint32; { LBA of first FAT sector on disk } - Device : PStorage_Device; { device pointer for I/O } - BootRecord : PBootRecord; { cached boot record (read once) } - end; - - TFATAsyncMode = (famRead, famWrite); - - TFATTransferState = ( - ftsIdle, - ftsPrepare, - ftsIssueIO, - ftsAwaitIO, - ftsAdvance, - ftsComplete, - ftsError - ); - - { Per-file cached metadata created by fat32OpenFile and stored in the - file descriptor's FSPrivate pointer. Avoids re-reading boot record, - directory listing, and searching for the file on every read/write. } - PFATOpenFile = ^TFATOpenFile; - PFATVolumeCtx = ^TFATVolumeCtx; - PFATFileIOAsyncCtx = ^TFATFileIOAsyncCtx; - - TFATOpenFile = record - Volume : PStorage_Volume; - BootRecord : PBootRecord; { borrows from TFATCache — do NOT kfree } - DataStart : uint32; { fatDataStartLBA result } - FirstCluster : uint32; { file's start cluster (0 if new/empty) } - ByteSize : uint32; { current file size in bytes } - AllocClusters: uint32; { current reserved cluster count for appends } - Extents : PLinkedListBase;{ cached cluster extents for fast sequential I/O } - DirCluster : uint32; { parent directory's start cluster } - CleanName : byteArray8; { cleaned 8.3 filename } - ExtPart : pchar; { heap-allocated extension string — owned } - ScratchSector: puint32; { per-open-file sector scratch buffer } - ScratchSize : uint32; { size of ScratchSector in bytes } - Exists : boolean; { true if file existed at open time } - FatDirty : boolean; { true if FAT cache must be flushed on close } - MetaDirty : boolean; { true if dir entry size/cluster needs sync on close } - end; - - TFATVolumeCtx = record - Cache : TFATCache; - TransferPoolBuf : pointer; - TransferFreeList : pointer; - Worker : proc.types.PProcessContext; - WorkHead : uint32; - WorkTail : uint32; - WorkCount : uint32; - WorkQueue : array[0..FAT_WORK_QUEUE_CAPACITY - 1] of PFATFileIOAsyncCtx; - end; - - TFATFileIOAsyncCtx = record - Next : PFATFileIOAsyncCtx; - VolumeCtx : PFATVolumeCtx; - State : TFATTransferState; - Mode : TFATAsyncMode; - Volume : PStorage_Volume; - OpenFile : PFATOpenFile; - Buffer : puint32; - Offset : uint32; - ByteCount : uint32; - BytesDone : uint32; - BytesOut : puint32; - Callback : TIOCallback; - CallbackData : pointer; - Cluster : uint32; - OrigByteSize : uint32; - DataStart : uint32; - BootRecord : PBootRecord; - ChunkBytes : uint32; - LastError : TError; - CursorValid : boolean; - CursorIndex : uint32; - CursorCluster: uint32; - ExtentValid : boolean; - ExtentIdx : uint32; - ExtentStartIdx : uint32; - end; - - { Async format state machine steps } - TFmtStep = ( - fmtBootSector, - fmtZeroFAT, - fmtFATEntries, - fmtRootDir, - fmtSystemDir, - fmtDone - ); - - PFmtContext = ^TFmtContext; - TFmtContext = record - Step : TFmtStep; - Volume : PStorage_Volume; - Disk : PStorage_Device; - SectorStart : uint32; - FATStart : uint32; - DataStart : uint32; - FATSize : uint32; - BatchPos : uint32; - BatchSize : uint32; - RootCluster : uint32; - SPC : uint32; - Buffer : puint32; - ZeroBuffer : puint32; - Callback : TIOCallback; - CallbackData : pointer; - end; - -var - filesystem : TFilesystem; - -procedure init; -procedure create_volume(volume : PStorage_Volume; sectors : uint32; start : uint32; config : puint32); -procedure create_volume_async(volume : PStorage_Volume; sectors : uint32; start : uint32; config : puint32; callback : TIOCallback; callbackData : pointer); -procedure detect_volumes(disk : PStorage_Device); -function identify_volume(volume : PStorage_Volume) : boolean; -function readDirectoryGen(volume : PStorage_volume; directory : pchar; status : puint32) : PLinkedListBase; -procedure writeDirectoryGen(volume : PStorage_volume; directory : pchar; dirName : pchar; attributes : uint32; statusOut : puint32); -procedure deleteFile(volume : PStorage_Volume; filePath : pchar; statusOut : puint32); -procedure deleteDir(volume : PStorage_Volume; path : pchar; statusOut : puint32); -procedure renameFile(volume : PStorage_Volume; filePath : pchar; newName : pchar; statusOut : puint32); -//function writeDirectory(volume : PStorage_volume; directory : pchar; attributes : uint32) : uint8; // need to handle parent table cluster overflow, need to take attributes -//function readDirectory(volume : PStorage_volume; directory : pchar; listPtr : PLinkedListBase) : uint8; //returns: 0 = success, 1 = dir not exsist, 2 = not directory, 3 = error - - -implementation - -{ ---- Module-level I/O caches ---- } -var - - { Sequential I/O chain-position cache: stores the last visited - (cluster-index, cluster-number) pair so the next call can resume - instead of walking the entire chain from cluster 0. } - sioc_vol : PStorage_Volume = nil; - sioc_fileClust : uint32 = 0; - sioc_chainIdx : uint32 = 0; - sioc_chainClust : uint32 = 0; - sioc_valid : boolean = false; - - { Free-cluster allocation hint: next findFreeClusters scan starts here - instead of cluster 2, making sequential allocations O(1). } - sioc_allocHint : uint32 = 2; - sioc_allocVol : PStorage_Volume = nil; - -{ Validate a FAT32 8.3 filename. - fullName should be in "NAME.EXT" form. - Returns true if valid: name part 1-8 chars, extension 0-3 chars, - no illegal characters. } -function isValidFAT32Name(fullName : pchar) : boolean; -var - i, fnLen, dotPos, nameLen, extLen : uint32; - c : char; -begin - isValidFAT32Name := false; - if fullName = nil then exit; - fnLen := stringSize(fullName); - if fnLen = 0 then exit; - - { Find last dot position } - dotPos := fnLen; - for i := 0 to fnLen - 1 do begin - if fullName[i] = '.' then - dotPos := i; - end; - - nameLen := dotPos; - if dotPos < fnLen then - extLen := fnLen - dotPos - 1 - else - extLen := 0; - - { Name part must be 1-8 characters } - if (nameLen < 1) or (nameLen > 8) then exit; - { Extension part must be 0-3 characters } - if extLen > 3 then exit; - - { Check all characters for validity } - for i := 0 to fnLen - 1 do begin - c := fullName[i]; - if i = dotPos then continue; { skip the dot separator } - { Reject control characters (< 0x20) except we don't need to check 0 since stringSize stops there } - if uint8(c) < $20 then exit; - { Reject illegal FAT32 characters: " * / : < > ? \ | } - case c of - '"', '*', '/', ':', '<', '>', '?', '\', '|': exit; - end; - end; - - isValidFAT32Name := true; -end; - -function load(ptr : void) : boolean; -begin - io.syslog.logln('FAT32', 'LOADED.') -end; - -function matchExtension(dirExt : TFATExtArray; ext : pchar) : boolean; -var - j : uint32; - c : char; - extDone : boolean; -begin - matchExtension:= true; - extDone := false; - for j:=0 to 2 do begin - if (not extDone) and (ext <> nil) and (ext[j] <> char(0)) then begin - c:= ext[j]; - { Uppercase a-z to A-Z for case-insensitive FAT32 matching } - if (c >= 'a') and (c <= 'z') then - c := char(uint8(c) - 32); - end else begin - c:= ' '; - extDone := true; - end; - if dirExt[j] <> c then begin - matchExtension:= false; - break; - end; - end; -end; - -function cleanString(str : pchar; status : puint32) : byteArray8; -var - i : uint32; - ii: uint32; -begin - push_trace('cleanstring()'); - if str = nil then begin - for i:=0 to 7 do cleanString[i]:= ' '; - exit; - end; - for i:=0 to 7 do begin - if str[i] = char(0) then begin - for ii:=i to 7 do begin - cleanString[ii]:= ' '; - end; - break; - end else begin - { Uppercase a-z to A-Z for FAT32 } - if (str[i] >= 'a') and (str[i] <= 'z') then - cleanString[i]:= char(uint8(str[i]) - 32) - else - cleanString[i]:= str[i]; - end; - end; - -end; - -{ Split a full filename like 'README.TXT' into name and extension parts. - Caller must free nameOut and extOut. } -procedure splitFileNameParts(fullName : pchar; var nameOut : pchar; var extOut : pchar); -var - fnLen, dotPos, i : uint32; -begin - push_trace('driver.storage.fs.fat32.splitFileNameParts'); - nameOut := nil; - extOut := nil; - if fullName = nil then begin - nameOut := stringNew(0); - extOut := stringNew(0); - exit; - end; - fnLen := stringSize(fullName); - dotPos := fnLen; - for i := 0 to fnLen - 1 do begin - if fullName[i] = '.' then - dotPos := i; - end; - nameOut := stringTrim(fullName, dotPos); - if dotPos < fnLen then - extOut := stringCopy(pchar(@fullName[dotPos + 1])) - else - extOut := stringNew(0); -end; - -function cleanStringCha(str : pchar) : pchar; -var - i : uint32; - ii: uint32; -begin - cleanStringCha:= pchar(kalloc(10)); - memset(uint32(cleanstringcha), 0, 10); - push_trace('cleanstringcha'); - if str = nil then begin - for i:=0 to 7 do cleanStringCha[i]:= ' '; - exit; - end; - for i:=0 to 7 do begin - if str[i] = char(0) then begin - for ii:=i to 7 do begin - cleanStringCha[ii]:= ' '; - end; - break; - end else begin - push_trace('cleanstringcha1.2'); - cleanstringcha[i]:= str[i]; - - end; - end; - -end; - -function readBootRecord(volume : PStorage_volume) : PBootRecord; -var - buffer : puint32; - bufSize : uint32; -begin - bufSize := volume^.device^.sectorSize; - if bufSize < 512 then bufSize := 512; - buffer:= puint32(kalloc(bufSize)); - memset(uint32(buffer), 0, bufSize); - driver.storage.mgr.storage_read(volume^.device, volume^.sectorStart, 1, buffer); - readBootRecord:= PBootRecord(buffer); -end; - -function bootSectorHasSignature(buffer : puint32) : boolean; -begin - bootSectorHasSignature := (puint8(uint32(buffer) + 510)^ = $55) and - (puint8(uint32(buffer) + 511)^ = $AA); -end; - -{ ---- FAT sector cache ---- } - -{ Get or lazily create the per-volume FAT32 context. - Stores the context pointer in volume^.fsPrivate. } -function fatVolumeCtxGet(volume : PStorage_Volume) : PFATVolumeCtx; -var - ctx : PFATVolumeCtx; - br : PBootRecord; - i : uint32; - base : uint32; - tx : PFATFileIOAsyncCtx; -begin - fatVolumeCtxGet := nil; - if volume = nil then exit; - - if volume^.fsPrivate <> nil then begin - fatVolumeCtxGet := PFATVolumeCtx(volume^.fsPrivate); - exit; - end; - - ctx := PFATVolumeCtx(kalloc(sizeof(TFATVolumeCtx))); - if ctx = nil then exit; - memset(uint32(ctx), 0, sizeof(TFATVolumeCtx)); - - ctx^.Cache.Data := puint32(kalloc(FAT_CACHE_LINES * 512)); - if ctx^.Cache.Data = nil then begin - kfree(puint32(ctx)); - exit; - end; - memset(uint32(ctx^.Cache.Data), 0, FAT_CACHE_LINES * 512); - - ctx^.Cache.EvictBuf := puint32(kalloc(512)); - if ctx^.Cache.EvictBuf = nil then begin - kfree(ctx^.Cache.Data); - kfree(puint32(ctx)); - exit; - end; - memset(uint32(ctx^.Cache.EvictBuf), 0, 512); - - ctx^.Cache.Busy := false; - for i := 0 to FAT_CACHE_LINES - 1 do begin - ctx^.Cache.Lines[i].Valid := false; - ctx^.Cache.Lines[i].Dirty := false; - ctx^.Cache.Lines[i].LRU := uint8(i mod FAT_CACHE_WAYS); - end; - - br := readBootRecord(volume); - ctx^.Cache.BootRecord := br; - ctx^.Cache.FatStart := volume^.sectorStart + br^.rsvSectors; - ctx^.Cache.Device := volume^.device; - - ctx^.TransferPoolBuf := kalloc(FAT_TRANSFER_POOL_CAPACITY * sizeof(TFATFileIOAsyncCtx)); - if ctx^.TransferPoolBuf <> nil then begin - base := uint32(ctx^.TransferPoolBuf); - if FAT_TRANSFER_POOL_CAPACITY > 0 then - for i := 0 to FAT_TRANSFER_POOL_CAPACITY - 1 do begin - tx := PFATFileIOAsyncCtx(base + (i * sizeof(TFATFileIOAsyncCtx))); - tx^.Next := PFATFileIOAsyncCtx(ctx^.TransferFreeList); - tx^.VolumeCtx := ctx; - tx^.State := ftsIdle; - ctx^.TransferFreeList := pointer(tx); - end; - end; - - volume^.fsPrivate := pointer(ctx); - fatVolumeCtxGet := ctx; -end; - -procedure fatCacheFlush(cache : PFATCache; volume : PStorage_Volume); forward; - -procedure FAT32LegacyReleaseVolumeCtx(volume : PStorage_Volume); -var - ctx : PFATVolumeCtx; -begin - if volume = nil then exit; - ctx := PFATVolumeCtx(volume^.fsPrivate); - if ctx = nil then exit; - - volume^.fsPrivate := nil; - fatCacheFlush(@ctx^.Cache, volume); - - if ctx^.Worker <> nil then begin - proc.mgr.terminate(ctx^.Worker^.ProcessID); - exit; - end; - - if ctx^.Cache.BootRecord <> nil then - kfree(puint32(ctx^.Cache.BootRecord)); - if ctx^.Cache.Data <> nil then - kfree(ctx^.Cache.Data); - if ctx^.Cache.EvictBuf <> nil then - kfree(ctx^.Cache.EvictBuf); - if ctx^.TransferPoolBuf <> nil then - kfree(void(ctx^.TransferPoolBuf)); - kfree(puint32(ctx)); -end; - -{ Get or lazily create the per-volume FAT cache. } -function fatCacheGet(volume : PStorage_Volume) : PFATCache; -var - ctx : PFATVolumeCtx; -begin - ctx := fatVolumeCtxGet(volume); - if ctx = nil then - fatCacheGet := nil - else - fatCacheGet := @ctx^.Cache; -end; - -function fatTransferAlloc(volume : PStorage_Volume) : PFATFileIOAsyncCtx; -var - ctx : PFATVolumeCtx; - transfer : PFATFileIOAsyncCtx; -begin - fatTransferAlloc := nil; - ctx := fatVolumeCtxGet(volume); - if ctx = nil then exit; - - asm pushf; cli end; - transfer := PFATFileIOAsyncCtx(ctx^.TransferFreeList); - if transfer <> nil then - ctx^.TransferFreeList := pointer(transfer^.Next); - asm popf end; - - if transfer = nil then exit; - - memset(uint32(transfer), 0, sizeof(TFATFileIOAsyncCtx)); - transfer^.VolumeCtx := ctx; - transfer^.State := ftsIdle; - fatTransferAlloc := transfer; -end; - -procedure fatTransferFree(ctx : PFATFileIOAsyncCtx); -var - volCtx : PFATVolumeCtx; -begin - if ctx = nil then exit; - volCtx := ctx^.VolumeCtx; - if volCtx = nil then exit; - - asm pushf; cli end; - ctx^.Next := PFATFileIOAsyncCtx(volCtx^.TransferFreeList); - volCtx^.TransferFreeList := pointer(ctx); - asm popf end; -end; - -function fatWorkerQueuePush(volCtx : PFATVolumeCtx; transfer : PFATFileIOAsyncCtx) : boolean; -begin - fatWorkerQueuePush := false; - if (volCtx = nil) or (transfer = nil) then exit; - - asm pushf; cli end; - if volCtx^.WorkCount < FAT_WORK_QUEUE_CAPACITY then begin - volCtx^.WorkQueue[volCtx^.WorkTail] := transfer; - volCtx^.WorkTail := (volCtx^.WorkTail + 1) mod FAT_WORK_QUEUE_CAPACITY; - volCtx^.WorkCount := volCtx^.WorkCount + 1; - fatWorkerQueuePush := true; - end; - asm popf end; -end; - -function fatWorkerQueuePop(volCtx : PFATVolumeCtx; var transfer : PFATFileIOAsyncCtx) : boolean; -begin - fatWorkerQueuePop := false; - transfer := nil; - if volCtx = nil then exit; - - asm pushf; cli end; - if volCtx^.WorkCount > 0 then begin - transfer := volCtx^.WorkQueue[volCtx^.WorkHead]; - volCtx^.WorkQueue[volCtx^.WorkHead] := nil; - volCtx^.WorkHead := (volCtx^.WorkHead + 1) mod FAT_WORK_QUEUE_CAPACITY; - volCtx^.WorkCount := volCtx^.WorkCount - 1; - fatWorkerQueuePop := true; - end; - asm popf end; -end; - -procedure fatWorkerWake(volCtx : PFATVolumeCtx); -begin - if (volCtx = nil) or (volCtx^.Worker = nil) then exit; - - asm pushf; cli end; - volCtx^.Worker^.PendingMsg := smCustom; - volCtx^.Worker^.MsgData := nil; - if volCtx^.Worker^.State = psAwaiting then - volCtx^.Worker^.State := psReady; - asm popf end; -end; - -procedure fat32_fileio_async_run_next(ctx : PFATFileIOAsyncCtx); forward; - -procedure fat32_transfer_worker(pctx : proc.types.PProcessContext); -var - volCtx : PFATVolumeCtx; - transfer : PFATFileIOAsyncCtx; -begin - volCtx := PFATVolumeCtx(pctx^.Local); - if volCtx = nil then begin - proc.mgr.proc_exit(1); - exit; - end; - - while true do begin - if pctx^.PendingMsg = smTerminate then begin - pctx^.PendingMsg := smNone; - proc.mgr.proc_exit(0); - exit; - end; - - if fatWorkerQueuePop(volCtx, transfer) then begin - fat32_fileio_async_run_next(transfer); - continue; - end; - - asm pushf; cli end; - if volCtx^.WorkCount = 0 then begin - pctx^.PendingMsg := smNone; - pctx^.State := psAwaiting; - end; - asm popf end; - - if pctx^.State = psAwaiting then - proc.mgr.proc_await; - end; -end; - -procedure fatWorkerEnsure(volume : PStorage_Volume); -var - volCtx : PFATVolumeCtx; -begin - volCtx := fatVolumeCtxGet(volume); - if volCtx = nil then exit; - if volCtx^.Worker <> nil then exit; - volCtx^.Worker := proc.mgr.create('fat32.xfer', @fat32_transfer_worker, void(volCtx), 1); -end; - -function fatScheduleTransfer(ctx : PFATFileIOAsyncCtx) : boolean; -var - volCtx : PFATVolumeCtx; -begin - fatScheduleTransfer := false; - if ctx = nil then exit; - - volCtx := ctx^.VolumeCtx; - if volCtx = nil then exit; - if volCtx^.Worker = nil then exit; - - if fatWorkerQueuePush(volCtx, ctx) then begin - fatWorkerWake(volCtx); - fatScheduleTransfer := true; - end; -end; - -{ Flush all dirty cache lines to disk. - CLI/STI protects cache metadata; disk I/O runs with interrupts enabled. } -procedure fatCacheFlush(cache : PFATCache; volume : PStorage_Volume); -var - i : uint32; - lba : uint32; - needFlush : boolean; -begin - { Acquire Busy lock — protects EvictBuf and prevents concurrent evictions } - while true do begin - asm pushf; cli end; - if not cache^.Busy then begin - cache^.Busy := true; - asm popf end; - break; - end; - asm popf end; - asm hlt end; - end; - - for i := 0 to FAT_CACHE_LINES - 1 do begin - asm pushf; cli end; - needFlush := cache^.Lines[i].Valid and cache^.Lines[i].Dirty; - if needFlush then begin - lba := cache^.FatStart + cache^.Lines[i].SectorIdx; - cache^.Lines[i].Dirty := false; - core.util.memcpy(uint32(cache^.Data) + (i * 512), - uint32(cache^.EvictBuf), 512); - end; - asm popf end; - - if needFlush then - driver.storage.mgr.storage_write( - cache^.Device, lba, 1, cache^.EvictBuf); - end; - - { Release lock } - asm pushf; cli end; - cache^.Busy := false; - asm popf end; -end; - -{ Invalidate all cache lines (e.g. on unmount). } -procedure fatCacheInvalidate(cache : PFATCache); -var - i : uint32; -begin - for i := 0 to FAT_CACHE_LINES - 1 do begin - cache^.Lines[i].Valid := false; - cache^.Lines[i].Dirty := false; - cache^.Lines[i].LRU := uint8(i mod FAT_CACHE_WAYS); - end; -end; - -{ Return the per-volume cached boot record without any allocation. - The FAT cache lazily reads and keeps a single copy; callers MUST NOT kfree it. } -function getCachedBootRecord(volume : PStorage_Volume) : PBootRecord; -begin - getCachedBootRecord := fatCacheGet(volume)^.BootRecord; -end; - -{ Read a single FAT entry using the sector cache. - 128 FAT entries per 512-byte sector. 2-way set-associative: set = fatSector mod FAT_CACHE_SETS. - CLI/STI protects cache metadata; disk I/O runs with interrupts enabled. } -function readFat(volume : PStorage_volume; cluster : uint32; bootRecord : PBootRecord) : uint32; -var - cache : PFATCache; - fatSecIdx : uint32; { which FAT sector this cluster lives in } - setIdx : uint32; { cache set index } - baseSlot : uint32; { first line index of this set } - entryOff : uint32; { offset within that sector (0..127) } - dataPtr : puint32; { pointer into cache data for the hit/victim slot } - lba : uint32; - doEvict : boolean; - evictLBA : uint32; - w : uint32; { way iterator } - victim : uint32; { chosen victim slot index } -begin - cache := fatCacheGet(volume); - fatSecIdx := cluster div 128; - setIdx := fatSecIdx mod FAT_CACHE_SETS; - baseSlot := setIdx * FAT_CACHE_WAYS; - entryOff := cluster mod 128; - - { Fast path: cache hit under CLI — check both ways } - asm pushf; cli end; - for w := 0 to FAT_CACHE_WAYS - 1 do begin - if cache^.Lines[baseSlot + w].Valid and (cache^.Lines[baseSlot + w].SectorIdx = fatSecIdx) then begin - dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); - readFat := dataPtr[entryOff] and $0FFFFFFF; - { Update LRU: this way is MRU } - cache^.Lines[baseSlot + w].LRU := 0; - cache^.Lines[baseSlot + (1 - w)].LRU := 1; - asm popf end; - exit; - end; - end; - asm popf end; - - { Miss path: acquire Busy spinlock to serialize eviction+load } - while true do begin - asm pushf; cli end; - if not cache^.Busy then begin - { Re-check for hit — another process may have loaded this sector } - for w := 0 to FAT_CACHE_WAYS - 1 do begin - if cache^.Lines[baseSlot + w].Valid and (cache^.Lines[baseSlot + w].SectorIdx = fatSecIdx) then begin - dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); - readFat := dataPtr[entryOff] and $0FFFFFFF; - cache^.Lines[baseSlot + w].LRU := 0; - cache^.Lines[baseSlot + (1 - w)].LRU := 1; - asm popf end; - exit; - end; - end; - cache^.Busy := true; - { Pick victim: prefer invalid way, else LRU way } - victim := baseSlot; - if (not cache^.Lines[baseSlot].Valid) then - victim := baseSlot - else if (not cache^.Lines[baseSlot + 1].Valid) then - victim := baseSlot + 1 - else if cache^.Lines[baseSlot + 1].LRU >= cache^.Lines[baseSlot].LRU then - victim := baseSlot + 1 - else - victim := baseSlot; - dataPtr := puint32(uint32(cache^.Data) + (victim * 512)); - doEvict := cache^.Lines[victim].Valid and cache^.Lines[victim].Dirty; - if doEvict then begin - evictLBA := cache^.FatStart + cache^.Lines[victim].SectorIdx; - core.util.memcpy(uint32(dataPtr), uint32(cache^.EvictBuf), 512); - end; - cache^.Lines[victim].Valid := false; - asm popf end; - break; - end; - asm popf end; - asm hlt end; - end; - - { Disk I/O with interrupts enabled — exclusive via Busy flag } - if doEvict then - driver.storage.mgr.storage_write(cache^.Device, evictLBA, 1, cache^.EvictBuf); - lba := cache^.FatStart + fatSecIdx; - driver.storage.mgr.storage_read(cache^.Device, lba, 1, dataPtr); - - { Update cache metadata, set LRU, release lock under CLI } - asm pushf; cli end; - cache^.Lines[victim].SectorIdx := fatSecIdx; - cache^.Lines[victim].Valid := true; - cache^.Lines[victim].Dirty := false; - { New entry is MRU, the other way is LRU } - cache^.Lines[victim].LRU := 0; - if victim = baseSlot then - cache^.Lines[baseSlot + 1].LRU := 1 - else - cache^.Lines[baseSlot].LRU := 1; - readFat := dataPtr[entryOff] and $0FFFFFFF; - cache^.Busy := false; - asm popf end; -end; - -{ Write a single FAT entry into cache. No immediate disk I/O — call fatCacheFlush later. - 2-way set-associative: set = fatSector mod FAT_CACHE_SETS. - CLI/STI protects cache metadata; disk I/O runs with interrupts enabled. } -procedure writeFat(volume : PStorage_volume; cluster : uint32; value : uint32; bootRecord : PBootRecord); -var - cache : PFATCache; - fatSecIdx : uint32; - setIdx : uint32; - baseSlot : uint32; - entryOff : uint32; - dataPtr : puint32; - lba : uint32; - doEvict : boolean; - evictLBA : uint32; - w : uint32; - victim : uint32; -begin - cache := fatCacheGet(volume); - fatSecIdx := cluster div 128; - setIdx := fatSecIdx mod FAT_CACHE_SETS; - baseSlot := setIdx * FAT_CACHE_WAYS; - entryOff := cluster mod 128; - - { Fast path: cache hit — check both ways } - asm pushf; cli end; - for w := 0 to FAT_CACHE_WAYS - 1 do begin - if cache^.Lines[baseSlot + w].Valid and (cache^.Lines[baseSlot + w].SectorIdx = fatSecIdx) then begin - dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); - if value = 0 then - dataPtr[entryOff] := 0 - else - dataPtr[entryOff] := (dataPtr[entryOff] and $F0000000) or (value and $0FFFFFFF); - cache^.Lines[baseSlot + w].Dirty := true; - { Update LRU: this way is MRU } - cache^.Lines[baseSlot + w].LRU := 0; - cache^.Lines[baseSlot + (1 - w)].LRU := 1; - asm popf end; - exit; - end; - end; - asm popf end; - - { Miss path: acquire Busy spinlock } - while true do begin - asm pushf; cli end; - if not cache^.Busy then begin - { Re-check for hit after acquiring lock } - for w := 0 to FAT_CACHE_WAYS - 1 do begin - if cache^.Lines[baseSlot + w].Valid and (cache^.Lines[baseSlot + w].SectorIdx = fatSecIdx) then begin - dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); - if value = 0 then - dataPtr[entryOff] := 0 - else - dataPtr[entryOff] := (dataPtr[entryOff] and $F0000000) or (value and $0FFFFFFF); - cache^.Lines[baseSlot + w].Dirty := true; - cache^.Lines[baseSlot + w].LRU := 0; - cache^.Lines[baseSlot + (1 - w)].LRU := 1; - asm popf end; - exit; - end; - end; - cache^.Busy := true; - { Pick victim: prefer invalid way, else LRU way } - victim := baseSlot; - if (not cache^.Lines[baseSlot].Valid) then - victim := baseSlot - else if (not cache^.Lines[baseSlot + 1].Valid) then - victim := baseSlot + 1 - else if cache^.Lines[baseSlot + 1].LRU >= cache^.Lines[baseSlot].LRU then - victim := baseSlot + 1 - else - victim := baseSlot; - dataPtr := puint32(uint32(cache^.Data) + (victim * 512)); - doEvict := cache^.Lines[victim].Valid and cache^.Lines[victim].Dirty; - if doEvict then begin - evictLBA := cache^.FatStart + cache^.Lines[victim].SectorIdx; - core.util.memcpy(uint32(dataPtr), uint32(cache^.EvictBuf), 512); - end; - cache^.Lines[victim].Valid := false; - asm popf end; - break; - end; - asm popf end; - asm hlt end; - end; - - { Disk I/O with interrupts enabled — exclusive via Busy flag } - if doEvict then - driver.storage.mgr.storage_write(cache^.Device, evictLBA, 1, cache^.EvictBuf); - lba := cache^.FatStart + fatSecIdx; - driver.storage.mgr.storage_read(cache^.Device, lba, 1, dataPtr); - - { Update cache, write value, set LRU, release lock under CLI } - asm pushf; cli end; - cache^.Lines[victim].SectorIdx := fatSecIdx; - cache^.Lines[victim].Valid := true; - cache^.Lines[victim].Dirty := false; - if value = 0 then - dataPtr[entryOff] := 0 - else - dataPtr[entryOff] := (dataPtr[entryOff] and $F0000000) or (value and $0FFFFFFF); - cache^.Lines[victim].Dirty := true; - cache^.Lines[victim].LRU := 0; - if victim = baseSlot then - cache^.Lines[baseSlot + 1].LRU := 1 - else - cache^.Lines[baseSlot].LRU := 1; - cache^.Busy := false; - asm popf end; -end; - -function getFatChain(volume : PStorage_volume; cluster : uint32; bootRecord : PBootRecord) : PLinkedListBase; -var - currentCluster : uint32; - currentClusterValue : uint32; - clusters : PLinkedListBase; - dirElm : puint32; - maxCluster : uint32; - iterCount : uint32; -begin - clusters:= LL_New(sizeof(uint32)); - currentCluster:= cluster; - currentClusterValue:= cluster; - if bootRecord^.sectorSize > 0 then - maxCluster := (bootRecord^.FATSize * bootRecord^.sectorSize) div 4 - else - maxCluster := 0; - iterCount := 0; - - while true do begin - if (maxCluster > 0) and (iterCount >= maxCluster) then - break; - currentClusterValue:= readFat(volume, currentClusterValue, bootRecord); - - if (currentClusterValue and $0FFFFFFF) = $0FFFFFF7 then begin - break; - end else if (currentClusterValue and $0FFFFFFF) >= $0FFFFFF8 then begin - dirElm:= LL_add(clusters); - dirElm^:= currentCluster; - break; - end else if currentClusterValue = 0 then begin - break; - end else begin - dirElm:= LL_add(clusters); - dirElm^:= currentCluster; - end; - - currentCluster := currentClusterValue; - iterCount := iterCount + 1; - end; - - getFatChain:= clusters; -end; - -function fatMaxClusterCount(bootRecord : PBootRecord) : uint32; -begin - if (bootRecord <> nil) and (bootRecord^.sectorSize > 0) then - fatMaxClusterCount := (bootRecord^.FATSize * bootRecord^.sectorSize) div 4 - else - fatMaxClusterCount := 0; -end; - -function fatDefaultSPC(sectors : uint32; sectorSize : uint32) : uint32; -begin - if sectorSize = 0 then begin - fatDefaultSPC := 1; - exit; - end; - - { Default to 4 KiB clusters for sane FAT32 throughput without excessive - slack on small files. Callers can still override via config. } - fatDefaultSPC := 4096 div sectorSize; - if fatDefaultSPC = 0 then - fatDefaultSPC := 1; - if fatDefaultSPC > 64 then - fatDefaultSPC := 64; -end; - -function fatComputeFATSize(sectors : uint32; spc : uint32; sectorSize : uint32) : uint32; -var - clusterCount : uint32; - fatBytes : uint32; -begin - if spc = 0 then - spc := 1; - if sectorSize = 0 then begin - fatComputeFATSize := 0; - exit; - end; - - clusterCount := (sectors + spc - 1) div spc; - fatBytes := clusterCount * 4; - fatComputeFATSize := (fatBytes + sectorSize - 1) div sectorSize; - if fatComputeFATSize = 0 then - fatComputeFATSize := 1; -end; - -function fatClustersForBytes(byteCount : uint32; bytesPerCluster : uint32) : uint32; -begin - if (byteCount = 0) or (bytesPerCluster = 0) then begin - fatClustersForBytes := 0; - exit; - end; - - fatClustersForBytes := (byteCount + bytesPerCluster - 1) div bytesPerCluster; -end; - -function fatPreallocClusterCount(bytesPerCluster : uint32) : uint32; -begin - if bytesPerCluster = 0 then begin - fatPreallocClusterCount := 1; - exit; - end; - - fatPreallocClusterCount := FAT_WRITE_PREALLOC_BYTES div bytesPerCluster; - if fatPreallocClusterCount = 0 then - fatPreallocClusterCount := 1; - if fatPreallocClusterCount < FAT_WRITE_PREALLOC_MIN_CLUSTERS then - fatPreallocClusterCount := FAT_WRITE_PREALLOC_MIN_CLUSTERS; - if fatPreallocClusterCount > FAT_WRITE_PREALLOC_MAX_CLUSTERS then - fatPreallocClusterCount := FAT_WRITE_PREALLOC_MAX_CLUSTERS; -end; - -procedure fatUpdateAllocHint(volume : PStorage_volume; bootRecord : PBootRecord; nextCluster : uint32); -var - maxCluster : uint32; -begin - maxCluster := fatMaxClusterCount(bootRecord); - if maxCluster <= 2 then begin - sioc_allocHint := 2; - sioc_allocVol := volume; - exit; - end; - - if nextCluster < 2 then - nextCluster := 2; - if nextCluster >= maxCluster then - nextCluster := 2; - - sioc_allocHint := nextCluster; - sioc_allocVol := volume; -end; - -function fatTryContiguousRange(volume : PStorage_volume; bootRecord : PBootRecord; - rangeStart : uint32; rangeEnd : uint32; needed : uint32; - var foundStart : uint32) : boolean; -var - scanPos : uint32; - runLen : uint32; - runPos : uint32; - fatVal : uint32; -begin - fatTryContiguousRange := false; - if (needed = 0) or (bootRecord = nil) or (rangeStart >= rangeEnd) then - exit; - - scanPos := rangeStart; - runLen := 0; - runPos := 0; - - while scanPos < rangeEnd do begin - fatVal := readFat(volume, scanPos, bootRecord); - if fatVal = 0 then begin - if runLen = 0 then - runPos := scanPos; - runLen := runLen + 1; - if runLen >= needed then begin - foundStart := runPos; - fatTryContiguousRange := true; - exit; - end; - end else - runLen := 0; - scanPos := scanPos + 1; - end; -end; - -function fatFindContiguousFreeRange(volume : PStorage_volume; amount : uint32; - bootRecord : PBootRecord; var runStart : uint32) : boolean; -var - scanStart : uint32; - maxCluster : uint32; -begin - fatFindContiguousFreeRange := false; - if amount = 0 then exit; - - maxCluster := fatMaxClusterCount(bootRecord); - if maxCluster <= 2 then exit; - - if (sioc_allocVol = volume) and (sioc_allocHint >= 2) and (sioc_allocHint < maxCluster) then - scanStart := sioc_allocHint - else - scanStart := 2; - - if fatTryContiguousRange(volume, bootRecord, scanStart, maxCluster, amount, runStart) or - ((scanStart > 2) and fatTryContiguousRange(volume, bootRecord, 2, scanStart, amount, runStart)) then - fatFindContiguousFreeRange := true; -end; - -procedure fatLinkContiguousRange(volume : PStorage_volume; bootRecord : PBootRecord; - previousTail : uint32; firstCluster : uint32; clusterCount : uint32; - var lastCluster : uint32); -var - cur : uint32; - i : uint32; -begin - lastCluster := previousTail; - if clusterCount = 0 then exit; - - if previousTail <> 0 then - writeFat(volume, previousTail, firstCluster, bootRecord); - - cur := firstCluster; - i := 1; - while i < clusterCount do begin - writeFat(volume, cur, cur + 1, bootRecord); - cur := cur + 1; - i := i + 1; - end; - - writeFat(volume, cur, $FFFFFFF8, bootRecord); - lastCluster := cur; - fatUpdateAllocHint(volume, bootRecord, lastCluster + 1); -end; - -function fatAppendExtent(extents : PLinkedListBase; firstCluster : uint32; - clusterCount : uint32) : boolean; -var - ext : PClusterExtent; - lastExt : PClusterExtent; - extCount : uint32; -begin - fatAppendExtent := false; - if (extents = nil) or (clusterCount = 0) then - exit; - - extCount := LL_Size(extents); - if extCount > 0 then begin - lastExt := PClusterExtent(LL_Get(extents, extCount - 1)); - if (lastExt <> nil) and ((lastExt^.StartCluster + lastExt^.ClusterCount) = firstCluster) then begin - lastExt^.ClusterCount := lastExt^.ClusterCount + clusterCount; - fatAppendExtent := true; - exit; - end; - end; - - ext := PClusterExtent(LL_Add(extents)); - if ext = nil then - exit; - ext^.StartCluster := firstCluster; - ext^.ClusterCount := clusterCount; - fatAppendExtent := true; -end; - -function fatBuildExtentList(volume : PStorage_volume; cluster : uint32; - bootRecord : PBootRecord) : PLinkedListBase; -var - extents : PLinkedListBase; - current : uint32; - nextCluster : uint32; - runStart : uint32; - runCount : uint32; - maxCluster : uint32; - iterCount : uint32; -begin - fatBuildExtentList := nil; - if (bootRecord = nil) or (cluster < 2) then - exit; - - extents := LL_New(sizeof(TClusterExtent)); - if extents = nil then - exit; - - if bootRecord^.sectorSize > 0 then - maxCluster := (bootRecord^.FATSize * bootRecord^.sectorSize) div 4 - else - maxCluster := 0; - - current := cluster; - runStart := cluster; - runCount := 1; - iterCount := 0; - - while true do begin - if (maxCluster > 0) and (iterCount >= maxCluster) then - break; - - nextCluster := readFat(volume, current, bootRecord); - if (nextCluster = 0) or ((nextCluster and $0FFFFFFF) = $0FFFFFF7) then - break; - - if (nextCluster and $0FFFFFFF) >= $0FFFFFF8 then begin - if not fatAppendExtent(extents, runStart, runCount) then begin - LL_Free(extents); - exit; - end; - fatBuildExtentList := extents; - exit; - end; - - if nextCluster = (current + 1) then - runCount := runCount + 1 - else begin - if not fatAppendExtent(extents, runStart, runCount) then begin - LL_Free(extents); - exit; - end; - runStart := nextCluster; - runCount := 1; - end; - - current := nextCluster; - iterCount := iterCount + 1; - end; - - if runCount > 0 then begin - if not fatAppendExtent(extents, runStart, runCount) then begin - LL_Free(extents); - exit; - end; - end; - - fatBuildExtentList := extents; -end; - -function fatLocateExtentRun(extents : PLinkedListBase; targetIdx : uint32; - hintValid : boolean; hintExtentIdx : uint32; - hintStartIdx : uint32; var runStartCluster : uint32; - var runClusterCount : uint32; var outExtentIdx : uint32; - var outExtentStartIdx : uint32) : boolean; -var - i : uint32; - clusterIx : uint32; - extCount : uint32; - ext : PClusterExtent; -begin - fatLocateExtentRun := false; - if extents = nil then - exit; - - extCount := LL_Size(extents); - if extCount = 0 then - exit; - - if hintValid and (hintExtentIdx < extCount) and (hintStartIdx <= targetIdx) then begin - i := hintExtentIdx; - clusterIx := hintStartIdx; - end else begin - i := 0; - clusterIx := 0; - end; - - while i < extCount do begin - ext := PClusterExtent(LL_Get(extents, i)); - if ext = nil then - exit; - - if targetIdx < (clusterIx + ext^.ClusterCount) then begin - runStartCluster := ext^.StartCluster + (targetIdx - clusterIx); - runClusterCount := ext^.ClusterCount - (targetIdx - clusterIx); - outExtentIdx := i; - outExtentStartIdx := clusterIx; - fatLocateExtentRun := true; - exit; - end; - - clusterIx := clusterIx + ext^.ClusterCount; - i := i + 1; - end; -end; - -function findFreeClusterExtents(volume : PStorage_volume; amount : uint32; - bootRecord : PBootRecord) : PLinkedListBase; -var - maxCluster : uint32; - scanStart : uint32; - currentAmount : uint32; - lastAllocated : uint32; - extents : PLinkedListBase; - - procedure appendExtent(firstCluster : uint32; clusterCount : uint32); - var - ext : PClusterExtent; - begin - ext := PClusterExtent(LL_Add(extents)); - ext^.StartCluster := firstCluster; - ext^.ClusterCount := clusterCount; - currentAmount := currentAmount + clusterCount; - lastAllocated := firstCluster + clusterCount - 1; - end; - - procedure scanRange(rangeStart : uint32; rangeEnd : uint32); - var - scanPos : uint32; - runStart : uint32; - runLen : uint32; - takeCount : uint32; - fatVal : uint32; - begin - scanPos := rangeStart; - while (scanPos < rangeEnd) and (currentAmount < amount) do begin - fatVal := readFat(volume, scanPos, bootRecord); - if fatVal <> 0 then begin - scanPos := scanPos + 1; - continue; - end; - - runStart := scanPos; - runLen := 0; - while scanPos < rangeEnd do begin - fatVal := readFat(volume, scanPos, bootRecord); - if fatVal <> 0 then - break; - runLen := runLen + 1; - scanPos := scanPos + 1; - end; - - takeCount := amount - currentAmount; - if runLen < takeCount then - takeCount := runLen; - appendExtent(runStart, takeCount); - end; - end; -begin - extents := LL_New(sizeof(TClusterExtent)); - currentAmount := 0; - lastAllocated := 0; - - if amount = 0 then begin - findFreeClusterExtents := extents; - exit; - end; - - maxCluster := fatMaxClusterCount(bootRecord); - if maxCluster <= 2 then begin - LL_Free(extents); - findFreeClusterExtents := nil; - exit; - end; - - if (sioc_allocVol = volume) and (sioc_allocHint >= 2) and (sioc_allocHint < maxCluster) then - scanStart := sioc_allocHint - else - scanStart := 2; - - scanRange(scanStart, maxCluster); - if (currentAmount < amount) and (scanStart > 2) then - scanRange(2, scanStart); - - if currentAmount < amount then begin - LL_Free(extents); - findFreeClusterExtents := nil; - exit; - end; - - fatUpdateAllocHint(volume, bootRecord, lastAllocated + 1); - findFreeClusterExtents := extents; -end; - -function compareByteArray8(str1 : byteArray8; str2 : byteArray8) : boolean; -var - i : uint32; -begin - push_trace('driver.storage.fs.fat32.compareArray'); - compareByteArray8:= true; - for i:=0 to 7 do begin - if str1[i] <> str2[i] then begin - compareByteArray8:= false; - break; - end; - end; -end; - -{ Extract the starting cluster number from a directory entry. } -function dirFirstCluster(dir : PDirectory) : uint32; -begin - dirFirstCluster := uint32(dir^.clusterLow) or uint32(dir^.clusterHigh shl 16); -end; - -{ Set the starting cluster number in a directory entry. } -procedure setDirFirstCluster(dir : PDirectory; cluster : uint32); -begin - dir^.clusterLow := uint16(cluster and $FFFF); - dir^.clusterHigh := uint16((cluster shr 16) and $FFFF); -end; - -{ Calculate the LBA of the first data sector on a FAT32 volume. } -function fatDataStartLBA(volume : PStorage_Volume; bootRecord : PBootRecord) : uint32; -begin - fatDataStartLBA := volume^.sectorStart + bootRecord^.rsvSectors + bootRecord^.FATSize; -end; - -{ Convert a cluster number to its starting LBA. } -function clusterToLBA(dataStart : uint32; spc : uint32; cluster : uint32) : uint32; -begin - if cluster < 2 then - clusterToLBA := dataStart - else - clusterToLBA := dataStart + ((cluster - 2) * spc); -end; - -{ Write an 8.3 extension into a TFATExtArray: uppercase, pad with spaces. } -procedure fillFatExt(var dest : TFATExtArray; extPart : pchar); -var - j : uint32; -begin - dest[0] := ' '; - dest[1] := ' '; - dest[2] := ' '; - if (extPart <> nil) and (extPart[0] <> char(0)) then begin - for j := 0 to 2 do begin - if extPart[j] = char(0) then break; - if (extPart[j] >= 'a') and (extPart[j] <= 'z') then - dest[j] := char(uint8(extPart[j]) - 32) - else - dest[j] := extPart[j]; - end; - end; -end; - -{ Search a single sector buffer for a directory entry matching name+ext. - Returns the index (0..entriesPerSec-1), or -1 if not found. - Sets hitEnd to true if a char(0) sentinel was reached (end of directory). } -function findEntryInSector(buffer : puint32; entriesPerSec : uint32; - name : byteArray8; ext : pchar; var hitEnd : boolean) : integer; -var - idx : uint32; - dir : PDirectory; -begin - findEntryInSector := -1; - hitEnd := false; - if entriesPerSec > 0 then - for idx := 0 to entriesPerSec - 1 do begin - dir := @PDirectory(buffer)[idx]; - if dir^.fileName[0] = char(0) then begin - hitEnd := true; - exit; - end; - if dir^.fileName[0] = char($E5) then continue; - if compareByteArray8(dir^.fileName, name) - and matchExtension(dir^.fileExtension, ext) then begin - findEntryInSector := integer(idx); - exit; - end; - end; -end; - -{ Search a single sector buffer for a free directory entry (char(0) or $E5). - Returns the index (0..entriesPerSec-1), or -1 if sector is full. } -function findFreeEntryInSector(buffer : puint32; entriesPerSec : uint32) : integer; -var - idx : uint32; - dir : PDirectory; -begin - findFreeEntryInSector := -1; - if entriesPerSec > 0 then - for idx := 0 to entriesPerSec - 1 do begin - dir := @PDirectory(buffer)[idx]; - if (dir^.fileName[0] = char(0)) or (dir^.fileName[0] = char($E5)) then begin - findFreeEntryInSector := integer(idx); - exit; - end; - end; -end; - -{ --- Higher-level helpers --- } - -type - TDirEntryLocation = record - Found : boolean; - SectorLBA : uint32; - EntryIdx : uint32; - end; - - TFileLookup = record - BootRecord : PBootRecord; - Dirs : PLinkedListBase; - Dir : PDirectory; - Exists : boolean; - Status : puint32; - end; - - TRunInfo = record - StartCluster : uint32; - LastCluster : uint32; - RunLen : uint32; - RunSectors : uint32; - RunBytes : uint32; - LBA : uint32; - end; - -{ Locate a named entry in the parent directory's raw sectors. - Walks the full FAT chain of parentCluster, scanning each sector - with findEntryInSector. On match, fills loc and leaves buffer - containing the matching sector (caller must kfree buffer). - Returns true if found. } -function locateDirEntry( - volume : PStorage_Volume; - parentCluster : uint32; - bootRecord : PBootRecord; - cleanName : byteArray8; - ext : pchar; - var loc : TDirEntryLocation; - buffer : puint32 -) : boolean; -var - dataStart : uint32; - spc : uint32; - entriesPerSector : uint32; - dirChain : PLinkedListBase; - dc : uint32; - ds : uint32; - curCluster : uint32; - clusterLBA : uint32; - hitEnd : boolean; - entryIdx : integer; - done : boolean; -begin - locateDirEntry := false; - loc.Found := false; - - dataStart := fatDataStartLBA(volume, bootRecord); - spc := uint32(bootRecord^.spc); - entriesPerSector := uint32(bootRecord^.sectorSize) div uint32(sizeof(TDirectory)); - dirChain := getFatChain(volume, parentCluster, bootRecord); - done := false; - - dc := 0; - while (dc < LL_size(dirChain)) and (not done) do begin - curCluster := puint32(LL_Get(dirChain, dc))^; - clusterLBA := clusterToLBA(dataStart, spc, curCluster); - ds := 0; - while (ds < spc) and (not done) do begin - driver.storage.mgr.storage_read(volume^.device, clusterLBA + ds, 1, buffer); - entryIdx := findEntryInSector(buffer, entriesPerSector, cleanName, ext, hitEnd); - if hitEnd then - done := true - else if entryIdx >= 0 then begin - loc.Found := true; - loc.SectorLBA := clusterLBA + ds; - loc.EntryIdx := uint32(entryIdx); - done := true; - end; - ds := ds + 1; - end; - dc := dc + 1; - end; - - LL_Free(dirChain); - locateDirEntry := loc.Found; -end; - -{ Locate an unused (free) directory entry slot in the parent directory. - Returns true if a free slot was found, filling loc and leaving buffer - containing the sector. } -function locateFreeDirEntry( - volume : PStorage_Volume; - parentCluster : uint32; - bootRecord : PBootRecord; - var loc : TDirEntryLocation; - buffer : puint32 -) : boolean; -var - dataStart : uint32; - spc : uint32; - entriesPerSector : uint32; - dirChain : PLinkedListBase; - dc : uint32; - ds : uint32; - curCluster : uint32; - clusterLBA : uint32; - entryIdx : integer; - found : boolean; -begin - locateFreeDirEntry := false; - loc.Found := false; - - dataStart := fatDataStartLBA(volume, bootRecord); - spc := uint32(bootRecord^.spc); - entriesPerSector := uint32(bootRecord^.sectorSize) div uint32(sizeof(TDirectory)); - dirChain := getFatChain(volume, parentCluster, bootRecord); - found := false; - - if LL_size(dirChain) > 0 then begin - for dc := 0 to LL_size(dirChain) - 1 do begin - curCluster := puint32(LL_Get(dirChain, dc))^; - clusterLBA := clusterToLBA(dataStart, spc, curCluster); - for ds := 0 to spc - 1 do begin - driver.storage.mgr.storage_read(volume^.device, clusterLBA + ds, 1, buffer); - entryIdx := findFreeEntryInSector(buffer, entriesPerSector); - if entryIdx >= 0 then begin - loc.Found := true; - loc.SectorLBA := clusterLBA + ds; - loc.EntryIdx := uint32(entryIdx); - found := true; - end; - if found then break; - end; - if found then break; - end; - end; - - LL_Free(dirChain); - locateFreeDirEntry := loc.Found; -end; - -{ Build a contiguous cluster run starting at startCluster. - Scans ahead via readFat, accumulating consecutive clusters up to - maxRunSectors worth of sectors. - Returns the number of clusters in the run (runLen). - Sets lastCluster to the last cluster in the run (= startCluster + runLen - 1). } -function buildContiguousRun( - volume : PStorage_Volume; - startCluster : uint32; - spc : uint32; - maxRunSectors : uint32; - bootRecord : PBootRecord; - var runLen : uint32; - var lastCluster : uint32 -) : uint32; -var - nextFat : uint32; -begin - runLen := 1; - lastCluster := startCluster; - - while (runLen * spc) < maxRunSectors do begin - nextFat := readFat(volume, lastCluster, bootRecord); - if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then - break; - if nextFat <> lastCluster + 1 then - break; - lastCluster := nextFat; - runLen := runLen + 1; - end; - - buildContiguousRun := runLen; -end; - -procedure updateSIOC( - volume : PStorage_Volume; - fileStartCluster : uint32; - chainIdx : uint32; - chainCluster : uint32 -); forward; - -function seekToClusterIndex( - volume : PStorage_Volume; - fileStartCluster : uint32; - targetIdx : uint32; - bootRecord : PBootRecord; - var curCluster : uint32; - var chainPos : uint32 -) : boolean; forward; - -function seekToClusterIndexHinted( - volume : PStorage_Volume; - fileStartCluster : uint32; - targetIdx : uint32; - bootRecord : PBootRecord; - hintValid : boolean; - hintIdx : uint32; - hintCluster : uint32; - var curCluster : uint32; - var chainPos : uint32 -) : boolean; forward; - -procedure fat32_fileio_async_complete(ctx : PFATFileIOAsyncCtx; error : TError); -begin - if ctx = nil then exit; - if ctx^.BytesOut <> nil then - ctx^.BytesOut^ := ctx^.BytesDone; - if ctx^.Callback <> nil then - ctx^.Callback(error, ctx^.CallbackData); - fatTransferFree(ctx); -end; - -function fat32_fileio_prepare_single_run(ctx : PFATFileIOAsyncCtx; - var sectorLBA : uint32; var chunkSectors : uint32) : boolean; -const - MAX_RUN_SECTORS = 2048; -var - run : TRunInfo; - curCluster : uint32; - chainPos : uint32; - currentOffset : uint32; - bytesPerCluster : uint32; - startClusterIdx : uint32; - inClusterOffset : uint32; - remaining : uint32; - skipBytes : uint32; - copyBytes : uint32; - secSize : uint32; - spc : uint32; - lastBytePos : uint32; - lastClusterUsed : uint32; - lastClusterIdx : uint32; - maxRunClusters : uint32; - extentIdx : uint32; - extentStartIdx : uint32; -begin - fat32_fileio_prepare_single_run := false; - if ctx = nil then - exit; - - currentOffset := ctx^.Offset + ctx^.BytesDone; - remaining := ctx^.ByteCount - ctx^.BytesDone; - spc := uint32(ctx^.BootRecord^.spc); - secSize := uint32(ctx^.BootRecord^.sectorSize); - bytesPerCluster := spc * secSize; - startClusterIdx := currentOffset div bytesPerCluster; - inClusterOffset := currentOffset mod bytesPerCluster; - - maxRunClusters := MAX_RUN_SECTORS div spc; - if maxRunClusters = 0 then - maxRunClusters := 1; - - if (ctx^.OpenFile <> nil) and (ctx^.OpenFile^.Extents <> nil) and - fatLocateExtentRun(ctx^.OpenFile^.Extents, startClusterIdx, - ctx^.ExtentValid, ctx^.ExtentIdx, ctx^.ExtentStartIdx, - run.StartCluster, run.RunLen, extentIdx, extentStartIdx) then begin - if run.RunLen > maxRunClusters then - run.RunLen := maxRunClusters; - run.LastCluster := run.StartCluster + run.RunLen - 1; - ctx^.ExtentValid := true; - ctx^.ExtentIdx := extentIdx; - ctx^.ExtentStartIdx := extentStartIdx; - curCluster := run.LastCluster; - chainPos := startClusterIdx + run.RunLen - 1; - end else begin - if not seekToClusterIndexHinted( - ctx^.Volume, ctx^.Cluster, startClusterIdx, ctx^.BootRecord, - ctx^.CursorValid, ctx^.CursorIndex, ctx^.CursorCluster, - curCluster, chainPos) then - exit; - - run.StartCluster := curCluster; - buildContiguousRun(ctx^.Volume, curCluster, spc, MAX_RUN_SECTORS, ctx^.BootRecord, run.RunLen, run.LastCluster); - end; - - run.RunSectors := run.RunLen * spc; - run.RunBytes := run.RunSectors * secSize; - run.LBA := clusterToLBA(ctx^.DataStart, spc, run.StartCluster); - - skipBytes := inClusterOffset; - copyBytes := run.RunBytes - skipBytes; - if copyBytes > remaining then - copyBytes := remaining; - - if (copyBytes = 0) or ((copyBytes mod secSize) <> 0) or ((skipBytes mod secSize) <> 0) then - exit; - - sectorLBA := run.LBA + (skipBytes div secSize); - chunkSectors := copyBytes div secSize; - ctx^.ChunkBytes := copyBytes; - - lastBytePos := skipBytes + copyBytes - 1; - lastClusterIdx := startClusterIdx + (lastBytePos div bytesPerCluster); - lastClusterUsed := run.StartCluster + (lastBytePos div bytesPerCluster); - updateSIOC(ctx^.Volume, ctx^.Cluster, lastClusterIdx, lastClusterUsed); - ctx^.CursorValid := true; - ctx^.CursorIndex := lastClusterIdx; - ctx^.CursorCluster := lastClusterUsed; - - fat32_fileio_prepare_single_run := true; -end; - -procedure fat32_fileio_direct_complete(error : TError; userdata : pointer); -var - ctx : PFATFileIOAsyncCtx; -begin - ctx := PFATFileIOAsyncCtx(userdata); - if ctx = nil then exit; - - if error = eNone then begin - ctx^.BytesDone := ctx^.BytesDone + ctx^.ChunkBytes; - if (ctx^.Mode = famWrite) and (ctx^.OpenFile <> nil) then begin - if (ctx^.BytesDone > 0) and ((ctx^.Offset + ctx^.BytesDone) > ctx^.OpenFile^.ByteSize) then begin - ctx^.OpenFile^.ByteSize := ctx^.Offset + ctx^.BytesDone; - ctx^.OpenFile^.MetaDirty := true; - end; - if (ctx^.OpenFile^.FirstCluster = 0) and (ctx^.Cluster <> 0) then begin - ctx^.OpenFile^.FirstCluster := ctx^.Cluster; - ctx^.OpenFile^.Exists := true; - ctx^.OpenFile^.MetaDirty := true; - end; - end; - end; - - fat32_fileio_async_complete(ctx, error); -end; - -procedure fat32_fileio_async_step(error : TError; userdata : pointer); forward; - -procedure fat32_fileio_async_run_next(ctx : PFATFileIOAsyncCtx); -const - MAX_RUN_SECTORS = 2048; -var - run : TRunInfo; - curCluster : uint32; - chainPos : uint32; - currentOffset : uint32; - bytesPerCluster : uint32; - startClusterIdx : uint32; - inClusterOffset : uint32; - remaining : uint32; - skipBytes : uint32; - copyBytes : uint32; - secSize : uint32; - spc : uint32; - sectorLBA : uint32; - chunkSectors : uint32; - lastBytePos : uint32; - lastClusterUsed : uint32; - lastClusterIdx : uint32; - maxRunClusters : uint32; - extentIdx : uint32; - extentStartIdx : uint32; - err : TError; -begin - if ctx = nil then exit; - - if ctx^.State = ftsAwaitIO then begin - if ctx^.LastError <> eNone then begin - ctx^.State := ftsError; - fat32_fileio_async_complete(ctx, ctx^.LastError); - exit; - end; - - ctx^.State := ftsAdvance; - ctx^.BytesDone := ctx^.BytesDone + ctx^.ChunkBytes; - - if (ctx^.Mode = famWrite) and (ctx^.OpenFile <> nil) then begin - if (ctx^.BytesDone > 0) and ((ctx^.Offset + ctx^.BytesDone) > ctx^.OpenFile^.ByteSize) then begin - ctx^.OpenFile^.ByteSize := ctx^.Offset + ctx^.BytesDone; - ctx^.OpenFile^.MetaDirty := true; - end; - if (ctx^.OpenFile^.FirstCluster = 0) and (ctx^.Cluster <> 0) then begin - ctx^.OpenFile^.FirstCluster := ctx^.Cluster; - ctx^.OpenFile^.Exists := true; - ctx^.OpenFile^.MetaDirty := true; - end; - end; - end; - - if ctx^.BytesDone >= ctx^.ByteCount then begin - ctx^.State := ftsComplete; - fat32_fileio_async_complete(ctx, eNone); - exit; - end; - - ctx^.State := ftsPrepare; - - currentOffset := ctx^.Offset + ctx^.BytesDone; - remaining := ctx^.ByteCount - ctx^.BytesDone; - spc := uint32(ctx^.BootRecord^.spc); - secSize := uint32(ctx^.BootRecord^.sectorSize); - bytesPerCluster := spc * secSize; - startClusterIdx := currentOffset div bytesPerCluster; - inClusterOffset := currentOffset mod bytesPerCluster; - - maxRunClusters := MAX_RUN_SECTORS div spc; - if maxRunClusters = 0 then - maxRunClusters := 1; - - if (ctx^.OpenFile <> nil) and (ctx^.OpenFile^.Extents <> nil) and - fatLocateExtentRun(ctx^.OpenFile^.Extents, startClusterIdx, - ctx^.ExtentValid, ctx^.ExtentIdx, ctx^.ExtentStartIdx, - run.StartCluster, run.RunLen, extentIdx, extentStartIdx) then begin - if run.RunLen > maxRunClusters then - run.RunLen := maxRunClusters; - run.LastCluster := run.StartCluster + run.RunLen - 1; - ctx^.ExtentValid := true; - ctx^.ExtentIdx := extentIdx; - ctx^.ExtentStartIdx := extentStartIdx; - curCluster := run.LastCluster; - chainPos := startClusterIdx + run.RunLen - 1; - end else begin - if not seekToClusterIndexHinted( - ctx^.Volume, ctx^.Cluster, startClusterIdx, ctx^.BootRecord, - ctx^.CursorValid, ctx^.CursorIndex, ctx^.CursorCluster, - curCluster, chainPos) then begin - ctx^.State := ftsError; - fat32_fileio_async_complete(ctx, eCorruptFilesystem); - exit; - end; - - run.StartCluster := curCluster; - buildContiguousRun(ctx^.Volume, curCluster, spc, MAX_RUN_SECTORS, ctx^.BootRecord, run.RunLen, run.LastCluster); - end; - run.RunSectors := run.RunLen * spc; - run.RunBytes := run.RunSectors * secSize; - run.LBA := clusterToLBA(ctx^.DataStart, spc, run.StartCluster); - - skipBytes := inClusterOffset; - copyBytes := run.RunBytes - skipBytes; - if copyBytes > remaining then - copyBytes := remaining; - - if (copyBytes = 0) or ((copyBytes mod secSize) <> 0) or ((skipBytes mod secSize) <> 0) then begin - ctx^.State := ftsError; - fat32_fileio_async_complete(ctx, eInvalidArgument); - exit; - end; - - sectorLBA := run.LBA + (skipBytes div secSize); - chunkSectors := copyBytes div secSize; - ctx^.ChunkBytes := copyBytes; - - lastBytePos := skipBytes + copyBytes - 1; - lastClusterIdx := startClusterIdx + (lastBytePos div bytesPerCluster); - lastClusterUsed := run.StartCluster + (lastBytePos div bytesPerCluster); - updateSIOC(ctx^.Volume, ctx^.Cluster, lastClusterIdx, lastClusterUsed); - ctx^.CursorValid := true; - ctx^.CursorIndex := lastClusterIdx; - ctx^.CursorCluster := lastClusterUsed; - - ctx^.State := ftsIssueIO; - - if ctx^.Mode = famRead then - err := driver.storage.mgr.storage_read_async( - ctx^.Volume^.device, sectorLBA, chunkSectors, - puint32(uint32(ctx^.Buffer) + ctx^.BytesDone), @fat32_fileio_async_step, ctx) - else - err := driver.storage.mgr.storage_write_async( - ctx^.Volume^.device, sectorLBA, chunkSectors, - puint32(uint32(ctx^.Buffer) + ctx^.BytesDone), @fat32_fileio_async_step, ctx); - - if err = eNone then - ctx^.State := ftsAwaitIO - else - ctx^.State := ftsError; - if err <> eNone then - fat32_fileio_async_complete(ctx, err); -end; - -procedure fat32_fileio_async_step(error : TError; userdata : pointer); -var - ctx : PFATFileIOAsyncCtx; -begin - ctx := PFATFileIOAsyncCtx(userdata); - if ctx = nil then exit; - ctx^.LastError := error; - if not fatScheduleTransfer(ctx) then begin - ctx^.State := ftsError; - fat32_fileio_async_complete(ctx, eQueueFull); - end; -end; - -{ Update the SIOC chain-position cache. } -procedure updateSIOC( - volume : PStorage_Volume; - fileStartCluster : uint32; - chainIdx : uint32; - chainCluster : uint32 -); -begin - sioc_vol := volume; - sioc_fileClust := fileStartCluster; - sioc_chainIdx := chainIdx; - sioc_chainClust := chainCluster; - sioc_valid := true; -end; - -{ Seek to a specific cluster index in a file's FAT chain. - Uses the SIOC cache to resume from a previously cached position - when possible, otherwise walks from the file's start cluster. - Returns true on success, false if the chain ended prematurely. } -function seekToClusterIndex( - volume : PStorage_Volume; - fileStartCluster : uint32; - targetIdx : uint32; - bootRecord : PBootRecord; - var curCluster : uint32; - var chainPos : uint32 -) : boolean; -var - nextFat : uint32; - maxCluster : uint32; -begin - seekToClusterIndex := false; - if bootRecord^.sectorSize > 0 then - maxCluster := (bootRecord^.FATSize * bootRecord^.sectorSize) div 4 - else - maxCluster := 0; - - { Resume from SIOC cache if it covers this file at a useful position } - if sioc_valid and (sioc_vol = volume) and (sioc_fileClust = fileStartCluster) - and (sioc_chainIdx <= targetIdx) then begin - curCluster := sioc_chainClust; - chainPos := sioc_chainIdx; - end else begin - curCluster := fileStartCluster; - chainPos := 0; - end; - - { Walk forward to targetIdx } - while chainPos < targetIdx do begin - if (maxCluster > 0) and (chainPos >= maxCluster) then - exit; - nextFat := readFat(volume, curCluster, bootRecord); - if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then - exit; - curCluster := nextFat; - chainPos := chainPos + 1; - end; - - seekToClusterIndex := true; -end; - -function seekToClusterIndexHinted( - volume : PStorage_Volume; - fileStartCluster : uint32; - targetIdx : uint32; - bootRecord : PBootRecord; - hintValid : boolean; - hintIdx : uint32; - hintCluster : uint32; - var curCluster : uint32; - var chainPos : uint32 -) : boolean; -var - nextFat : uint32; - maxCluster : uint32; -begin - seekToClusterIndexHinted := false; - if bootRecord^.sectorSize > 0 then - maxCluster := (bootRecord^.FATSize * bootRecord^.sectorSize) div 4 - else - maxCluster := 0; - - if hintValid and (hintIdx <= targetIdx) then begin - curCluster := hintCluster; - chainPos := hintIdx; - end else begin - if not seekToClusterIndex(volume, fileStartCluster, targetIdx, bootRecord, curCluster, chainPos) then - exit; - seekToClusterIndexHinted := true; - exit; - end; - - while chainPos < targetIdx do begin - if (maxCluster > 0) and (chainPos >= maxCluster) then - exit; - nextFat := readFat(volume, curCluster, bootRecord); - if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then - exit; - curCluster := nextFat; - chainPos := chainPos + 1; - end; - - seekToClusterIndexHinted := true; -end; - -//TODO add optional attributes flag to refine what i return -function getDirEntries(volume : PStorage_volume; cluster : uint32; bootRecord : PBootRecord) : PLinkedListBase; -var - buffer : puint32; - bufferI : puint32; - clusters : PLinkedListBase; - directories : PLinkedListBase; - i : uint32 = 0; - maxEntries : uint32; - datastart : uint32; - sectorLocation : uint32; - dirElm : puint32; -begin - push_trace('driver.storage.fs.fat32.getDirEntries.enter'); - directories:= LL_New(sizeof(TDirectory)); - - clusters:= PLinkedListBase(getFatChain(volume, cluster, bootRecord)); - if (clusters = nil) or (LL_size(clusters) = 0) then begin - getDirEntries := directories; - if clusters <> nil then LL_Free(clusters); - push_trace('driver.storage.fs.fat32.getDirEntries.exit'); - exit; - end; - push_trace('driver.storage.fs.fat32.getDirEntries.allocBuffer'); - buffer:= puint32(kalloc( (bootRecord^.sectorSize * bootRecord^.spc) * LL_size(clusters) )); - memset(uint32(buffer), 0, (bootRecord^.sectorSize * bootRecord^.spc) * LL_size(clusters) ); - - dataStart:= fatDataStartLBA(volume, bootRecord); - - push_trace('driver.storage.fs.fat32.getDirEntries.readSectors'); - for i:=0 to LL_size(clusters) - 1 do begin - sectorLocation:= clusterToLBA(dataStart, bootRecord^.spc, puint32(LL_Get(clusters, i))^); - bufferI:= puint32(uint32(buffer) + uint32(i * bootRecord^.spc * bootRecord^.sectorSize)); - driver.storage.mgr.storage_read(volume^.device, sectorLocation, bootRecord^.spc, bufferI); - end; - - i:=0; - maxEntries:= ((bootRecord^.sectorSize * bootRecord^.spc) * LL_size(clusters)) div sizeof(TDirectory); - while i < maxEntries do begin - if PDirectory(buffer)[i].fileName[0] = char(0) then break; - - { Skip deleted entries ($E5 = FAT32 deleted marker) } - if PDirectory(buffer)[i].fileName[0] = char($E5) then begin - i+=1; - continue; - end; - - dirElm:= LL_Add(directories); - PDirectory(dirElm)^:= PDirectory(buffer)[i]; - i+=1; - end; - - getDirEntries:= directories; //get last . - LL_Free(clusters); - kfree(buffer); - push_trace('driver.storage.fs.fat32.getDirEntries.exit'); -end; - -function fat2GenericEntries(list : PLinkedListBase) : PLinkedListBase; -var - i : uint32; - entry : PDirectory_Entry; - dir : PDirectory; - dirElm : puint32; - nameBuf : pchar; - nameLen : uint32; - extLen : uint32; -begin - push_trace('driver.storage.fs.fat32.fat2GenericEntries.enter'); - puint32(entry) := kalloc(sizeof(TDirectory_Entry)); - fat2GenericEntries:= LL_New(sizeof(TDirectory_Entry)); - - if LL_size(list) > 0 then begin - - for i:= 0 to LL_Size(list) - 1 do begin - dir := PDirectory(LL_get(list, i)); - - { Skip deleted entries (should already be filtered by getDirEntries, but guard here too) } - if dir^.fileName[0] = char($E5) then continue; - - { Trim trailing spaces from fileName (8 bytes) } - nameLen := 8; - while (nameLen > 0) and (dir^.fileName[nameLen - 1] = ' ') do - nameLen := nameLen - 1; - - { Trim trailing spaces from extension (3 bytes) } - extLen := 3; - while (extLen > 0) and (dir^.fileExtension[extLen - 1] = ' ') do - extLen := extLen - 1; - - { Build combined filename: NAME.EXT or just NAME } - if extLen > 0 then begin - nameBuf := pchar(kalloc(nameLen + 1 + extLen + 1)); - memcpy(uint32(@dir^.fileName[0]), uint32(nameBuf), nameLen); - nameBuf[nameLen] := '.'; - memcpy(uint32(@dir^.fileExtension[0]), uint32(@nameBuf[nameLen + 1]), extLen); - nameBuf[nameLen + 1 + extLen] := char(0); - end else begin - nameBuf := pchar(kalloc(nameLen + 1)); - memcpy(uint32(@dir^.fileName[0]), uint32(nameBuf), nameLen); - nameBuf[nameLen] := char(0); - end; - - entry^.fileName := nameBuf; - - if dir^.attributes = $10 then begin - entry^.entryType:= TDirectory_Entry_Type.directoryEntry; - end else begin - entry^.entryType:= TDirectory_Entry_Type.fileEntry; - end; - - entry^.fileSize := dir^.byteSize; - entry^.modifiedDate := dir^.modifiedDate; - entry^.modifiedTime := dir^.modifiedTime; - entry^.attributes := dir^.attributes; - - //add to list - dirElm:= LL_add(fat2GenericEntries); - PDirectory_Entry(dirElm)^:= entry^; - end; - end; - - LL_Free(list); - kfree(puint32(entry)); - push_trace('driver.storage.fs.fat32.fat2GenericEntries.exit'); -end; - -//need to find out why having multiple dir stings isn't working, maybe the ls command? did I fix this? -function readDirectory(volume : PStorage_volume; directory : pchar; statusOut : puint32) : PLinkedListBase; //statusout: 0 = success, 1 = dir not exsist, 2 = not directory, 3 = invalid name, 4= already exists -var - bootRecord : PBootRecord; - directoryStrings : PLinkedListBase; - directories : PLinkedListBase; - cluster : uint32; - i : uint32; - ii : uint32 = 0; - dirEntry : PDirectory; - status : puint32; -begin - status:= puint32(kalloc(sizeof(uint32))); - push_trace('driver.storage.fs.fat32.readDirectory.enter'); - status^:= ord(eNone); - bootRecord:= readBootRecord(volume); - directoryStrings:= LL_fromString(directory, '/'); - directories:= getDirEntries(volume, bootRecord^.rootCluster, bootRecord); - - if LL_size(directoryStrings) > 0 then begin - for i:=0 to (LL_Size(directoryStrings) ) do begin /// maybe -1 will work - ii:=0; - - while true do begin - if ii > LL_Size(directories) - 1 then begin - status^:= ord(eDirectoryDoesNotExist); - break; - end; - dirEntry:= PDirectory(LL_Get(directories, ii)); - - if compareByteArray8( dirEntry^.fileName, cleanString( pchar(puint32(LL_Get(directoryStrings, i))^), status)) then begin - cluster:= dirFirstCluster(dirEntry); - break; - end; - ii+=1; - end; - - if status^ <> 0 then break; - - LL_Free(directories); - directories:= getDirEntries(volume, cluster, bootRecord); - - if i = LL_Size(directoryStrings) - 1 then break; - end; - end else begin - while true do begin - if ii > LL_Size(directories) - 1 then break; - dirEntry:= PDirectory(LL_Get(directories, ii)); - ii+=1; - end; - end; - - readDirectory:= directories; - - if statusOut <> nil then statusOut^ := status^; - kfree(status); - LL_Free(directoryStrings); - kfree(puint32(bootRecord)); - push_trace('driver.storage.fs.fat32.readDirectory.exit'); -end; - -function readDirectoryGen(volume : PStorage_volume; directory : pchar; status : puint32) : PLinkedListBase; //returns: 0 = success, 1 = dir not exsist, 2 = not directory, 3 = invalid name, 4= already exists -begin - push_trace('driver.storage.fs.fat32.readDirectoryGen.enter'); - readDirectoryGen:= fat2GenericEntries(readDirectory(volume, directory, status)); - push_trace('driver.storage.fs.fat32.readDirectoryGen.exit'); -end; - -//need to allow for setting file extension -function writeDirectory(volume : PStorage_volume; directory : pchar; dirName : pchar; attributes : uint32; statusOut : puint32) : uint32; // need to handle parent table cluster overflow, need to take attributes -var - directories : PLinkedListBase; - parentDirectory : PDirectory; - parentCluster : uint32; - extents : PLinkedListBase; - extent : PClusterExtent; - cluster : uint32; - bootRecord : PBootRecord; - buffer : puint32; - bufferPointer : PDirectory; - dataStart : uint32; - EntriesPerSector : uint32; - sectorLocation : uint32; - dataOffset : uint32; - i : uint32; - - thisArray : byteArray8 = ('.',' ',' ',' ',' ',' ',' ',' '); - parentArray : byteArray8 = ('.','.',' ',' ',' ',' ',' ',' '); - status : puint32; - - namePart : pchar; - extPart : pchar; -begin - push_trace('driver.storage.fs.fat32.writeDirectory.enter'); - io.syslog.logln('FAT32', 'writeDirectory: enter'); - writeDirectory := 0; - status:= puint32(kalloc(sizeof(uint32))); - status^:= ord(eNone); - - { Validate the directory name as a valid FAT32 8.3 name } - if not isValidFAT32Name(dirName) then begin - io.syslog.logln('FAT32', 'writeDirectory: invalid FAT32 name, aborting'); - statusOut^:= ord(eInvalidFileName); - kfree(status); - exit; - end; - - push_trace('driver.storage.fs.fat32.writeDirectory.readDir'); - directories:= readDirectory(volume, directory, status); - - { If the parent directory does not exist, bail out } - if status^ <> ord(eNone) then begin - io.syslog.logln('FAT32', 'writeDirectory: parent directory not found'); - statusOut^:= status^; - kfree(status); - LL_Free(directories); - exit; - end; - - { Split dirName for duplicate checking } - splitFileNameParts(dirName, namePart, extPart); - - if(LL_size(directories) > 1) then begin - for i:=0 to LL_Size(directories) - 1 do begin - if compareByteArray8(PDirectory(LL_get(directories, i))^.fileName, cleanString( namePart , status)) - and matchExtension(PDirectory(LL_get(directories, i))^.fileExtension, extPart) then begin - status^:= ord(eDirectoryAlreadyExists); - io.syslog.logln('FAT32', 'writeDirectory: entry already exists'); - end; - end; - end; - - kfree(void(namePart)); - kfree(void(extPart)); - - bootRecord:= readBootRecord(volume); - datastart:= fatDataStartLBA(volume, bootRecord); - - if status^ = ord(eNone) then begin - push_trace('driver.storage.fs.fat32.writeDirectory.createEntry'); - - { Check if directory sector is full } - EntriesPerSector:= uint32(bootRecord^.sectorSize) div uint32(sizeof(TDirectory)); - if LL_size(directories) >= EntriesPerSector then begin - { Current sector is full — would need cluster chain extension (not yet supported) } - io.syslog.logln('FAT32', 'writeDirectory: directory sector full'); - statusOut^:= ord(eDirectoryFull); - kfree(status); - kfree(puint32(bootRecord)); - LL_Free(directories); - exit; - end; - - parentDirectory:= PDirectory(LL_Get(directories, 0)); - parentCluster:= dirFirstCluster(parentDirectory); - - extents := findFreeClusterExtents(volume, 1, bootRecord); - if extents = nil then begin - { No free clusters available — disk is full } - io.syslog.logln('FAT32', 'writeDirectory: disk full'); - statusOut^:= ord(eDiskFull); - kfree(status); - kfree(puint32(bootRecord)); - LL_Free(directories); - exit; - end; - extent := PClusterExtent(LL_Get(extents, 0)); - cluster := extent^.StartCluster; - io.syslog.logln('FAT32', 'writeDirectory: allocated cluster'); - LL_Free(extents); - buffer:= puint32(kalloc(bootRecord^.sectorSize)); - - if attributes = $10 then begin // if directory - - memset(uint32(buffer), 0, bootRecord^.sectorSize); - - bufferPointer:= @PDirectory(buffer)[0]; - bufferPointer^.fileName:= thisArray; //TODO implement time - bufferPointer^.attributes:= attributes; - setDirFirstCluster(bufferPointer, cluster); - - bufferPointer:= @PDirectory(buffer)[1]; - bufferPointer^.fileName:= parentArray; //TODO implement time - bufferPointer^.attributes:= attributes; - setDirFirstCluster(bufferPointer, parentCluster); - - //write to disk - driver.storage.mgr.storage_write(volume^.device, clusterToLBA(dataStart, bootRecord^.spc, cluster), 1, buffer); - - //write fat - writeFat(volume, cluster, $FFFFFFF8, bootRecord); - end; - memset(uint32(buffer), 0, bootRecord^.sectorSize); - //calculate write cluster using directories and parentCluster - sectorLocation:= LL_size(directories) * sizeof(TDirectory) div bootRecord^.sectorSize; - - //dataOffset:= datastart + ( (LL_size(directories) * sizeof(PDirectory)) - (sizeUsed * bootRecord^.sectorSize)); - driver.storage.mgr.storage_read(volume^.device, - clusterToLBA(dataStart, bootRecord^.spc, parentCluster) + sectorLocation, 1, buffer); - - //construct my dir entry - EntriesPerSector:= uint32(bootRecord^.sectorSize) div uint32(sizeof(TDirectory)); - bufferPointer:= @PDirectory(buffer)[LL_size(directories) - ((LL_size(directories) div EntriesPerSector) * EntriesPerSector)]; - - { Split dirName into name + extension parts for FAT32 8.3 format } - splitFileNameParts(dirName, namePart, extPart); - bufferPointer^.fileName:= cleanString(namePart, status); - bufferPointer^.attributes:= attributes; - if attributes = 0 then begin - { Copy extension characters for files } - fillFatExt(bufferPointer^.fileExtension, extPart); - end else begin - fillFatExt(bufferPointer^.fileExtension, nil); - end; - kfree(void(namePart)); - kfree(void(extPart)); - setDirFirstCluster(bufferPointer, cluster); - - writeDirectory:= cluster; - - push_trace('driver.storage.fs.fat32.writeDirectory.writeToDisk'); - //write to disk - driver.storage.mgr.storage_write(volume^.device, - clusterToLBA(dataStart, bootRecord^.spc, parentCluster) + sectorLocation, 1, buffer); - io.syslog.logln('FAT32', 'writeDirectory: entry written to disk'); - kfree(buffer); - end; - - statusOut^:= status^; - kfree(status); - kfree(puint32(bootRecord)); - LL_Free(directories); - io.syslog.logln('FAT32', 'writeDirectory: exit'); - -end; - -procedure writeDirectoryGen(volume : PStorage_volume; directory : pchar; dirName : pchar; attributes : uint32; statusOut : puint32); // need to handle parent table cluster overflow, need to take attributes -begin - push_trace('driver.storage.fs.fat32.writeDirectoryGen.enter'); - writeDirectory(volume, directory, dirName, attributes, statusOut); - push_trace('driver.storage.fs.fat32.writeDirectoryGen.exit'); -end; - -{ Split a full path like 'SYSTEM/FILE.TXT' into parent dir ('SYSTEM') and name ('FILE.TXT'). - If no separator, parentOut is empty string and nameOut is the whole path. - Caller must free parentOut and nameOut. } -procedure splitPathParts(fullPath : pchar; var parentOut : pchar; var nameOut : pchar); -var - pathLen, lastSlash, i : uint32; -begin - parentOut := nil; - nameOut := nil; - if fullPath = nil then begin - parentOut := stringNew(0); - nameOut := stringNew(0); - exit; - end; - pathLen := stringSize(fullPath); - if pathLen = 0 then begin - parentOut := stringNew(0); - nameOut := stringNew(0); - exit; - end; - - { Find last '/' separator } - lastSlash := pathLen; { sentinel: no slash found } - for i := 0 to pathLen - 1 do begin - if fullPath[i] = '/' then - lastSlash := i; - end; - - if lastSlash < pathLen then begin - parentOut := stringTrim(fullPath, lastSlash); - nameOut := stringCopy(pchar(@fullPath[lastSlash + 1])); - end else begin - parentOut := stringNew(0); - nameOut := stringCopy(fullPath); - end; -end; - -{ Free a FAT chain starting from the given cluster — marks all clusters as free (0). - Includes loop protection: aborts if iteration count exceeds maxCluster or cluster - is out of range (< 2 or >= maxCluster). } -procedure freeFatChain(volume : PStorage_volume; startCluster : uint32; bootRecord : PBootRecord); -var - currentCluster : uint32; - nextCluster : uint32; - maxCluster : uint32; - iter : uint32; -begin - push_trace('driver.storage.fs.fat32.freeFatChain.enter'); - if (bootRecord^.FATSize > 0) and (bootRecord^.sectorsize > 0) then - maxCluster := (bootRecord^.FATSize * bootRecord^.sectorsize) div 4 - else - maxCluster := 0; - currentCluster := startCluster; - iter := 0; - while true do begin - if (currentCluster < 2) or (currentCluster >= maxCluster) then begin - io.syslog.logln('FAT32', 'freeFatChain: cluster out of range, aborting'); - break; - end; - if iter >= maxCluster then begin - io.syslog.logln('FAT32', 'freeFatChain: iteration limit hit, possible cycle'); - break; - end; - nextCluster := readFat(volume, currentCluster, bootRecord); - writeFat(volume, currentCluster, 0, bootRecord); - iter := iter + 1; - - if (nextCluster and $0FFFFFFF) >= $0FFFFFF8 then break; { end of chain } - if (nextCluster and $0FFFFFFF) = $0FFFFFF7 then break; { bad cluster } - if nextCluster = 0 then break; { already free } - - currentCluster := nextCluster; - end; - push_trace('driver.storage.fs.fat32.freeFatChain.exit'); -end; - -{ Resolve a full path into parentCluster + leafName. - Splits the path, reads the parent directory, and returns - the parent cluster. On failure sets statusOut and returns false. - Caller must kfree parentDir and leafName when done. - bootRecord must already be read by the caller. } -function resolveParentCluster( - volume : PStorage_Volume; - path : pchar; - bootRecord : PBootRecord; - var parentDir : pchar; - var leafName : pchar; - var parentClust : uint32; - var statusOut : uint32 -) : boolean; -var - directories : PLinkedListBase; - parentEntry : PDirectory; - status : puint32; -begin - resolveParentCluster := false; - splitPathParts(path, parentDir, leafName); - - if (leafName = nil) or (stringSize(leafName) = 0) then begin - statusOut := ord(eInvalidFileName); - exit; - end; - - if (parentDir = nil) or (stringSize(parentDir) = 0) then begin - parentClust := bootRecord^.rootCluster; - resolveParentCluster := true; - exit; - end; - - status := puint32(kalloc(4)); - status^ := ord(eNone); - directories := readDirectory(volume, parentDir, status); - if status^ <> ord(eNone) then begin - statusOut := status^; - LL_Free(directories); - kfree(status); - exit; - end; - if LL_size(directories) < 1 then begin - statusOut := ord(eDirectoryDoesNotExist); - LL_Free(directories); - kfree(status); - exit; - end; - parentEntry := PDirectory(LL_Get(directories, 0)); - parentClust := dirFirstCluster(parentEntry); - LL_Free(directories); - kfree(status); - resolveParentCluster := true; -end; - -{ Delete a file from the volume. filePath is relative to volume root, e.g. 'SYSTEM/FILE.TXT' or 'FILE.TXT' } -procedure deleteFile(volume : PStorage_Volume; filePath : pchar; statusOut : puint32); -var - parentDir : pchar; - fileName : pchar; - namePart : pchar; - extPart : pchar; - bootRecord : PBootRecord; - parentCluster : uint32; - status : puint32; - buffer : puint32; - dir : PDirectory; - cluster : uint32; - cleanFileName : byteArray8; - loc : TDirEntryLocation; - resolveStatus : uint32; -begin - push_trace('driver.storage.fs.fat32.deleteFile.enter'); - io.syslog.logln('FAT32', 'deleteFile: enter'); - status := puint32(kalloc(4)); - status^ := ord(eNone); - - bootRecord := readBootRecord(volume); - - { Resolve parent directory + leaf name from full path } - resolveStatus := ord(eNone); - if not resolveParentCluster(volume, filePath, bootRecord, parentDir, fileName, parentCluster, resolveStatus) then begin - io.syslog.logln('FAT32', 'deleteFile: resolve failed'); - if statusOut <> nil then statusOut^ := resolveStatus; - kfree(status); - kfree(puint32(bootRecord)); - if parentDir <> nil then kfree(void(parentDir)); - if fileName <> nil then kfree(void(fileName)); - exit; - end; - - { Split filename for 8.3 matching } - splitFileNameParts(fileName, namePart, extPart); - cleanFileName := cleanString(namePart, status); - - { Locate target entry in parent directory } - buffer := puint32(kalloc(bootRecord^.sectorSize)); - if not locateDirEntry(volume, parentCluster, bootRecord, cleanFileName, extPart, loc, buffer) then begin - io.syslog.logln('FAT32', 'deleteFile: file not found'); - if statusOut <> nil then statusOut^ := ord(eFileDoesNotExist); - kfree(buffer); - kfree(void(namePart)); - kfree(void(extPart)); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(fileName)); - exit; - end; - - dir := @PDirectory(buffer)[loc.EntryIdx]; - - { Don't allow deleting directories via deleteFile } - if (dir^.attributes and $10) = $10 then begin - io.syslog.logln('FAT32', 'deleteFile: target is a directory, not a file'); - if statusOut <> nil then statusOut^ := ord(eNotADirectory); - kfree(buffer); - kfree(void(namePart)); - kfree(void(extPart)); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(fileName)); - exit; - end; - - cluster := dirFirstCluster(dir); - - { Free the FAT chain for this file } - io.syslog.logln('FAT32', 'deleteFile: freeing FAT chain'); - if cluster >= 2 then - freeFatChain(volume, cluster, bootRecord); - - { Mark entry deleted and write back } - dir^.fileName[0] := char($E5); - driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, buffer); - - if statusOut <> nil then statusOut^ := ord(eNone); - - kfree(buffer); - kfree(void(namePart)); - kfree(void(extPart)); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(fileName)); - push_trace('driver.storage.fs.fat32.deleteFile.exit'); - io.syslog.logln('FAT32', 'deleteFile: done'); -end; - -{ Delete a directory from the volume. path is relative to volume root. Directory must be empty. } -procedure deleteDir(volume : PStorage_Volume; path : pchar; statusOut : puint32); -var - parentDir : pchar; - dirName : pchar; - namePart : pchar; - extPart : pchar; - bootRecord : PBootRecord; - childDirs : PLinkedListBase; - parentCluster : uint32; - status : puint32; - buffer : puint32; - dir : PDirectory; - cluster : uint32; - childCount : uint32; - cleanFileName : byteArray8; - loc : TDirEntryLocation; - resolveStatus : uint32; -begin - push_trace('driver.storage.fs.fat32.deleteDir.enter'); - io.syslog.logln('FAT32', 'deleteDir: enter'); - status := puint32(kalloc(4)); - status^ := ord(eNone); - - bootRecord := readBootRecord(volume); - - { Resolve parent directory + leaf name from full path } - resolveStatus := ord(eNone); - if not resolveParentCluster(volume, path, bootRecord, parentDir, dirName, parentCluster, resolveStatus) then begin - io.syslog.logln('FAT32', 'deleteDir: resolve failed'); - if statusOut <> nil then statusOut^ := resolveStatus; - kfree(status); - kfree(puint32(bootRecord)); - if parentDir <> nil then kfree(void(parentDir)); - if dirName <> nil then kfree(void(dirName)); - exit; - end; - - { Split dirName for 8.3 matching } - splitFileNameParts(dirName, namePart, extPart); - cleanFileName := cleanString(namePart, status); - - { Locate target entry in parent directory } - buffer := puint32(kalloc(bootRecord^.sectorSize)); - if not locateDirEntry(volume, parentCluster, bootRecord, cleanFileName, extPart, loc, buffer) then begin - io.syslog.logln('FAT32', 'deleteDir: directory not found'); - if statusOut <> nil then statusOut^ := ord(eDirectoryDoesNotExist); - kfree(buffer); - kfree(void(namePart)); - kfree(void(extPart)); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(dirName)); - exit; - end; - - dir := @PDirectory(buffer)[loc.EntryIdx]; - - { Must be a directory } - if (dir^.attributes and $10) <> $10 then begin - if statusOut <> nil then statusOut^ := ord(eNotADirectory); - kfree(buffer); - kfree(void(namePart)); - kfree(void(extPart)); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(dirName)); - exit; - end; - - cluster := dirFirstCluster(dir); - - { Check if directory is empty — should contain only '.' and '..' } - childDirs := getDirEntries(volume, cluster, bootRecord); - childCount := LL_size(childDirs); - LL_Free(childDirs); - - if childCount > 2 then begin - io.syslog.logln('FAT32', 'deleteDir: directory not empty, refusing to delete'); - if statusOut <> nil then statusOut^ := ord(eDirectoryNotEmpty); - kfree(buffer); - kfree(void(namePart)); - kfree(void(extPart)); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(dirName)); - exit; - end; - - { Free the FAT chain for this directory's data } - if cluster >= 2 then - freeFatChain(volume, cluster, bootRecord); - - { Mark entry deleted and write back } - dir^.fileName[0] := char($E5); - driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, buffer); - - if statusOut <> nil then statusOut^ := ord(eNone); - - kfree(buffer); - kfree(void(namePart)); - kfree(void(extPart)); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(dirName)); - push_trace('driver.storage.fs.fat32.deleteDir.exit'); - io.syslog.logln('FAT32', 'deleteDir: done'); -end; - -{ Rename a file or directory on disk. filePath is the current relative path, - newName is the new leaf name (e.g. 'NEWFILE.TXT'). Only changes the 8.3 - directory entry in-place — does not move between directories. } -procedure renameFile(volume : PStorage_Volume; filePath : pchar; newName : pchar; statusOut : puint32); -var - parentDir : pchar; - fileName : pchar; - namePart : pchar; - extPart : pchar; - newNamePart : pchar; - newExtPart : pchar; - bootRecord : PBootRecord; - parentCluster : uint32; - j : uint32; - status : puint32; - buffer : puint32; - dir : PDirectory; - cleanName : byteArray8; - newCleanName : byteArray8; - loc : TDirEntryLocation; - resolveStatus : uint32; -begin - push_trace('driver.storage.fs.fat32.renameFile.enter'); - io.syslog.logln('FAT32', 'renameFile: enter'); - status := puint32(kalloc(4)); - status^ := ord(eNone); - - bootRecord := readBootRecord(volume); - - { Validate new name before doing any work } - if (newName = nil) or (stringSize(newName) = 0) then begin - io.syslog.logln('FAT32', 'renameFile: invalid new name'); - if statusOut <> nil then statusOut^ := ord(eInvalidFileName); - kfree(status); - kfree(puint32(bootRecord)); - exit; - end; - - { Resolve parent directory + leaf name from full path } - resolveStatus := ord(eNone); - if not resolveParentCluster(volume, filePath, bootRecord, parentDir, fileName, parentCluster, resolveStatus) then begin - io.syslog.logln('FAT32', 'renameFile: resolve failed'); - if statusOut <> nil then statusOut^ := resolveStatus; - kfree(status); - kfree(puint32(bootRecord)); - if parentDir <> nil then kfree(void(parentDir)); - if fileName <> nil then kfree(void(fileName)); - exit; - end; - - { Split current filename for 8.3 matching } - splitFileNameParts(fileName, namePart, extPart); - cleanName := cleanString(namePart, status); - - { Locate target entry in parent directory } - buffer := puint32(kalloc(bootRecord^.sectorSize)); - if not locateDirEntry(volume, parentCluster, bootRecord, cleanName, extPart, loc, buffer) then begin - io.syslog.logln('FAT32', 'renameFile: file not found'); - if statusOut <> nil then statusOut^ := ord(eFileDoesNotExist); - kfree(buffer); - kfree(void(namePart)); - kfree(void(extPart)); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(fileName)); - exit; - end; - - dir := @PDirectory(buffer)[loc.EntryIdx]; - - { Apply new 8.3 name in-place } - splitFileNameParts(newName, newNamePart, newExtPart); - newCleanName := cleanString(newNamePart, status); - for j := 0 to 7 do - dir^.fileName[j] := newCleanName[j]; - fillFatExt(dir^.fileExtension, newExtPart); - kfree(void(newNamePart)); - kfree(void(newExtPart)); - - { Write back this one sector } - driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, buffer); - - if statusOut <> nil then statusOut^ := ord(eNone); - - kfree(buffer); - kfree(void(namePart)); - kfree(void(extPart)); - kfree(status); - kfree(puint32(bootRecord)); - kfree(void(parentDir)); - kfree(void(fileName)); - push_trace('driver.storage.fs.fat32.renameFile.exit'); - io.syslog.logln('FAT32', 'renameFile: done'); -end; - -// function checkExists(volume : PStorage_Volume; directory : pchar; fileName : pchar; fileExtension : pchar; entry : PDirectory_Entry) : uint32; -// var -// bootRecord : PBootRecord; -// directories : PLinkedListBase; -// dir : PDirectory; -// genDir : PDirectory_Entry; -// data : puint32; -// statusOut : puint32; -// i : uint32; -// exists : boolean = false; -// begin -// bootRecord := readBootRecord(volume); -// directories := readDirectory(volume, directory, statusOut); -// datastart:= volume^.sectorStart + 1 + bootRecord^.FATSize + bootRecord^.rsvSectors; - -// for i:=0 to LL_Size(directories) -1 do begin -// dir:= PDirectory(LL_Get(directories, i)); -// if (dir^.fileName = entry^.fileName) and (dir^.fileExtension = entry^.extension) then begin -// exists:= true; -// break; -// end; -// end; - -// PDirectory_Entry := PDirectory_Entry(kalloc(20)); - -// PDirectory_Entry^.fileName := pchar(@dir^.fileName); -// PDirectory_Entry^.extension := pchar(@dir^.fileExtension); - -// end; - -//TODO check directory commands for errors with a clean disk - -{ ========================================================================== } -{ Async format state machine (create_volume_async) } -{ ========================================================================== } - -procedure fmt_step_complete(error : TError; userdata : pointer); forward; - -procedure fmt_run_next(ctx : PFmtContext); -var - writeCount : uint32; -begin - case ctx^.Step of - fmtBootSector: begin - io.syslog.logln('FAT32', 'fmt: fmtBootSector'); - { Write boot sector at partition start } - driver.storage.mgr.storage_write_async(ctx^.Disk, ctx^.SectorStart, 1, - ctx^.Buffer, @fmt_step_complete, pointer(ctx)); - end; - fmtZeroFAT: begin - io.syslog.logln('FAT32', 'fmt: fmtZeroFAT batch'); - { Issue next FAT zero batch } - if ctx^.BatchPos >= ctx^.FATSize then begin - io.syslog.logln('FAT32', 'fmt: FAT zero complete, moving to fmtFATEntries'); - { FAT zeroing complete — move to FAT entries } - kfree(ctx^.ZeroBuffer); - ctx^.ZeroBuffer := nil; - kfree(ctx^.Buffer); - ctx^.Buffer := puint32(kalloc(ctx^.Disk^.sectorSize)); - memset(uint32(ctx^.Buffer), 0, ctx^.Disk^.sectorSize); - puint32(ctx^.Buffer)[0] := $0FFFFFF8; { media type (reserved entry 0) } - puint32(ctx^.Buffer)[1] := $0FFFFFFF; { clean marker (reserved entry 1) } - puint32(ctx^.Buffer)[2] := $0FFFFFF8; { root cluster EOC (entry 2) } - puint32(ctx^.Buffer)[3] := $0FFFFFF8; { SYSTEM dir cluster EOC (entry 3) } - ctx^.Step := fmtFATEntries; - fmt_run_next(ctx); - exit; - end; - if (ctx^.FATSize - ctx^.BatchPos) >= ctx^.BatchSize then - writeCount := ctx^.BatchSize - else - writeCount := ctx^.FATSize - ctx^.BatchPos; - driver.storage.mgr.storage_write_async(ctx^.Disk, - ctx^.FATStart + ctx^.BatchPos, writeCount, ctx^.ZeroBuffer, - @fmt_step_complete, pointer(ctx)); - end; - fmtFATEntries: begin - io.syslog.logln('FAT32', 'fmt: fmtFATEntries'); - { Write FAT entries sector } - driver.storage.mgr.storage_write_async(ctx^.Disk, ctx^.FATStart, 1, - ctx^.Buffer, @fmt_step_complete, pointer(ctx)); - end; - fmtRootDir: begin - io.syslog.logln('FAT32', 'fmt: fmtRootDir'); - { Build root directory: "." + ".." + "SYSTEM" entries } - memset(uint32(ctx^.Buffer), 0, ctx^.Disk^.sectorSize); - { "." entry — volume label } - PDirectory(ctx^.Buffer)[0].fileName[0] := '.'; - PDirectory(ctx^.Buffer)[0].fileName[1] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[2] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[3] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[4] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[5] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[6] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[7] := ' '; - PDirectory(ctx^.Buffer)[0].attributes := $08; - PDirectory(ctx^.Buffer)[0].clusterLow := ctx^.RootCluster; - { ".." entry } - PDirectory(ctx^.Buffer)[1].fileName[0] := '.'; - PDirectory(ctx^.Buffer)[1].fileName[1] := '.'; - PDirectory(ctx^.Buffer)[1].fileName[2] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[3] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[4] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[5] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[6] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[7] := ' '; - PDirectory(ctx^.Buffer)[1].attributes := $10; - PDirectory(ctx^.Buffer)[1].clusterLow := ctx^.RootCluster; - { "SYSTEM" directory entry pointing to cluster 3 } - PDirectory(ctx^.Buffer)[2].fileName[0] := 'S'; - PDirectory(ctx^.Buffer)[2].fileName[1] := 'Y'; - PDirectory(ctx^.Buffer)[2].fileName[2] := 'S'; - PDirectory(ctx^.Buffer)[2].fileName[3] := 'T'; - PDirectory(ctx^.Buffer)[2].fileName[4] := 'E'; - PDirectory(ctx^.Buffer)[2].fileName[5] := 'M'; - PDirectory(ctx^.Buffer)[2].fileName[6] := ' '; - PDirectory(ctx^.Buffer)[2].fileName[7] := ' '; - PDirectory(ctx^.Buffer)[2].attributes := $10; - PDirectory(ctx^.Buffer)[2].clusterLow := 3; - driver.storage.mgr.storage_write_async(ctx^.Disk, - clusterToLBA(ctx^.DataStart, ctx^.SPC, ctx^.RootCluster), 1, ctx^.Buffer, - @fmt_step_complete, pointer(ctx)); - end; - fmtSystemDir: begin - io.syslog.logln('FAT32', 'fmt: fmtSystemDir'); - { Build SYSTEM directory: "." and ".." } - memset(uint32(ctx^.Buffer), 0, ctx^.Disk^.sectorSize); - PDirectory(ctx^.Buffer)[0].fileName[0] := '.'; - PDirectory(ctx^.Buffer)[0].fileName[1] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[2] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[3] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[4] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[5] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[6] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[7] := ' '; - PDirectory(ctx^.Buffer)[0].attributes := $10; - PDirectory(ctx^.Buffer)[0].clusterLow := 3; - PDirectory(ctx^.Buffer)[1].fileName[0] := '.'; - PDirectory(ctx^.Buffer)[1].fileName[1] := '.'; - PDirectory(ctx^.Buffer)[1].fileName[2] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[3] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[4] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[5] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[6] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[7] := ' '; - PDirectory(ctx^.Buffer)[1].attributes := $10; - PDirectory(ctx^.Buffer)[1].clusterLow := ctx^.RootCluster; - driver.storage.mgr.storage_write_async(ctx^.Disk, - clusterToLBA(ctx^.DataStart, ctx^.SPC, 3), 1, ctx^.Buffer, - @fmt_step_complete, pointer(ctx)); - end; - fmtDone: begin - io.syslog.logln('FAT32', 'fmt: fmtDone — format complete'); - { All done — clean up and notify caller } - kfree(ctx^.Buffer); - ctx^.Buffer := nil; - if ctx^.Callback <> nil then - ctx^.Callback(eNone, ctx^.CallbackData); - kfree(puint32(ctx)); - end; - end; -end; - -procedure fmt_step_complete(error : TError; userdata : pointer); -var - ctx : PFmtContext; -begin - ctx := PFmtContext(userdata); - if error <> eNone then begin - io.syslog.logln('FAT32', 'fmt_step_complete: I/O error — aborting format'); - { Error — abort format, clean up, notify caller } - if ctx^.Buffer <> nil then kfree(ctx^.Buffer); - if ctx^.ZeroBuffer <> nil then kfree(ctx^.ZeroBuffer); - if ctx^.Callback <> nil then - ctx^.Callback(error, ctx^.CallbackData); - kfree(puint32(ctx)); - exit; - end; - - { Advance to next step } - case ctx^.Step of - fmtBootSector: begin - ctx^.Step := fmtZeroFAT; - ctx^.BatchPos := 0; - end; - fmtZeroFAT: begin - ctx^.BatchPos := ctx^.BatchPos + ctx^.BatchSize; - { Step stays fmtZeroFAT — fmt_run_next checks if more batches remain } - end; - fmtFATEntries: begin - ctx^.Step := fmtRootDir; - end; - fmtRootDir: begin - ctx^.Step := fmtSystemDir; - end; - fmtSystemDir: begin - ctx^.Step := fmtDone; - end; - end; - fmt_run_next(ctx); -end; - -procedure create_volume_async(volume : PStorage_Volume; sectors : uint32; start : uint32; - config : puint32; callback : TIOCallback; callbackData : pointer); -var - ctx : PFmtContext; - bootRecord : PBootRecord; - spc : uint32; -begin - push_trace('driver.storage.fs.fat32.create_volume_async'); - io.syslog.logln('FAT32', 'create_volume_async: enter'); - - ctx := PFmtContext(kalloc(sizeof(TFmtContext))); - if ctx = nil then begin - io.syslog.logln('FAT32', 'create_volume_async: OOM allocating context'); - if callback <> nil then callback(eOutOfMemory, callbackData); - exit; - end; - - ctx^.Volume := volume; - ctx^.Disk := volume^.device; - ctx^.SectorStart := start; - ctx^.RootCluster := 2; - ctx^.Callback := callback; - ctx^.CallbackData := callbackData; - ctx^.ZeroBuffer := nil; - - if config <> nil then - spc := config^ - else - spc := fatDefaultSPC(sectors, ctx^.Disk^.sectorSize); - if spc = 0 then - spc := fatDefaultSPC(sectors, ctx^.Disk^.sectorSize); - ctx^.SPC := spc; - - ctx^.FATSize := fatComputeFATSize(sectors, spc, ctx^.Disk^.sectorSize); - ctx^.FATStart := start + 32; { boot sector included in reserved sectors } - ctx^.DataStart := ctx^.FATStart + ctx^.FATSize; - - { Allocate boot sector buffer and build boot record } - ctx^.Buffer := puint32(kalloc(ctx^.Disk^.sectorSize + 512)); - if ctx^.Buffer = nil then begin - if callback <> nil then callback(eOutOfMemory, callbackData); - kfree(puint32(ctx)); - exit; - end; - memset(uint32(ctx^.Buffer), 0, ctx^.Disk^.sectorSize); - - bootRecord := PBootRecord(ctx^.Buffer); - bootRecord^.jmp2boot := $0; - bootRecord^.OEMName[0] := 'A'; - bootRecord^.OEMName[1] := 'S'; - bootRecord^.OEMName[2] := 'U'; - bootRecord^.OEMName[3] := 'R'; - bootRecord^.OEMName[4] := 'O'; - bootRecord^.OEMName[5] := ' '; - bootRecord^.OEMName[6] := 'V'; - bootRecord^.OEMName[7] := '1'; - bootRecord^.sectorSize := ctx^.Disk^.sectorSize; - bootRecord^.spc := uint8(spc); - bootRecord^.rsvSectors := 32; - bootRecord^.numFats := 1; - bootRecord^.mediaDescp := $F8; - bootRecord^.hiddenSectors := start; - bootRecord^.manySectors := sectors; - bootRecord^.FATSize := ctx^.FATSize; - bootRecord^.rootCluster := ctx^.RootCluster; - bootRecord^.FSInfoCluster := 0; - bootRecord^.driveNumber := $80; - bootRecord^.volumeID := 62; - bootRecord^.bsignature := $29; - bootRecord^.identString[0] := 'F'; - bootRecord^.identString[1] := 'A'; - bootRecord^.identString[2] := 'T'; - bootRecord^.identString[3] := '3'; - bootRecord^.identString[4] := '2'; - bootRecord^.identString[5] := ' '; - bootRecord^.identString[6] := ' '; - bootRecord^.identString[7] := ' '; - - { Boot sector signature at bytes 510-511 } - puint8(uint32(ctx^.Buffer) + 510)^ := $55; - puint8(uint32(ctx^.Buffer) + 511)^ := $AA; - - { Allocate zero buffer for FAT batches } - if ctx^.FATSize > 128 then - ctx^.BatchSize := 128 - else - ctx^.BatchSize := ctx^.FATSize; - ctx^.BatchPos := 0; - ctx^.ZeroBuffer := puint32(kalloc(ctx^.Disk^.sectorSize * ctx^.BatchSize)); - if ctx^.ZeroBuffer = nil then begin - kfree(ctx^.Buffer); - if callback <> nil then callback(eOutOfMemory, callbackData); - kfree(puint32(ctx)); - exit; - end; - memset(uint32(ctx^.ZeroBuffer), 0, ctx^.Disk^.sectorSize * ctx^.BatchSize); - - { Kick off the first step: write boot sector } - io.syslog.logln('FAT32', 'create_volume_async: kicking off fmtBootSector'); - ctx^.Step := fmtBootSector; - fmt_run_next(ctx); -end; - -{ ========================================================================== } -{ Synchronous create_volume (task context only) } -{ ========================================================================== } - -procedure create_volume(volume : PStorage_Volume; sectors : uint32; start : uint32; config : puint32); -var - buffer : puint32; - zeroBuffer : puint32; - bootRecord : PBootRecord; - dataStart : uint32; - fatStart : uint32; - FATSize : uint32; - batchSize : uint32; - batchPos : uint32; - - asuroArray : byteArray8 = ('A','S','U','R','O',' ','V','1'); - fatArray : byteArray8 = ('F','A','T','3','2',' ',' ',' '); - thisArray : byteArray8 = ('.',' ',' ',' ',' ',' ',' ',' '); - parentArray : byteArray8 = ('.','.',' ',' ',' ',' ',' ',' '); - - asuroFileArray : byteArray8 = ('A','S','U','R','O',' ',' ',' '); - mountFileArray : byteArray8 = ('M','O','U','N','T',' ',' ',' '); - programFileArray : byteArray8 = ('P','R','O','G','R','A','M','S'); - rootCluster : uint32 = 2; - - sysArray : byteArray8 = ('S','Y','S','T','E','M',' ',' '); - progArray : byteArray8 = ('P','R','O','G','R','A','M','S'); - userArray : byteArray8 = ('U','S','E','R',' ',' ',' ',' '); - - status : puint32; - disk : PStorage_device; - spc : uint32; - -begin - push_trace('driver.storage.fs.fat32.create_volume()'); - io.syslog.logln('FAT32', 'create_volume (sync): enter'); - - disk := volume^.device; - - { Default sectors-per-cluster to 1 if config is nil } - if config <> nil then - spc := config^ - else - spc := fatDefaultSPC(sectors, disk^.sectorSize); - if spc = 0 then - spc := fatDefaultSPC(sectors, disk^.sectorSize); - - //driver.storage.fs.fat32 structure - (* BootRecord *) - (* reserved sectors *) - (* File Allocation Table *) - (* Data Area *) - - buffer:= puint32(kalloc(disk^.sectorSize+512)); - memset(uint32(buffer), 0, disk^.sectorSize); - - bootRecord:= PBootRecord(buffer); - - FATSize:= fatComputeFATSize(sectors, spc, disk^.sectorsize); - - bootRecord^.jmp2boot := $0; //TODO impliment boot jump - bootRecord^.OEMName := asuroArray; - bootRecord^.sectorSize := disk^.sectorsize; - bootRecord^.spc := uint8(spc); - bootRecord^.rsvSectors := 32; //32 is standard - bootRecord^.numFats := 1; - bootRecord^.mediaDescp := $F8; - bootRecord^.hiddenSectors := start; - bootRecord^.manySectors := sectors; - bootRecord^.FATSize := FATSize; - bootRecord^.rootCluster := rootCluster; - bootRecord^.FSInfoCluster := 0; - bootRecord^.driveNumber := $80; - bootRecord^.volumeID := 62; //+ puint32(@driver.timer.rtc.getDateTime())^; - bootRecord^.bsignature := $29; - bootRecord^.identString := fatArray; - - - { Write the boot sector signature marker at bytes 510-511 } - puint8(uint32(buffer) + 510)^ := $55; - puint8(uint32(buffer) + 511)^ := $AA; - - driver.storage.mgr.storage_write(disk, start, 1, puint32(buffer)); - io.syslog.logln('FAT32', 'create_volume (sync): boot sector written'); - - fatStart:= start + bootRecord^.rsvSectors; - dataStart:= fatStart + bootRecord^.FATSize; - - { Batch zero the FAT: write 128 sectors at a time instead of 1 } - if FATSize > 128 then - batchSize := 128 - else - batchSize := FATSize; - zeroBuffer:= puint32(kalloc( disk^.sectorSize * batchSize )); - memset(uint32(zeroBuffer), 0, disk^.sectorSize * batchSize); - - batchPos := 0; - while batchPos < FATSize do begin - if (FATSize - batchPos) >= batchSize then - driver.storage.mgr.storage_write(disk, fatStart + batchPos, batchSize, zeroBuffer) - else - driver.storage.mgr.storage_write(disk, fatStart + batchPos, FATSize - batchPos, zeroBuffer); - batchPos += batchSize; - end; - - kfree(buffer); - kfree(zeroBuffer); - io.syslog.logln('FAT32', 'create_volume (sync): FAT zeroed'); - - buffer:= puint32(kalloc(disk^.sectorSize)); - memset(uint32(buffer), 0, disk^.sectorSize); - - puint32(buffer)[0]:= $0FFFFFF8; //media type marker (reserved entry 0) - puint32(buffer)[1]:= $0FFFFFFF; //clean/dirty marker (reserved entry 1) - puint32(buffer)[2]:= $0FFFFFF8; //root cluster end-of-chain (entry 2) - - driver.storage.mgr.storage_write(disk, fatStart, 1, buffer); - io.syslog.logln('FAT32', 'create_volume (sync): FAT entries written'); - - kfree(buffer); - - buffer:= puint32(kalloc(disk^.sectorsize)); - memset(uint32(buffer), 0, disk^.sectorsize); - - PDirectory(buffer)[0].fileName := thisArray; - PDirectory(buffer)[0].attributes := $08; - PDirectory(buffer)[0].clusterLow := rootCluster; - - PDirectory(buffer)[1].fileName := parentArray; - PDirectory(buffer)[1].attributes := $10; - PDirectory(buffer)[1].clusterLow := rootCluster; - - driver.storage.mgr.storage_write(disk, clusterToLBA(dataStart, spc, rootCluster), 1, buffer); - io.syslog.logln('FAT32', 'create_volume (sync): root dir written'); - - memset(uint32(buffer), 0, disk^.sectorsize); - - status := puint32(kalloc(sizeof(uint32))); - writeDirectory(volume, '', 'SYSTEM', $10, status); - io.syslog.logln('FAT32', 'create_volume (sync): SYSTEM dir created'); - kfree(status); - - kfree(buffer); - io.syslog.logln('FAT32', 'create_volume (sync): done'); - - - -end; - -{ Count free clusters by scanning the FAT sector by sector. - Returns the number of free clusters (FAT entries == 0). } -function countFreeFATClusters(volume : PStorage_Volume; bootRecord : PBootRecord) : uint32; -var - fatStart : uint32; - maxCluster : uint32; - entriesPerSect : uint32; - sectorIdx : uint32; - entryIdx : uint32; - clusterNum : uint32; - freeCount : uint32; - fatSectors : uint32; - fatBuffer : puint32; -begin - countFreeFATClusters := 0; - if (bootRecord^.sectorsize = 0) or (bootRecord^.FATSize = 0) then exit; - - fatStart := volume^.sectorStart + bootRecord^.rsvSectors; - maxCluster := (bootRecord^.FATSize * bootRecord^.sectorsize) div 4; - entriesPerSect := bootRecord^.sectorsize div 4; - fatSectors := bootRecord^.FATSize; - - fatBuffer := puint32(kalloc(bootRecord^.sectorsize)); - freeCount := 0; - - for sectorIdx := 0 to fatSectors - 1 do begin - if sectorIdx * entriesPerSect >= maxCluster then break; - driver.storage.mgr.storage_read(volume^.device, fatStart + sectorIdx, 1, fatBuffer); - for entryIdx := 0 to entriesPerSect - 1 do begin - clusterNum := sectorIdx * entriesPerSect + entryIdx; - if clusterNum < 2 then continue; - if clusterNum >= maxCluster then break; - if fatBuffer[entryIdx] = 0 then - freeCount := freeCount + 1; - end; - end; - - kfree(fatBuffer); - countFreeFATClusters := freeCount; -end; - -function identify_volume(volume : PStorage_Volume) : boolean; -var - buffer : puint32; - bootRecord : PBootRecord; - bufSize : uint32; - fatNameOk : boolean; - spc : uint8; - i : uint32; -begin - push_trace('driver.storage.fs.fat32.identify_volume'); - io.syslog.logln('FAT32', 'identify_volume: enter'); - identify_volume := false; - if volume^.device = nil then exit; - if volume^.device^.dispatchRead = nil then exit; - - bufSize := volume^.device^.sectorSize; - if bufSize < 512 then bufSize := 512; - buffer := puint32(kalloc(bufSize)); - memset(uint32(buffer), 0, bufSize); - - driver.storage.mgr.storage_read(volume^.device, volume^.sectorStart, 1, buffer); - bootRecord := PBootRecord(buffer); - - fatNameOk := true; - for i := 0 to 4 do begin - if bootRecord^.identString[i] <> 'FAT32'[i + 1] then begin - fatNameOk := false; - break; - end; - end; - - spc := bootRecord^.spc; - - if bootSectorHasSignature(buffer) and - (bootRecord^.bsignature = $29) and - fatNameOk and - (bootRecord^.sectorSize = volume^.device^.sectorSize) and - (bootRecord^.sectorSize >= 512) and - (spc <> 0) and - ((spc and (spc - 1)) = 0) and - (bootRecord^.rsvSectors > 0) and - (bootRecord^.FATSize > 0) and - (bootRecord^.numFats > 0) and - (bootRecord^.rootCluster >= 2) then begin - io.syslog.logln('FAT32', 'identify_volume: FAT32 signature matched'); - identify_volume := true; - volume^.freeSectors := countFreeFATClusters(volume, bootRecord) * bootRecord^.spc; - end else begin - io.syslog.logln('FAT32', 'identify_volume: not a FAT32 volume'); - end; - - kfree(buffer); -end; - -procedure detect_volumes(disk : PStorage_Device); -var - buffer : puint32; - bufSize : uint32; - i : uint8; - volume : PStorage_volume; -begin - push_trace('driver.storage.fs.fat32.detectVolumes()'); - - bufSize := disk^.sectorSize; - if bufSize < 512 then bufSize := 512; - buffer := puint32(kalloc(bufSize)); - memset(uint32(buffer), 0, bufSize); - - { Read from sector 0 to check for FAT32 boot record } - if disk^.dispatchRead = nil then begin - io.syslog.writestringln('FAT32: detect_volumes: device has no read dispatch.'); - kfree(buffer); - exit; - end; - - driver.storage.mgr.storage_read(disk, 0, 1, buffer); - - if bootSectorHasSignature(buffer) and (PBootRecord(buffer)^.bsignature = $29) then begin - io.syslog.writestringln('FAT32: volume found!'); - volume := PStorage_volume(kalloc(sizeof(TStorage_Volume))); - memset(uint32(volume), 0, sizeof(TStorage_Volume)); - volume^.device := disk; - volume^.sectorStart := 0; - volume^.sectorSize := PBootRecord(buffer)^.sectorSize; - volume^.sectorCount := disk^.maxSectorCount; - volume^.filesystem := @filesystem; - volume^.freeSectors := countFreeFATClusters(volume, PBootRecord(buffer)) * PBootRecord(buffer)^.spc; - volume^.isBootDrive := false; - - driver.storage.vol.mgr.register_volume(disk, volume); - end; - - kfree(buffer); -end; - -{ ---- Per-file open / close for VFS metadata caching ---- } - -{ Called by VFS on OpenFile. Reads the boot record (cached per-volume), - scans the directory for the file, and returns a TFATOpenFile context - that writeFileAtOffset / readFileAtOffset can reuse on every call. - Also populates fileSize so the VFS can skip calling fileSizeCallback. } -function fat32OpenFile(volume : PStorage_Volume; directory : pchar; - fileName : pchar; var fileSize : uint32) : pointer; -var - ofi : PFATOpenFile; - bootRecord : PBootRecord; - dirs : PLinkedListBase; - status : puint32; - tempdir : PDirectory; - i : uint32; - cleanFileName : byteArray8; - otherCFN : byteArray8; - namePart : pchar; - extPart : pchar; -begin - push_trace('driver.storage.fs.fat32.openFile.enter'); - fileSize := 0; - - bootRecord := getCachedBootRecord(volume); - - ofi := PFATOpenFile(kalloc(SizeOf(TFATOpenFile))); - memset(uint32(ofi), 0, SizeOf(TFATOpenFile)); - ofi^.Volume := volume; - ofi^.BootRecord := bootRecord; - ofi^.DataStart := fatDataStartLBA(volume, bootRecord); - ofi^.ScratchSize := bootRecord^.sectorSize; - ofi^.ScratchSector := puint32(kalloc(ofi^.ScratchSize)); - if ofi^.ScratchSector <> nil then - memset(uint32(ofi^.ScratchSector), 0, ofi^.ScratchSize); - ofi^.Exists := false; - - { Read directory entries } - status := puint32(kalloc(sizeof(uint32))); - status^ := 0; - dirs := readDirectory(volume, directory, status); - - { Determine parent cluster (Lesson 18: root has no '.' entry) } - if (directory = nil) or (stringSize(directory) = 0) then - ofi^.DirCluster := bootRecord^.rootCluster - else begin - if (dirs <> nil) and (LL_size(dirs) > 0) then begin - tempdir := PDirectory(LL_get(dirs, 0)); - if tempdir^.fileName[0] = '.' then - ofi^.DirCluster := dirFirstCluster(tempdir) - else - ofi^.DirCluster := bootRecord^.rootCluster; - end else - ofi^.DirCluster := bootRecord^.rootCluster; - end; - - { Parse file name and compute cleaned 8.3 name } - splitFileNameParts(fileName, namePart, extPart); - cleanFileName := cleanString(namePart, status); - ofi^.CleanName := cleanFileName; - ofi^.ExtPart := stringCopy(extPart); - - { Search for file in directory entries } - if (dirs <> nil) and (LL_size(dirs) > 0) then begin - for i := 0 to LL_Size(dirs) - 1 do begin - tempdir := PDirectory(LL_get(dirs, i)); - otherCFN := cleanString(tempdir^.filename, status); - if compareByteArray8(cleanFileName, otherCFN) and matchExtension(tempdir^.fileExtension, extPart) then begin - ofi^.FirstCluster := dirFirstCluster(tempdir); - ofi^.ByteSize := tempdir^.byteSize; - ofi^.Exists := true; - end; - end; - end; - - ofi^.AllocClusters := fatClustersForBytes(ofi^.ByteSize, - uint32(bootRecord^.spc) * uint32(bootRecord^.sectorSize)); - if ofi^.Exists and (ofi^.FirstCluster >= 2) and (ofi^.ByteSize > 0) then - ofi^.Extents := fatBuildExtentList(volume, ofi^.FirstCluster, bootRecord); - - kfree(void(namePart)); - kfree(void(extPart)); - if dirs <> nil then LL_Free(dirs); - kfree(puint32(status)); - - fileSize := ofi^.ByteSize; - fat32OpenFile := pointer(ofi); - push_trace('driver.storage.fs.fat32.openFile.exit'); -end; - -{ Called by VFS on CloseFile to free the per-file context. } -procedure fat32CloseFile(ctx : pointer); -var - ofi : PFATOpenFile; - fatCache : PFATCache; - dirBuf : puint32; - rawDir : PDirectory; - loc : TDirEntryLocation; -begin - if ctx = nil then exit; - ofi := PFATOpenFile(ctx); - if ofi^.FatDirty and (ofi^.Volume <> nil) then begin - fatCache := fatCacheGet(ofi^.Volume); - if fatCache <> nil then - fatCacheFlush(fatCache, ofi^.Volume); - end; - if ofi^.MetaDirty and (ofi^.Volume <> nil) and (ofi^.BootRecord <> nil) then begin - dirBuf := puint32(kalloc(ofi^.BootRecord^.sectorSize)); - if (dirBuf <> nil) and locateDirEntry(ofi^.Volume, ofi^.DirCluster, ofi^.BootRecord, - ofi^.CleanName, ofi^.ExtPart, loc, dirBuf) then begin - rawDir := @PDirectory(dirBuf)[loc.EntryIdx]; - rawDir^.byteSize := ofi^.ByteSize; - if ofi^.FirstCluster <> 0 then - setDirFirstCluster(rawDir, ofi^.FirstCluster); - driver.storage.mgr.storage_write(ofi^.Volume^.device, loc.SectorLBA, 1, dirBuf); - end; - if dirBuf <> nil then - kfree(dirBuf); - end; - if ofi^.ExtPart <> nil then - kfree(void(ofi^.ExtPart)); - if ofi^.Extents <> nil then - LL_Free(ofi^.Extents); - if ofi^.ScratchSector <> nil then - kfree(ofi^.ScratchSector); - kfree(puint32(ofi)); -end; - -{ Write byteCount bytes to a file beginning at byte offset. - If offset + byteCount exceeds the current file size, the file is extended - by allocating new clusters. Returns the number of bytes actually written. } -function writeFileAtOffset(volume : PStorage_Volume; directory : pchar; - fileName : pchar; offset : uint32; - buffer : puint32; byteCount : uint32; - ctx : pointer) : uint32; -const - MAX_RUN_SECTORS = 2048; { cap per multi-sector write = 1MB } -var - lookup : TFileLookup; - run : TRunInfo; - ofi : PFATOpenFile; - i : uint32; - cluster : uint32; - dataStart : uint32; - cleanFileName : byteArray8; - otherCFN : byteArray8; - tempdir : PDirectory; - namePart : pchar; - extPart : pchar; - ioBuf : puint32; - bytesPerCluster : uint32; - startClusterIdx : uint32; - inClusterOffset : uint32; - chainPos : uint32; - curCluster : uint32; - srcPos : uint32; - remaining : uint32; - fileSize : uint32; - origByteSize : uint32; - newEnd : uint32; - oldClusters : uint32; - needClusters : uint32; - targetClusters : uint32; - extraClusters : uint32; - newExtents : PLinkedListBase; - lastCluster : uint32; - nextFat : uint32; - extent : PClusterExtent; - { dir entry update — raw sector scan (Lesson 18) } - dirCluster : uint32; - dirBuf : puint32; - rawDir : PDirectory; - loc : TDirEntryLocation; - { Multi-sector write variables } - spc : uint32; - secSize : uint32; - skipBytes : uint32; - copyBytes : uint32; - firstSectorOff : uint32; - partialBytes : uint32; - fullSectors : uint32; - batchBytes : uint32; - sectorLBA : uint32; - fatCache : PFATCache; - chainWalkLimit : uint32; - firstNewCluster : uint32; - reserveClusters : uint32; -begin - push_trace('driver.storage.fs.fat32.writeFileAtOffset.enter'); - writeFileAtOffset := 0; - ofi := PFATOpenFile(ctx); - - if ofi <> nil then begin - { Fast path: use cached open-file metadata } - lookup.BootRecord := ofi^.BootRecord; - lookup.Dirs := nil; - lookup.Dir := nil; - lookup.Status := nil; - lookup.Exists := ofi^.Exists; - dataStart := ofi^.DataStart; - dirCluster := ofi^.DirCluster; - cleanFileName := ofi^.CleanName; - extPart := ofi^.ExtPart; - namePart := nil; - if lookup.Exists then begin - origByteSize := ofi^.ByteSize; - cluster := ofi^.FirstCluster; - end else begin - origByteSize := 0; - cluster := 0; - end; - end else begin - { Slow path: rediscover everything from disk } - lookup.Status := puint32(kalloc(sizeof(uint32))); - lookup.Status^ := 0; - lookup.BootRecord := readBootRecord(volume); - lookup.Dirs := readDirectory(volume, directory, lookup.Status); - lookup.Exists := false; - dataStart := fatDataStartLBA(volume, lookup.BootRecord); - - splitFileNameParts(fileName, namePart, extPart); - cleanFileName := cleanString(namePart, lookup.Status); - - if LL_size(lookup.Dirs) > 0 then begin - for i := 0 to LL_Size(lookup.Dirs) - 1 do begin - tempdir := PDirectory(LL_get(lookup.Dirs, i)); - otherCFN := cleanString(tempdir^.filename, lookup.Status); - if compareByteArray8(cleanFileName, otherCFN) and matchExtension(tempdir^.fileExtension, extPart) then begin - lookup.Dir := tempdir; - lookup.Exists := true; - end; - end; - end; - - { Determine parent cluster (Lesson 18: root has no '.' entry) } - if (directory = nil) or (stringSize(directory) = 0) then - dirCluster := lookup.BootRecord^.rootCluster - else begin - if LL_size(lookup.Dirs) > 0 then begin - tempdir := PDirectory(LL_get(lookup.Dirs, 0)); - if tempdir^.fileName[0] = '.' then - dirCluster := dirFirstCluster(tempdir) - else - dirCluster := lookup.BootRecord^.rootCluster; - end else - dirCluster := lookup.BootRecord^.rootCluster; - end; - - if lookup.Exists then begin - origByteSize := lookup.Dir^.byteSize; - cluster := dirFirstCluster(lookup.Dir); - end else begin - origByteSize := 0; - cluster := 0; - end; - end; - fileSize := origByteSize; - if lookup.BootRecord^.sectorSize > 0 then - chainWalkLimit := (lookup.BootRecord^.FATSize * lookup.BootRecord^.sectorSize) div 4 - else - chainWalkLimit := 0; - - if not lookup.Exists then begin - { --- Create a new zero-length file entry on disk --- } - { dirCluster, cleanFileName, extPart already set above } - - dirBuf := puint32(kalloc(lookup.BootRecord^.sectorSize)); - - if not locateFreeDirEntry(volume, dirCluster, lookup.BootRecord, loc, dirBuf) then begin - kfree(dirBuf); - io.syslog.logln('FAT32', 'writeFileAtOffset: exit-A no free dir slot'); - if ofi = nil then begin - if namePart <> nil then kfree(void(namePart)); - if extPart <> nil then kfree(void(extPart)); - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); - end; - exit; - end; - - rawDir := @PDirectory(dirBuf)[loc.EntryIdx]; - memset(uint32(rawDir), 0, sizeof(TDirectory)); - rawDir^.fileName := cleanFileName; - rawDir^.attributes := 0; { regular file } - fillFatExt(rawDir^.fileExtension, extPart); - rawDir^.clusterLow := 0; - rawDir^.clusterHigh := 0; - rawDir^.byteSize := 0; - driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, dirBuf); - - kfree(dirBuf); - - { Invalidate chain cache — new file } - sioc_valid := false; - end; - newEnd := offset + byteCount; - bytesPerCluster := uint32(lookup.BootRecord^.spc) * uint32(lookup.BootRecord^.sectorSize); - - { ---- Extend file if writing past EOF ---- } - if newEnd > fileSize then begin - if ofi <> nil then - oldClusters := ofi^.AllocClusters - else - oldClusters := fatClustersForBytes(fileSize, bytesPerCluster); - needClusters := fatClustersForBytes(newEnd, bytesPerCluster); - targetClusters := needClusters; - if (ofi <> nil) and (needClusters > oldClusters) then begin - reserveClusters := fatPreallocClusterCount(bytesPerCluster); - targetClusters := oldClusters + reserveClusters; - if targetClusters < needClusters then - targetClusters := needClusters; - end; - - if targetClusters > oldClusters then begin - extraClusters := targetClusters - oldClusters; - if oldClusters > 0 then begin - { Walk to last cluster using cache if available } - if sioc_valid and (sioc_vol = volume) and (sioc_fileClust = cluster) then - lastCluster := sioc_chainClust - else - lastCluster := cluster; - nextFat := readFat(volume, lastCluster, lookup.BootRecord); - while ((nextFat and $0FFFFFFF) < $0FFFFFF8) and (nextFat <> 0) do begin - if chainWalkLimit = 0 then - break; - chainWalkLimit := chainWalkLimit - 1; - if chainWalkLimit = 0 then begin - io.syslog.logln('FAT32', 'writeFileAtOffset: FAT loop detected while finding tail'); - if ofi = nil then begin - if namePart <> nil then kfree(void(namePart)); - if extPart <> nil then kfree(void(extPart)); - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); - end; - exit; - end; - lastCluster := nextFat; - nextFat := readFat(volume, lastCluster, lookup.BootRecord); - end; - end else begin - { Zero-length file: first new cluster becomes start cluster } - lastCluster := 0; - end; - - if fatFindContiguousFreeRange(volume, extraClusters, lookup.BootRecord, firstNewCluster) then begin - fatLinkContiguousRange(volume, lookup.BootRecord, lastCluster, firstNewCluster, extraClusters, lastCluster); - if oldClusters = 0 then - cluster := firstNewCluster; - if ofi <> nil then begin - if ofi^.Extents = nil then - ofi^.Extents := LL_New(sizeof(TClusterExtent)); - if ofi^.Extents <> nil then - fatAppendExtent(ofi^.Extents, firstNewCluster, extraClusters); - end; - end else begin - newExtents := findFreeClusterExtents(volume, extraClusters, lookup.BootRecord); - if newExtents = nil then begin - io.syslog.logln('FAT32', 'writeFileAtOffset: exit-B disk full'); - { Undo just-created dir entry if file was new } - if not lookup.Exists then begin - dirBuf := puint32(kalloc(lookup.BootRecord^.sectorSize)); - driver.storage.mgr.storage_read(volume^.device, loc.SectorLBA, 1, dirBuf); - PDirectory(dirBuf)[loc.EntryIdx].fileName[0] := char($E5); - driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, dirBuf); - kfree(dirBuf); - end; - if ofi = nil then begin - if namePart <> nil then kfree(void(namePart)); - if extPart <> nil then kfree(void(extPart)); - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); - end; - exit; - end; - - { Link new extents } - if LL_Size(newExtents) > 0 then begin - for i := 0 to LL_Size(newExtents) - 1 do begin - extent := PClusterExtent(LL_Get(newExtents, i)); - fatLinkContiguousRange(volume, lookup.BootRecord, lastCluster, - extent^.StartCluster, extent^.ClusterCount, lastCluster); - if (ofi <> nil) and (extent <> nil) then begin - if ofi^.Extents = nil then - ofi^.Extents := LL_New(sizeof(TClusterExtent)); - if ofi^.Extents <> nil then - fatAppendExtent(ofi^.Extents, extent^.StartCluster, extent^.ClusterCount); - end; - if (oldClusters = 0) and (i = 0) then - cluster := extent^.StartCluster; - end; - end; - LL_Free(newExtents); - end; - - if ofi <> nil then - ofi^.FatDirty := true; - - if ofi <> nil then - ofi^.AllocClusters := targetClusters; - - { Update chain cache: last new cluster is the new tail } - updateSIOC(volume, cluster, targetClusters - 1, lastCluster); - - end; - - fileSize := newEnd; - end; - - { ---- Walk to starting cluster using cache, then write sector by sector ---- } - startClusterIdx := offset div bytesPerCluster; - inClusterOffset := offset mod bytesPerCluster; - - if not seekToClusterIndex(volume, cluster, startClusterIdx, lookup.BootRecord, curCluster, chainPos) then begin - io.syslog.logln('FAT32', 'writeFileAtOffset: exit-C chain broke seeking to start'); - if ofi = nil then begin - if namePart <> nil then kfree(void(namePart)); - if extPart <> nil then kfree(void(extPart)); - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); - end; - exit; - end; - - spc := uint32(lookup.BootRecord^.spc); - secSize := uint32(lookup.BootRecord^.sectorSize); - if (ofi <> nil) and (ofi^.ScratchSector <> nil) and (ofi^.ScratchSize >= secSize) then - ioBuf := ofi^.ScratchSector - else - ioBuf := nil; - remaining := byteCount; - srcPos := 0; - - while (remaining > 0) do begin - { Build contiguous cluster run starting at curCluster } - run.StartCluster := curCluster; - buildContiguousRun(volume, curCluster, spc, MAX_RUN_SECTORS, lookup.BootRecord, run.RunLen, run.LastCluster); - curCluster := run.LastCluster; - - run.RunSectors := run.RunLen * spc; - run.RunBytes := run.RunSectors * secSize; - run.LBA := clusterToLBA(dataStart, spc, run.StartCluster); - - if srcPos = 0 then - skipBytes := inClusterOffset - else - skipBytes := 0; - - copyBytes := run.RunBytes - skipBytes; - if copyBytes > remaining then - copyBytes := remaining; - - firstSectorOff := skipBytes mod secSize; - - { Phase 1: Partial first sector — read-modify-write } - if firstSectorOff > 0 then begin - if ioBuf = nil then - ioBuf := puint32(kalloc(secSize)); - if ioBuf = nil then - break; - sectorLBA := run.LBA + (skipBytes div secSize); - partialBytes := secSize - firstSectorOff; - if partialBytes > copyBytes then - partialBytes := copyBytes; - driver.storage.mgr.storage_read(volume^.device, sectorLBA, 1, ioBuf); - core.util.memcpy(uint32(buffer) + srcPos, uint32(ioBuf) + firstSectorOff, partialBytes); - driver.storage.mgr.storage_write(volume^.device, sectorLBA, 1, ioBuf); - srcPos := srcPos + partialBytes; - remaining := remaining - partialBytes; - copyBytes := copyBytes - partialBytes; - skipBytes := skipBytes + partialBytes; - end; - - { Phase 2: Full sectors — batch write directly from caller buffer } - fullSectors := copyBytes div secSize; - if fullSectors > 0 then begin - sectorLBA := run.LBA + (skipBytes div secSize); - batchBytes := fullSectors * secSize; - driver.storage.mgr.storage_write(volume^.device, sectorLBA, fullSectors, - puint32(uint32(buffer) + srcPos)); - srcPos := srcPos + batchBytes; - remaining := remaining - batchBytes; - copyBytes := copyBytes - batchBytes; - skipBytes := skipBytes + batchBytes; - end; - - { Phase 3: Partial last sector — read-modify-write } - if copyBytes > 0 then begin - if ioBuf = nil then - ioBuf := puint32(kalloc(secSize)); - if ioBuf = nil then - break; - sectorLBA := run.LBA + (skipBytes div secSize); - driver.storage.mgr.storage_read(volume^.device, sectorLBA, 1, ioBuf); - core.util.memcpy(uint32(buffer) + srcPos, uint32(ioBuf), copyBytes); - driver.storage.mgr.storage_write(volume^.device, sectorLBA, 1, ioBuf); - srcPos := srcPos + copyBytes; - remaining := remaining - copyBytes; - end; - - { Update chain cache to last cluster in this run } - updateSIOC(volume, cluster, chainPos + run.RunLen - 1, curCluster); - chainPos := chainPos + run.RunLen; - - if remaining > 0 then begin - nextFat := readFat(volume, curCluster, lookup.BootRecord); - if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then begin - io.syslog.log('FAT32', 'writeFileAtOffset: exit-D chain broke in write loop, srcPos='); - io.syslog.writeint(srcPos); - io.syslog.writestring(' remaining='); - io.syslog.writeint(remaining); - io.syslog.writestring(' cluster='); - io.syslog.writeint(curCluster); - io.syslog.writestring(' fat='); - io.syslog.writehexln(nextFat); - break; - end; - curCluster := nextFat; - end; - end; - - if (ioBuf <> nil) and ((ofi = nil) or (ioBuf <> ofi^.ScratchSector)) then - kfree(ioBuf); - - { Flush dirty FAT cache entries to disk only for uncached callers. - Open-file callers defer this to fat32CloseFile to avoid flushing on every chunk. } - if ofi = nil then begin - fatCache := fatCacheGet(volume); - if fatCache <> nil then - fatCacheFlush(fatCache, volume); - end; - - { ---- Update directory entry byteSize if file was extended ---- } - { Only commit actual bytes written — never advance size past what was really written (Lesson 22) } - if (srcPos > 0) and ((offset + srcPos) > origByteSize) then begin - if ofi <> nil then begin - ofi^.MetaDirty := true; - end else begin - { Lesson 18: search raw directory sectors, don't use LL indices. - dirCluster, cleanFileName, extPart were set once in the discovery phase. } - dirBuf := puint32(kalloc(lookup.BootRecord^.sectorSize)); - - if locateDirEntry(volume, dirCluster, lookup.BootRecord, cleanFileName, extPart, loc, dirBuf) then begin - rawDir := @PDirectory(dirBuf)[loc.EntryIdx]; - rawDir^.byteSize := offset + srcPos; - { If file was zero-length, update cluster pointers too } - if origByteSize = 0 then - setDirFirstCluster(rawDir, cluster); - driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, dirBuf); - end; - - kfree(dirBuf); - end; - end; - - { Update cached open-file metadata so the next read/write sees the new state } - if ofi <> nil then begin - if (srcPos > 0) and ((offset + srcPos) > ofi^.ByteSize) then - ofi^.ByteSize := offset + srcPos; - if (ofi^.FirstCluster = 0) and (cluster <> 0) then begin - ofi^.FirstCluster := cluster; - ofi^.Exists := true; - ofi^.MetaDirty := true; - end; - end; - - if ofi = nil then begin - if namePart <> nil then kfree(void(namePart)); - if extPart <> nil then kfree(void(extPart)); - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); - end; - writeFileAtOffset := srcPos; - push_trace('driver.storage.fs.fat32.writeFileAtOffset.exit'); -end; - -{ Read byteCount bytes from a file beginning at byte offset 'offset'. - Returns the number of bytes actually read (may be less than byteCount if EOF reached). - - NOTE: This implementation reads sectors sequentially from the first cluster of the - file, consistent with the existing readFile behaviour. It does NOT follow the FAT - chain between clusters \u2014 it assumes contiguous allocation. This is acceptable for - the initial streaming implementation. A future improvement should cache the last - visited (offset, cluster) pair in the open-file entry to make sequential chunked - reads O(1) per call instead of O(n) in the FAT chain length. } - -function getFileSize(volume : PStorage_Volume; directory : pchar; fileName : pchar) : uint32; -var - bootRecord : PBootRecord; - dirs : PLinkedListBase; - tempdir : PDirectory; - statusOut : puint32; - i : uint32; - cleanFileName : byteArray8; - otherCFN : byteArray8; - namePart : pchar; - extPart : pchar; - fsize : uint32; -begin - push_trace('driver.storage.fs.fat32.getFileSize.enter'); - getFileSize := 0; - - statusOut := puint32(kalloc(sizeof(uint32))); - statusOut^ := 0; - bootRecord := readBootRecord(volume); - dirs := readDirectory(volume, directory, statusOut); - - splitFileNameParts(fileName, namePart, extPart); - - fsize := 0; - if LL_size(dirs) > 0 then begin - cleanFileName := cleanString(namePart, statusOut); - for i := 0 to LL_Size(dirs) - 1 do begin - tempdir := PDirectory(LL_get(dirs, i)); - otherCFN := cleanString(tempdir^.filename, statusOut); - if compareByteArray8(cleanFileName, otherCFN) and matchExtension(tempdir^.fileExtension, extPart) then begin - fsize := tempdir^.byteSize; - break; - end; - end; - end; - - kfree(void(namePart)); - kfree(void(extPart)); - LL_Free(dirs); - kfree(puint32(bootRecord)); - kfree(puint32(statusOut)); - getFileSize := fsize; - push_trace('driver.storage.fs.fat32.getFileSize.exit'); -end; - -function readFileAtOffset(volume : PStorage_Volume; directory : pchar; fileName : pchar; offset : uint32; buffer : puint32; byteCount : uint32; ctx : pointer) : uint32; -const - MAX_RUN_SECTORS = 2048; { cap per multi-sector read = 1 MB } -var - lookup : TFileLookup; - run : TRunInfo; - ofi : PFATOpenFile; - i : uint32; - cluster : uint32; - dataStart : uint32; - cleanFileName : byteArray8; - otherCFN : byteArray8; - tempdir : PDirectory; - namePart : pchar; - extPart : pchar; - bytesPerCluster : uint32; - startClusterIdx : uint32; - inClusterOffset : uint32; - chainPos : uint32; - curCluster : uint32; - destPos : uint32; - remaining : uint32; - fileSize : uint32; - nextFat : uint32; - spc : uint32; - secSize : uint32; - { Multi-sector run variables } - runBuf : puint32; - skipBytes : uint32; { bytes to skip at start of run buffer } - copyBytes : uint32; - chunkSectors : uint32; - chunkBytes : uint32; -begin - push_trace('driver.storage.fs.fat32.readFileAtOffset.enter'); - readFileAtOffset := 0; - ofi := PFATOpenFile(ctx); - - if ofi <> nil then begin - { Fast path: use cached open-file metadata } - lookup.BootRecord := ofi^.BootRecord; - lookup.Dirs := nil; - lookup.Dir := nil; - lookup.Status := nil; - lookup.Exists := ofi^.Exists; - dataStart := ofi^.DataStart; - if lookup.Exists then begin - fileSize := ofi^.ByteSize; - cluster := ofi^.FirstCluster; - end else begin - fileSize := 0; - cluster := 0; - end; - end else begin - { Slow path: rediscover everything from disk } - lookup.Status := puint32(kalloc(sizeof(uint32))); - lookup.Status^ := 0; - lookup.BootRecord := readBootRecord(volume); - lookup.Dirs := readDirectory(volume, directory, lookup.Status); - lookup.Exists := false; - dataStart := fatDataStartLBA(volume, lookup.BootRecord); - - splitFileNameParts(fileName, namePart, extPart); - - if LL_size(lookup.Dirs) > 0 then begin - cleanFileName := cleanString(namePart, lookup.Status); - for i := 0 to LL_Size(lookup.Dirs) - 1 do begin - tempdir := PDirectory(LL_get(lookup.Dirs, i)); - otherCFN := cleanString(tempdir^.filename, lookup.Status); - if compareByteArray8(cleanFileName, otherCFN) and matchExtension(tempdir^.fileExtension, extPart) then begin - lookup.Dir := tempdir; - lookup.Exists := true; - end; - end; - end; - - kfree(void(namePart)); - kfree(void(extPart)); - - if lookup.Exists then begin - fileSize := lookup.Dir^.byteSize; - cluster := dirFirstCluster(lookup.Dir); - end else begin - fileSize := 0; - cluster := 0; - end; - end; - - if not lookup.Exists then begin - if ofi = nil then begin - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); - end; - push_trace('driver.storage.fs.fat32.readFileAtOffset.notFound'); - exit; - end; - - if offset >= fileSize then begin - if ofi = nil then begin - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); - end; - exit; - end; - - { Clamp read length to not exceed file size } - remaining := byteCount; - if offset + remaining > fileSize then - remaining := fileSize - offset; - - spc := uint32(lookup.BootRecord^.spc); - secSize := uint32(lookup.BootRecord^.sectorSize); - bytesPerCluster := spc * secSize; - startClusterIdx := offset div bytesPerCluster; - inClusterOffset := offset mod bytesPerCluster; - - { Determine curCluster at startClusterIdx using SIOC cache } - if not seekToClusterIndex(volume, cluster, startClusterIdx, lookup.BootRecord, curCluster, chainPos) then begin - if ofi = nil then begin - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); - end; - exit; - end; - - destPos := 0; - - while (remaining > 0) do begin - { Build a contiguous cluster run starting at curCluster } - run.StartCluster := curCluster; - buildContiguousRun(volume, curCluster, spc, MAX_RUN_SECTORS, lookup.BootRecord, run.RunLen, curCluster); - - run.RunSectors := run.RunLen * spc; - run.RunBytes := run.RunSectors * secSize; - run.LBA := clusterToLBA(dataStart, spc, run.StartCluster); - - { Calculate how many bytes from this run we actually need } - if destPos = 0 then - skipBytes := inClusterOffset - else - skipBytes := 0; - - copyBytes := run.RunBytes - skipBytes; - if copyBytes > remaining then - copyBytes := remaining; - - { Determine if we can read directly into caller's buffer (aligned, full run) } - if (skipBytes = 0) and (copyBytes >= run.RunBytes) then begin - { Direct read into output buffer — no temp allocation } - driver.storage.mgr.storage_read(volume^.device, run.LBA, run.RunSectors, - puint32(uint32(buffer) + destPos)); - destPos := destPos + copyBytes; - remaining := remaining - copyBytes; - end else if (skipBytes = 0) and ((copyBytes mod secSize) = 0) then begin - { Partial run but sector-aligned — read only needed sectors } - chunkSectors := copyBytes div secSize; - driver.storage.mgr.storage_read(volume^.device, run.LBA, chunkSectors, - puint32(uint32(buffer) + destPos)); - destPos := destPos + copyBytes; - remaining := remaining - copyBytes; - end else begin - { Unaligned — need temp buffer for partial first/last sectors } - { Only allocate for sectors we actually touch } - chunkSectors := (skipBytes + copyBytes + secSize - 1) div secSize; - if chunkSectors > run.RunSectors then chunkSectors := run.RunSectors; - chunkBytes := chunkSectors * secSize; - runBuf := puint32(kalloc(chunkBytes)); - run.LBA := clusterToLBA(dataStart, spc, run.StartCluster) + (skipBytes div secSize); - - { Adjust skipBytes for sectors we're skipping entirely } - skipBytes := skipBytes mod secSize; - driver.storage.mgr.storage_read(volume^.device, run.LBA, chunkSectors, runBuf); - core.util.memcpy(uint32(runBuf) + skipBytes, uint32(buffer) + destPos, copyBytes); - kfree(runBuf); - destPos := destPos + copyBytes; - remaining := remaining - copyBytes; - end; - - { Update SIOC chain cache to last cluster in this run } - updateSIOC(volume, cluster, chainPos + run.RunLen - 1, curCluster); - chainPos := chainPos + run.RunLen; - - { Advance to next cluster beyond this run } - if remaining > 0 then begin - nextFat := readFat(volume, curCluster, lookup.BootRecord); - if ((nextFat and $0FFFFFFF) >= $0FFFFFF8) or (nextFat = 0) then - break; - curCluster := nextFat; - end; - end; - - if ofi = nil then begin - LL_Free(lookup.Dirs); - kfree(puint32(lookup.BootRecord)); - kfree(puint32(lookup.Status)); - end; - readFileAtOffset := destPos; - push_trace('driver.storage.fs.fat32.readFileAtOffset.exit'); -end; - -procedure readFileAtOffsetAsync(volume : PStorage_Volume; directory : pchar; fileName : pchar; - offset : uint32; buffer : puint32; byteCount : uint32; - ctx : pointer; bytesRead : puint32; - callback : TIOCallback; callbackData : pointer); -var - ofi : PFATOpenFile; - asyncCtx : PFATFileIOAsyncCtx; - sectorLBA : uint32; - chunkSectors : uint32; - err : TError; -begin - if bytesRead <> nil then bytesRead^ := 0; - ofi := PFATOpenFile(ctx); - - if (ofi = nil) or (buffer = nil) or (byteCount = 0) then begin - if callback <> nil then callback(eInvalidArgument, callbackData); - exit; - end; - - if not ofi^.Exists then begin - if callback <> nil then callback(eFileDoesNotExist, callbackData); - exit; - end; - - if offset >= ofi^.ByteSize then begin - if callback <> nil then callback(eNone, callbackData); - exit; - end; - - if offset + byteCount > ofi^.ByteSize then - byteCount := ofi^.ByteSize - offset; - - if ((offset mod ofi^.BootRecord^.sectorSize) <> 0) or ((byteCount mod ofi^.BootRecord^.sectorSize) <> 0) then begin - if bytesRead <> nil then - bytesRead^ := readFileAtOffset(volume, directory, fileName, offset, buffer, byteCount, ctx); - if callback <> nil then callback(eNone, callbackData); - exit; - end; - - asyncCtx := fatTransferAlloc(volume); - if asyncCtx = nil then begin - if callback <> nil then callback(eOutOfMemory, callbackData); - exit; - end; - asyncCtx^.Mode := famRead; - asyncCtx^.State := ftsPrepare; - asyncCtx^.Volume := volume; - asyncCtx^.OpenFile := ofi; - asyncCtx^.Buffer := buffer; - asyncCtx^.Offset := offset; - asyncCtx^.ByteCount := byteCount; - asyncCtx^.BytesOut := bytesRead; - asyncCtx^.Callback := callback; - asyncCtx^.CallbackData := callbackData; - asyncCtx^.Cluster := ofi^.FirstCluster; - asyncCtx^.DataStart := ofi^.DataStart; - asyncCtx^.BootRecord := ofi^.BootRecord; - asyncCtx^.LastError := eNone; - asyncCtx^.CursorValid := false; - asyncCtx^.ExtentValid := false; - if fat32_fileio_prepare_single_run(asyncCtx, sectorLBA, chunkSectors) and - (asyncCtx^.ChunkBytes = asyncCtx^.ByteCount) then begin - err := driver.storage.mgr.storage_read_async( - volume^.device, sectorLBA, chunkSectors, buffer, - @fat32_fileio_direct_complete, asyncCtx); - if err <> eNone then begin - fatTransferFree(asyncCtx); - if callback <> nil then callback(err, callbackData); - end; - exit; - end; - fatWorkerEnsure(volume); - if not fatScheduleTransfer(asyncCtx) then begin - fatTransferFree(asyncCtx); - if callback <> nil then callback(eQueueFull, callbackData); - end; -end; - -procedure writeFileAtOffsetAsync(volume : PStorage_Volume; directory : pchar; fileName : pchar; - offset : uint32; buffer : puint32; byteCount : uint32; - ctx : pointer; bytesWritten : puint32; - callback : TIOCallback; callbackData : pointer); -var - ofi : PFATOpenFile; - asyncCtx : PFATFileIOAsyncCtx; - cluster : uint32; - dataStart : uint32; - origByteSize : uint32; - newEnd : uint32; - bytesPerCluster : uint32; - oldClusters : uint32; - needClusters : uint32; - targetClusters : uint32; - extraClusters : uint32; - newExtents : PLinkedListBase; - lastCluster : uint32; - nextFat : uint32; - i : uint32; - extent : PClusterExtent; - dirBuf : puint32; - rawDir : PDirectory; - loc : TDirEntryLocation; - chainWalkLimit : uint32; - firstNewCluster : uint32; - reserveClusters : uint32; - sectorLBA : uint32; - chunkSectors : uint32; - err : TError; -begin - if bytesWritten <> nil then bytesWritten^ := 0; - ofi := PFATOpenFile(ctx); - - if (ofi = nil) or (buffer = nil) or (byteCount = 0) then begin - if callback <> nil then callback(eInvalidArgument, callbackData); - exit; - end; - - if ((offset mod ofi^.BootRecord^.sectorSize) <> 0) or ((byteCount mod ofi^.BootRecord^.sectorSize) <> 0) then begin - if bytesWritten <> nil then - bytesWritten^ := writeFileAtOffset(volume, directory, fileName, offset, buffer, byteCount, ctx); - if callback <> nil then callback(eNone, callbackData); - exit; - end; - - dataStart := ofi^.DataStart; - origByteSize := ofi^.ByteSize; - if ofi^.BootRecord^.sectorSize > 0 then - chainWalkLimit := (ofi^.BootRecord^.FATSize * ofi^.BootRecord^.sectorSize) div 4 - else - chainWalkLimit := 0; - if ofi^.Exists then - cluster := ofi^.FirstCluster - else - cluster := 0; - - if not ofi^.Exists then begin - dirBuf := puint32(kalloc(ofi^.BootRecord^.sectorSize)); - if dirBuf = nil then begin - if callback <> nil then callback(eOutOfMemory, callbackData); - exit; - end; - if not locateFreeDirEntry(volume, ofi^.DirCluster, ofi^.BootRecord, loc, dirBuf) then begin - kfree(dirBuf); - if callback <> nil then callback(eDirectoryFull, callbackData); - exit; - end; - rawDir := @PDirectory(dirBuf)[loc.EntryIdx]; - memset(uint32(rawDir), 0, sizeof(TDirectory)); - rawDir^.fileName := ofi^.CleanName; - rawDir^.attributes := 0; - fillFatExt(rawDir^.fileExtension, ofi^.ExtPart); - rawDir^.clusterLow := 0; - rawDir^.clusterHigh := 0; - rawDir^.byteSize := 0; - driver.storage.mgr.storage_write(volume^.device, loc.SectorLBA, 1, dirBuf); - kfree(dirBuf); - ofi^.Exists := true; - sioc_valid := false; - end; - - newEnd := offset + byteCount; - bytesPerCluster := uint32(ofi^.BootRecord^.spc) * uint32(ofi^.BootRecord^.sectorSize); - - if newEnd > origByteSize then begin - oldClusters := ofi^.AllocClusters; - needClusters := fatClustersForBytes(newEnd, bytesPerCluster); - targetClusters := needClusters; - if needClusters > oldClusters then begin - reserveClusters := fatPreallocClusterCount(bytesPerCluster); - targetClusters := oldClusters + reserveClusters; - if targetClusters < needClusters then - targetClusters := needClusters; - end; - - if targetClusters > oldClusters then begin - extraClusters := targetClusters - oldClusters; - if oldClusters > 0 then begin - if sioc_valid and (sioc_vol = volume) and (sioc_fileClust = cluster) then - lastCluster := sioc_chainClust - else - lastCluster := cluster; - nextFat := readFat(volume, lastCluster, ofi^.BootRecord); - while ((nextFat and $0FFFFFFF) < $0FFFFFF8) and (nextFat <> 0) do begin - if chainWalkLimit = 0 then begin - if callback <> nil then callback(eCorruptFilesystem, callbackData); - exit; - end; - chainWalkLimit := chainWalkLimit - 1; - lastCluster := nextFat; - nextFat := readFat(volume, lastCluster, ofi^.BootRecord); - end; - end else - lastCluster := 0; - - if fatFindContiguousFreeRange(volume, extraClusters, ofi^.BootRecord, firstNewCluster) then begin - fatLinkContiguousRange(volume, ofi^.BootRecord, lastCluster, firstNewCluster, extraClusters, lastCluster); - if oldClusters = 0 then - cluster := firstNewCluster; - if ofi^.Extents = nil then - ofi^.Extents := LL_New(sizeof(TClusterExtent)); - if ofi^.Extents <> nil then - fatAppendExtent(ofi^.Extents, firstNewCluster, extraClusters); - end else begin - newExtents := findFreeClusterExtents(volume, extraClusters, ofi^.BootRecord); - if newExtents = nil then begin - if callback <> nil then callback(eDiskFull, callbackData); - exit; - end; - - if LL_Size(newExtents) > 0 then begin - for i := 0 to LL_Size(newExtents) - 1 do begin - extent := PClusterExtent(LL_Get(newExtents, i)); - fatLinkContiguousRange(volume, ofi^.BootRecord, lastCluster, - extent^.StartCluster, extent^.ClusterCount, lastCluster); - if extent <> nil then begin - if ofi^.Extents = nil then - ofi^.Extents := LL_New(sizeof(TClusterExtent)); - if ofi^.Extents <> nil then - fatAppendExtent(ofi^.Extents, extent^.StartCluster, extent^.ClusterCount); - end; - if (oldClusters = 0) and (i = 0) then - cluster := extent^.StartCluster; - end; - end; - LL_Free(newExtents); - end; - - ofi^.FatDirty := true; - ofi^.AllocClusters := targetClusters; - updateSIOC(volume, cluster, targetClusters - 1, lastCluster); - end; - end; - - asyncCtx := fatTransferAlloc(volume); - if asyncCtx = nil then begin - if callback <> nil then callback(eOutOfMemory, callbackData); - exit; - end; - asyncCtx^.Mode := famWrite; - asyncCtx^.State := ftsPrepare; - asyncCtx^.Volume := volume; - asyncCtx^.OpenFile := ofi; - asyncCtx^.Buffer := buffer; - asyncCtx^.Offset := offset; - asyncCtx^.ByteCount := byteCount; - asyncCtx^.BytesOut := bytesWritten; - asyncCtx^.Callback := callback; - asyncCtx^.CallbackData := callbackData; - asyncCtx^.Cluster := cluster; - asyncCtx^.OrigByteSize := origByteSize; - asyncCtx^.DataStart := dataStart; - asyncCtx^.BootRecord := ofi^.BootRecord; - - if (cluster <> 0) and ((offset + byteCount) > ofi^.ByteSize) then - ofi^.MetaDirty := true; - - asyncCtx^.LastError := eNone; - asyncCtx^.CursorValid := false; - asyncCtx^.ExtentValid := false; - if fat32_fileio_prepare_single_run(asyncCtx, sectorLBA, chunkSectors) and - (asyncCtx^.ChunkBytes = asyncCtx^.ByteCount) then begin - err := driver.storage.mgr.storage_write_async( - volume^.device, sectorLBA, chunkSectors, buffer, - @fat32_fileio_direct_complete, asyncCtx); - if err <> eNone then begin - fatTransferFree(asyncCtx); - if callback <> nil then callback(err, callbackData); - end; - exit; - end; - fatWorkerEnsure(volume); - if not fatScheduleTransfer(asyncCtx) then begin - fatTransferFree(asyncCtx); - if callback <> nil then callback(eQueueFull, callbackData); - end; -end; - -procedure init(); -begin - { Legacy reference unit only. The rewritten public FAT32 unit handles - filesystem registration and callback selection. } -end; - -end. diff --git a/src/driver/storage/fs/driver.storage.fs.fat32.pas b/src/driver/storage/fs/driver.storage.fs.fat32.pas index 83e7e494..f1b68838 100644 --- a/src/driver/storage/fs/driver.storage.fs.fat32.pas +++ b/src/driver/storage/fs/driver.storage.fs.fat32.pas @@ -50,6 +50,7 @@ begin filesystem.closeFileCallback := @FAT32CloseFile; filesystem.createDirAsyncCallback := nil; filesystem.readDirAsyncCallback := nil; + filesystem.formatParamFlags := FS_FORMAT_PARAM_CLUSTER_SIZE; driver.storage.fs.mgr.register_filesystem(@filesystem); end; diff --git a/src/driver/storage/fs/driver.storage.fs.flatfs.pas b/src/driver/storage/fs/driver.storage.fs.flatfs.pas index 133c3a30..10b53fe2 100644 --- a/src/driver/storage/fs/driver.storage.fs.flatfs.pas +++ b/src/driver/storage/fs/driver.storage.fs.flatfs.pas @@ -63,7 +63,7 @@ var procedure init; -procedure create_volume(volume : PStorage_Volume; sectors : uint32; start : uint32; config : puint32); +procedure create_volume(volume : PStorage_Volume; sectors : uint32; start : uint32; config : PFSFormatParams); procedure detect_volumes(disk : PStorage_Device); function read_directory(volume : PStorage_Volume; directory : pchar; status : PuInt32) : PLinkedListBase; procedure write_directory(volume : PStorage_Volume; directory : pchar; status : PuInt32); @@ -72,7 +72,7 @@ procedure read_file(volume : PStorage_Volume; fileName : pchar; data : PuInt32; implementation -procedure create_volume(volume : PStorage_Volume; sectors : uint32; start : uint32; config : puint32); +procedure create_volume(volume : PStorage_Volume; sectors : uint32; start : uint32; config : PFSFormatParams); var info : PDisk_Info; entryTable : PFile_Entry; @@ -102,8 +102,11 @@ begin info^.fileCount := 1000; info^.signature := $0B00B1E5; - if config^ <> 0 then begin - info^.fileCount := config^; + if (config <> nil) and + (config^.Version = FS_FORMAT_PARAMS_VERSION_1) and + ((config^.Flags and FS_FORMAT_PARAM_FILE_COUNT) <> 0) and + (config^.FileCount <> 0) then begin + info^.fileCount := config^.FileCount; end; driver.storage.mgr.storage_write(volume^.device, start, 1, PuInt32(info));// what happens if buffer is smaller than 512? @@ -975,6 +978,7 @@ begin filesystem.createcallback:= @create_volume; filesystem.detectcallback:= @detect_volumes; filesystem.identifyCallback:= @identify_volume; + filesystem.formatParamFlags := FS_FORMAT_PARAM_FILE_COUNT; // filesystem.writecallback:= @writeFile; // filesystem.readcallback := @readFile; diff --git a/src/driver/storage/vol/driver.storage.vol.mgr.pas b/src/driver/storage/vol/driver.storage.vol.mgr.pas index 673d3dae..9f27918c 100644 --- a/src/driver/storage/vol/driver.storage.vol.mgr.pas +++ b/src/driver/storage/vol/driver.storage.vol.mgr.pas @@ -45,6 +45,11 @@ var { --- Lifecycle --- } procedure init(); +function watch_lifecycle(callback : TStorageLifecycleCallback; userdata : pointer) : uint32; +procedure unwatch_lifecycle(watchId : uint32); +function volume_is_busy(volume : PStorage_Volume) : boolean; +function volume_is_formatting(volume : PStorage_Volume) : boolean; +procedure volume_set_mounted(volume : PStorage_Volume; mounted : boolean); { --- Volume queries --- } function get_volume_list() : PDList; @@ -74,8 +79,8 @@ procedure init_disk_async(device : PStorage_Device; callback : TIOCallback; callbackData : pointer); { --- Filesystem operations --- } -function format_volume(device : PStorage_Device; volIndex : uint32; filesystemName : pchar; config : puint32) : boolean; -function format_volume_async(device : PStorage_Device; volIndex : uint32; filesystemName : pchar; config : puint32; callback : TIOCallback; callbackData : pointer) : boolean; +function format_volume(device : PStorage_Device; volIndex : uint32; filesystemName : pchar; config : PFSFormatParams) : boolean; +function format_volume_async(device : PStorage_Device; volIndex : uint32; filesystemName : pchar; config : PFSFormatParams; callback : TIOCallback; callbackData : pointer) : boolean; procedure delete_volume(volume : PStorage_Volume); implementation @@ -86,17 +91,102 @@ uses proc.types; type + PStorageLifecycleWatch = ^TStorageLifecycleWatch; + TStorageLifecycleWatch = record + Active : boolean; + ID : uint32; + Callback : TStorageLifecycleCallback; + UserData : pointer; + end; + PFormatVolumeAsyncCtx = ^TFormatVolumeAsyncCtx; TFormatVolumeAsyncCtx = record Volume : PStorage_Volume; Filesystem : PFilesystem; - Config : puint32; + Config : PFSFormatParams; UserCallback : TIOCallback; UserData : pointer; end; +const + MAX_STORAGE_LIFECYCLE_WATCHES = 32; + { ===================== helpers ===================== } +var + LifecycleWatches : array[0..MAX_STORAGE_LIFECYCLE_WATCHES - 1] of TStorageLifecycleWatch; + NextLifecycleWatchID : uint32; + +procedure notify_lifecycle(event : TStorageLifecycleEvent; device : PStorage_Device; volume : PStorage_Volume; error : TError); +var + i : uint32; +begin + for i := 0 to MAX_STORAGE_LIFECYCLE_WATCHES - 1 do begin + if LifecycleWatches[i].Active and (LifecycleWatches[i].Callback <> nil) then + LifecycleWatches[i].Callback(event, device, volume, error, LifecycleWatches[i].UserData); + end; +end; + +procedure set_volume_flag(volume : PStorage_Volume; flag : uint32; enabled : boolean); +begin + if volume = nil then exit; + if enabled then + volume^.lifecycleFlags := volume^.lifecycleFlags or flag + else + volume^.lifecycleFlags := volume^.lifecycleFlags and (not flag); +end; + +function find_volume_index(volume : PStorage_Volume) : sint32; +var + i : uint32; +begin + find_volume_index := -1; + if volume = nil then exit; + if volumes = nil then exit; + if DL_Size(volumes) = 0 then exit; + for i := 0 to DL_Size(volumes) - 1 do begin + if get_volume(i) = volume then begin + find_volume_index := sint32(i); + exit; + end; + end; +end; + +procedure auto_mount_volume(volume : PStorage_Volume); +var + volIdx : sint32; + volNumStr : pchar; + volName : pchar; + prefix : pchar; + mountPath : pchar; +begin + if volume = nil then exit; + if volume^.filesystem = nil then exit; + + volIdx := find_volume_index(volume); + if volIdx < 0 then exit; + + volNumStr := intToString(uint32(volIdx)); + volName := stringConcat('vol', volNumStr); + kfree(void(volNumStr)); + prefix := stringNew(6); + prefix[0] := '/'; + prefix[1] := 'd'; + prefix[2] := 'i'; + prefix[3] := 's'; + prefix[4] := 'k'; + prefix[5] := '/'; + mountPath := stringConcat(prefix, volName); + + driver.storage.vfs.mountVolume(mountPath, volume); + if volume^.isBootDrive then + driver.storage.vfs.mountVolume('/boot', volume); + + kfree(void(volName)); + kfree(void(prefix)); + kfree(void(mountPath)); +end; + function find_partition_slot_by_start(mbr : PMaster_Boot_Record; sectorStart : uint32) : sint32; var i : uint32; @@ -215,18 +305,27 @@ begin err := eInvalidArgument; if err = eNone then begin - update_partition_system_id_sync(ctx^.Volume^.device, ctx^.Volume^.sectorStart, ctx^.Filesystem^.system_id); - ctx^.Volume^.filesystem := ctx^.Filesystem; - ctx^.Filesystem^.createCallback(ctx^.Volume, ctx^.Volume^.sectorCount, ctx^.Volume^.sectorStart, ctx^.Config); + if not driver.storage.vfs.InvalidateVolume(ctx^.Volume) then + err := eFileInUse + else begin + update_partition_system_id_sync(ctx^.Volume^.device, ctx^.Volume^.sectorStart, ctx^.Filesystem^.system_id); + ctx^.Volume^.filesystem := ctx^.Filesystem; + ctx^.Filesystem^.createCallback(ctx^.Volume, ctx^.Volume^.sectorCount, ctx^.Volume^.sectorStart, ctx^.Config); - { Re-probe the freshly formatted volume so the caller gets a verified - filesystem assignment and refreshed free-space metadata. } - ctx^.Volume^.filesystem := nil; - driver.storage.fs.mgr.probe_volume(ctx^.Volume); - if ctx^.Volume^.filesystem <> ctx^.Filesystem then - err := eUnsupportedFilesystem; + { Re-probe the freshly formatted volume so the caller gets a verified + filesystem assignment and refreshed free-space metadata. } + ctx^.Volume^.filesystem := nil; + driver.storage.fs.mgr.probe_volume(ctx^.Volume); + if ctx^.Volume^.filesystem <> ctx^.Filesystem then + err := eUnsupportedFilesystem + else + auto_mount_volume(ctx^.Volume); + end; end; + set_volume_flag(ctx^.Volume, STORAGE_VOLUME_FLAG_FORMATTING, false); + notify_lifecycle(sleVolumeFormatCompleted, ctx^.Volume^.device, ctx^.Volume, err); + if ctx^.Config <> nil then kfree(void(ctx^.Config)); if ctx^.UserCallback <> nil then @@ -249,6 +348,7 @@ begin volume^.freeSectors := 0; volume^.filesystem := nil; volume^.isBootDrive := device^.isBootDevice; + volume^.lifecycleFlags := 0; driver.storage.fs.mgr.probe_volume(volume); register_volume(device, volume); end; @@ -291,8 +391,16 @@ begin end; if found <> nil then begin - if not driver.storage.vfs.InvalidateVolume(found) then + set_volume_flag(found, STORAGE_VOLUME_FLAG_INVALIDATING, true); + notify_lifecycle(sleVolumeInvalidating, device, found, eNone); + if not driver.storage.vfs.InvalidateVolume(found) then begin + set_volume_flag(found, STORAGE_VOLUME_FLAG_INVALIDATING, false); exit; + end; + set_volume_flag(found, STORAGE_VOLUME_FLAG_INVALIDATING, false); + notify_lifecycle(sleVolumeInvalidated, device, found, eNone); + set_volume_flag(found, STORAGE_VOLUME_FLAG_REMOVED, true); + notify_lifecycle(sleVolumeRemoved, device, found, eNone); remove_volume_from_device_list(device, found); DL_Delete(volumes, i); kfree(puint32(found)); @@ -309,10 +417,17 @@ begin while i < DL_Size(volumes) do begin v := PStorage_Volume(Void(DL_Get(volumes, i))^); if v^.device = device then begin + set_volume_flag(v, STORAGE_VOLUME_FLAG_INVALIDATING, true); + notify_lifecycle(sleVolumeInvalidating, device, v, eNone); if not driver.storage.vfs.InvalidateVolume(v) then begin + set_volume_flag(v, STORAGE_VOLUME_FLAG_INVALIDATING, false); i := i + 1; continue; end; + set_volume_flag(v, STORAGE_VOLUME_FLAG_INVALIDATING, false); + notify_lifecycle(sleVolumeInvalidated, device, v, eNone); + set_volume_flag(v, STORAGE_VOLUME_FLAG_REMOVED, true); + notify_lifecycle(sleVolumeRemoved, device, v, eNone); DL_Delete(volumes, i); remove_volume_from_device_list(device, v); kfree(puint32(v)); @@ -327,6 +442,76 @@ procedure init(); begin push_trace('driver.storage.vol.mgr.init'); volumes := DL_New(sizeof(Pointer)); + memset(uint32(@LifecycleWatches[0]), 0, sizeof(LifecycleWatches)); + NextLifecycleWatchID := 1; +end; + +function watch_lifecycle(callback : TStorageLifecycleCallback; userdata : pointer) : uint32; +var + i : uint32; +begin + watch_lifecycle := 0; + if callback = nil then exit; + for i := 0 to MAX_STORAGE_LIFECYCLE_WATCHES - 1 do begin + if not LifecycleWatches[i].Active then begin + LifecycleWatches[i].Active := true; + LifecycleWatches[i].ID := NextLifecycleWatchID; + LifecycleWatches[i].Callback := callback; + LifecycleWatches[i].UserData := userdata; + watch_lifecycle := NextLifecycleWatchID; + NextLifecycleWatchID := NextLifecycleWatchID + 1; + if NextLifecycleWatchID = 0 then + NextLifecycleWatchID := 1; + exit; + end; + end; +end; + +procedure unwatch_lifecycle(watchId : uint32); +var + i : uint32; +begin + if watchId = 0 then exit; + for i := 0 to MAX_STORAGE_LIFECYCLE_WATCHES - 1 do begin + if LifecycleWatches[i].Active and (LifecycleWatches[i].ID = watchId) then begin + LifecycleWatches[i].Active := false; + LifecycleWatches[i].ID := 0; + LifecycleWatches[i].Callback := nil; + LifecycleWatches[i].UserData := nil; + exit; + end; + end; +end; + +function volume_is_busy(volume : PStorage_Volume) : boolean; +begin + volume_is_busy := false; + if volume = nil then exit; + volume_is_busy := + (volume^.lifecycleFlags and (STORAGE_VOLUME_FLAG_FORMATTING or STORAGE_VOLUME_FLAG_INVALIDATING or STORAGE_VOLUME_FLAG_REMOVED)) <> 0; +end; + +function volume_is_formatting(volume : PStorage_Volume) : boolean; +begin + volume_is_formatting := false; + if volume = nil then exit; + volume_is_formatting := (volume^.lifecycleFlags and STORAGE_VOLUME_FLAG_FORMATTING) <> 0; +end; + +procedure volume_set_mounted(volume : PStorage_Volume; mounted : boolean); +begin + if volume = nil then exit; + if mounted then begin + if (volume^.lifecycleFlags and STORAGE_VOLUME_FLAG_MOUNTED) = 0 then begin + set_volume_flag(volume, STORAGE_VOLUME_FLAG_MOUNTED, true); + notify_lifecycle(sleVolumeMounted, volume^.device, volume, eNone); + end; + end else begin + if (volume^.lifecycleFlags and STORAGE_VOLUME_FLAG_MOUNTED) <> 0 then begin + set_volume_flag(volume, STORAGE_VOLUME_FLAG_MOUNTED, false); + notify_lifecycle(sleVolumeUnmounted, volume^.device, volume, eNone); + end; + end; end; { ===================== Volume queries ===================== } @@ -346,6 +531,7 @@ begin DL_Add(device^.volumes); DL_Set(device^.volumes, DL_Size(device^.volumes) - 1, puint32(@volPtr)); end; + notify_lifecycle(sleVolumeAdded, device, volume, eNone); end; function get_volume_list() : PDList; @@ -756,7 +942,7 @@ end; { ===================== Filesystem operations ===================== } -function format_volume(device : PStorage_Device; volIndex : uint32; filesystemName : pchar; config : puint32) : boolean; +function format_volume(device : PStorage_Device; volIndex : uint32; filesystemName : pchar; config : PFSFormatParams) : boolean; var vol : PStorage_Volume; fs : PFilesystem; @@ -771,6 +957,15 @@ begin fs := driver.storage.fs.mgr.find_filesystem_by_name(filesystemName); if fs = nil then exit; if fs^.createCallback = nil then exit; + if volume_is_busy(vol) then exit; + + set_volume_flag(vol, STORAGE_VOLUME_FLAG_FORMATTING, true); + notify_lifecycle(sleVolumeFormatStarted, device, vol, eNone); + if not driver.storage.vfs.InvalidateVolume(vol) then begin + set_volume_flag(vol, STORAGE_VOLUME_FLAG_FORMATTING, false); + notify_lifecycle(sleVolumeFormatCompleted, device, vol, eFileInUse); + exit; + end; update_partition_system_id_sync(device, vol^.sectorStart, fs^.system_id); vol^.filesystem := fs; @@ -778,15 +973,22 @@ begin vol^.filesystem := nil; driver.storage.fs.mgr.probe_volume(vol); format_volume := vol^.filesystem = fs; + if format_volume then + auto_mount_volume(vol); + set_volume_flag(vol, STORAGE_VOLUME_FLAG_FORMATTING, false); + if format_volume then + notify_lifecycle(sleVolumeFormatCompleted, device, vol, eNone) + else + notify_lifecycle(sleVolumeFormatCompleted, device, vol, eUnsupportedFilesystem); end; function format_volume_async(device : PStorage_Device; volIndex : uint32; filesystemName : pchar; - config : puint32; callback : TIOCallback; callbackData : pointer) : boolean; + config : PFSFormatParams; callback : TIOCallback; callbackData : pointer) : boolean; var vol : PStorage_Volume; fs : PFilesystem; ctx : PFormatVolumeAsyncCtx; - cfgCopy : puint32; + cfgCopy : PFSFormatParams; begin push_trace('driver.storage.vol.mgr.format_volume_async'); format_volume_async := false; @@ -798,15 +1000,19 @@ begin fs := driver.storage.fs.mgr.find_filesystem_by_name(filesystemName); if fs = nil then exit; if fs^.createCallback = nil then exit; + if volume_is_busy(vol) then begin + if callback <> nil then callback(eFileInUse, callbackData); + exit; + end; cfgCopy := nil; if config <> nil then begin - cfgCopy := puint32(kalloc(sizeof(uint32))); + cfgCopy := PFSFormatParams(kalloc(sizeof(TFSFormatParams))); if cfgCopy = nil then begin if callback <> nil then callback(eOutOfMemory, callbackData); exit; end; - cfgCopy^ := config^; + memcpy(uint32(config), uint32(cfgCopy), sizeof(TFSFormatParams)); end; ctx := PFormatVolumeAsyncCtx(kalloc(sizeof(TFormatVolumeAsyncCtx))); @@ -821,7 +1027,12 @@ begin ctx^.UserCallback := callback; ctx^.UserData := callbackData; + set_volume_flag(vol, STORAGE_VOLUME_FLAG_FORMATTING, true); + notify_lifecycle(sleVolumeFormatStarted, device, vol, eNone); + if proc.mgr.create('vol.fmt', @format_volume_worker, void(ctx), 1) = nil then begin + set_volume_flag(vol, STORAGE_VOLUME_FLAG_FORMATTING, false); + notify_lifecycle(sleVolumeFormatCompleted, device, vol, eOutOfMemory); if cfgCopy <> nil then kfree(void(cfgCopy)); kfree(void(ctx)); if callback <> nil then callback(eOutOfMemory, callbackData); diff --git a/src/proc/proc.mgr.pas b/src/proc/proc.mgr.pas index 7ef253d0..83dfb486 100644 --- a/src/proc/proc.mgr.pas +++ b/src/proc/proc.mgr.pas @@ -119,11 +119,17 @@ var prio : uint8; begin push_trace('proc.mgr.create'); + create := nil; if priority = 0 then prio := 1 else prio := priority; { Allocate process context } ctx := PProcessContext(kalloc(SizeOf(TProcessContext))); + if ctx = nil then begin + io.syslog.logln('PROCMGR', 'Failed to allocate process context.'); + pop_trace; + exit; + end; memset(uint32(ctx), 0, SizeOf(TProcessContext)); { Identity } @@ -154,15 +160,45 @@ begin { Resources } ctx^.Resources := void(DL_New(SizeOf(TResourceBinding))); + if ctx^.Resources = nil then begin + io.syslog.logln('PROCMGR', 'Failed to allocate resource list.'); + kfree(void(ctx)); + pop_trace; + exit; + end; { Per-process file descriptor table } ctx^.FDTable := void(fd_table_new); + if ctx^.FDTable = nil then begin + io.syslog.logln('PROCMGR', 'Failed to allocate FD table.'); + DL_Free(PDList(ctx^.Resources)); + kfree(void(ctx)); + pop_trace; + exit; + end; { Per-process working directory } ctx^.Cwd := stringCopy('/'); + if ctx^.Cwd = nil then begin + io.syslog.logln('PROCMGR', 'Failed to allocate working directory.'); + fd_table_free(PFDTable(ctx^.FDTable)); + DL_Free(PDList(ctx^.Resources)); + kfree(void(ctx)); + pop_trace; + exit; + end; { Allocate per-process core.version stack } stack := kalloc(PROCESS_STACK_SIZE); + if stack = nil then begin + io.syslog.logln('PROCMGR', 'Failed to allocate process stack.'); + kfree(void(ctx^.Cwd)); + fd_table_free(PFDTable(ctx^.FDTable)); + DL_Free(PDList(ctx^.Resources)); + kfree(void(ctx)); + pop_trace; + exit; + end; ctx^.StackBase := stack; ctx^.StackTop := uint32(stack) + PROCESS_STACK_SIZE; @@ -215,6 +251,21 @@ begin { Add to process table } asm pushf; cli end; slotPtr := DL_Add(Processes); + if slotPtr = nil then begin + asm popf end; + io.syslog.logln('PROCMGR', 'Failed to allocate process table slot.'); + if ctx^.StackBase <> nil then + kfree(ctx^.StackBase); + if ctx^.Cwd <> nil then + kfree(void(ctx^.Cwd)); + if ctx^.FDTable <> nil then + fd_table_free(PFDTable(ctx^.FDTable)); + if ctx^.Resources <> nil then + DL_Free(PDList(ctx^.Resources)); + kfree(void(ctx)); + pop_trace; + exit; + end; slotPtr^ := uint32(ctx); { Mark as ready for scheduling } -- 2.54.0 From 096db148d078edba6bd034a7eacf7ebeefcceac0 Mon Sep 17 00:00:00 2001 From: Aaron Date: Wed, 18 Mar 2026 21:05:36 +0000 Subject: [PATCH 07/21] fat32: fix pre-alloc cluster leak; add write-handle ref-count - Remove FAT_WRITE_PREALLOC_BYTES (redundant at typical cluster sizes) - Simplify fatPreallocClusterCount to always return MAX_CLUSTERS - FAT32CloseFile: truncate pre-allocated tail clusters back to ByteSize on close, freeing FAT chain entries beyond the file's real size - Add TFATWriteRef + WriteRefs[32] in TFATVolumeCtx and Registered flag on TFATOpenFile to track concurrent write handles per dir entry - fatRegisterWriteHandle / fatUnregisterWriteHandle: CLI-guarded slot scan keyed by (DirLoc.SectorLBA, DirLoc.EntryIdx) - Register on first FAT chain extension in FAT32EnsureCapacityForWrite - Truncation in FAT32CloseFile skipped unless this is the last writer (prevents RunMap corruption in concurrent-open scenarios) - Add forward decl for fatFreeClusterChain to fix ordering issue - app.iotest: update default path to /disk/vol2/IOTEST.BIN --- src/app/app.iotest.pas | 2 +- .../ctl/ahci/driver.storage.ctl.ahci.pas | 10 +- .../fs/fat32/driver.storage.fs.fat32.core.pas | 148 +++- .../{ => fat32}/driver.storage.fs.fat32.pas | 6 +- .../fat32/driver.storage.fs.fat32.types.pas | 19 +- .../fs/fat32/driver.storage.fs.fat32.vol.pas | 660 ++++++++++++++++++ 6 files changed, 818 insertions(+), 27 deletions(-) rename src/driver/storage/fs/{ => fat32}/driver.storage.fs.fat32.pas (93%) create mode 100644 src/driver/storage/fs/fat32/driver.storage.fs.fat32.vol.pas diff --git a/src/app/app.iotest.pas b/src/app/app.iotest.pas index 0ead3324..4156a96d 100644 --- a/src/app/app.iotest.pas +++ b/src/app/app.iotest.pas @@ -34,7 +34,7 @@ const TICK_HZ = 1024; { timer ISR frequency } IO_WAIT_TIMEOUT_TICKS = 10 * TICK_HZ; DELETE_TIMEOUT_TICKS = 5 * TICK_HZ; - DEFAULT_PATH : pchar = '/disk/vol0/IOTEST.BIN'; + DEFAULT_PATH : pchar = '/disk/vol2/IOTEST.BIN'; type TIOTSyncWaitCtx = record diff --git a/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas b/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas index d449211c..745d7c2f 100644 --- a/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas +++ b/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas @@ -69,7 +69,7 @@ function send_read_capacity(device : PAHCI_Device; sectorCount : puint32; blockS implementation const - AHCI_ENABLE_NCQ = false; + AHCI_ENABLE_NCQ = true; function min_u32(a : uint32; b : uint32) : uint32; begin @@ -421,15 +421,15 @@ begin start_port(device^.port); { Re-enable port interrupts and bail — device is non-functional. } - device^.port^.int_enable := $40000003; + device^.port^.int_enable := $4000000B; kfree(buffer); exit; end; { Re-enable port interrupts now that IDENTIFY polling is done. - $40000003 = bit 0 (D2H Register FIS) | bit 1 (PIO Setup FIS) | bit 30 (Task File Error) - These three bits cover all normal completions and hardware error events. } - device^.port^.int_enable := $40000003; + $4000000B = bit 0 (D2H Register FIS) | bit 1 (PIO Setup FIS) | bit 3 (Set Device Bits / NCQ) | bit 30 (Task File Error) + Bit 3 is required for NCQ completion notifications via SDB FIS. } + device^.port^.int_enable := $4000000B; b8 := puint16(buffer); memcpy(uint32(buffer), uint32(@device^.ata_info[0]), 512); diff --git a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.core.pas b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.core.pas index 712081d7..2d2ef953 100644 --- a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.core.pas +++ b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.core.pas @@ -8,6 +8,7 @@ uses driver.storage.mgr, driver.storage.types, core.strings, + core.util, arch.x86.util, debug.tracer, io.syslog, @@ -59,6 +60,7 @@ var function readFat(volume : PStorage_Volume; cluster : uint32; bootRecord : PBootRecord) : uint32; forward; procedure writeFat(volume : PStorage_Volume; cluster : uint32; value : uint32; bootRecord : PBootRecord); forward; +procedure fatFreeClusterChain(volume : PStorage_Volume; info : PFATVolumeInfo; startCluster : uint32); forward; function isEndOfChain(value : uint32) : boolean; begin @@ -105,20 +107,66 @@ begin fatClustersForBytes := (byteCount + bytesPerCluster - 1) div bytesPerCluster; end; +{ Register ofi in the volume write-ref table (call once DirLoc is valid). } +procedure fatRegisterWriteHandle(volCtx : PFATVolumeCtx; ofi : PFATOpenFile); +var + i : uint32; + freeSlot : sint32; +begin + if (volCtx = nil) or (ofi = nil) or ofi^.Registered then exit; + asm pushf; cli end; + freeSlot := -1; + for i := 0 to FAT_OPEN_HANDLE_SLOTS - 1 do begin + if (volCtx^.WriteRefs[i].Count > 0) and + (volCtx^.WriteRefs[i].SectorLBA = ofi^.DirLoc.SectorLBA) and + (volCtx^.WriteRefs[i].EntryIdx = ofi^.DirLoc.EntryIdx) then begin + volCtx^.WriteRefs[i].Count := volCtx^.WriteRefs[i].Count + 1; + ofi^.Registered := true; + asm popf end; + exit; + end; + if (freeSlot < 0) and (volCtx^.WriteRefs[i].Count = 0) then + freeSlot := sint32(i); + end; + if freeSlot >= 0 then begin + volCtx^.WriteRefs[uint32(freeSlot)].SectorLBA := ofi^.DirLoc.SectorLBA; + volCtx^.WriteRefs[uint32(freeSlot)].EntryIdx := ofi^.DirLoc.EntryIdx; + volCtx^.WriteRefs[uint32(freeSlot)].Count := 1; + ofi^.Registered := true; + end; + { If all slots are full, Registered stays false → truncation skipped for safety } + asm popf end; +end; + +{ Unregister ofi and return the count that was present before decrement. } +function fatUnregisterWriteHandle(volCtx : PFATVolumeCtx; ofi : PFATOpenFile) : uint32; +var + i : uint32; +begin + fatUnregisterWriteHandle := 0; + if (volCtx = nil) or (ofi = nil) or not ofi^.Registered then exit; + asm pushf; cli end; + for i := 0 to FAT_OPEN_HANDLE_SLOTS - 1 do begin + if (volCtx^.WriteRefs[i].Count > 0) and + (volCtx^.WriteRefs[i].SectorLBA = ofi^.DirLoc.SectorLBA) and + (volCtx^.WriteRefs[i].EntryIdx = ofi^.DirLoc.EntryIdx) then begin + fatUnregisterWriteHandle := volCtx^.WriteRefs[i].Count; + volCtx^.WriteRefs[i].Count := volCtx^.WriteRefs[i].Count - 1; + ofi^.Registered := false; + asm popf end; + exit; + end; + end; + asm popf end; +end; + function fatPreallocClusterCount(bytesPerCluster : uint32) : uint32; begin - if bytesPerCluster = 0 then begin - fatPreallocClusterCount := 1; - exit; - end; - - fatPreallocClusterCount := FAT_WRITE_PREALLOC_BYTES div bytesPerCluster; - if fatPreallocClusterCount = 0 then - fatPreallocClusterCount := 1; + { Always pre-allocate FAT_WRITE_PREALLOC_MAX_CLUSTERS. The MIN guard + keeps the value sane if the constant is ever lowered below MIN. } + fatPreallocClusterCount := FAT_WRITE_PREALLOC_MAX_CLUSTERS; if fatPreallocClusterCount < FAT_WRITE_PREALLOC_MIN_CLUSTERS then fatPreallocClusterCount := FAT_WRITE_PREALLOC_MIN_CLUSTERS; - if fatPreallocClusterCount > FAT_WRITE_PREALLOC_MAX_CLUSTERS then - fatPreallocClusterCount := FAT_WRITE_PREALLOC_MAX_CLUSTERS; end; function isValidFAT32Name(fullName : pchar) : boolean; @@ -781,6 +829,10 @@ var w : uint32; begin ctx := FAT32GetVolumeCtx(volume); + if ctx = nil then begin + readFat := 0; + exit; + end; cache := @ctx^.Cache; fatSecIdx := cluster div 128; setIdx := fatSecIdx mod FAT_CACHE_SETS; @@ -870,6 +922,7 @@ var w : uint32; begin ctx := FAT32GetVolumeCtx(volume); + if ctx = nil then exit; cache := @ctx^.Cache; fatSecIdx := cluster div 128; setIdx := fatSecIdx mod FAT_CACHE_SETS; @@ -1617,6 +1670,10 @@ begin if FAT32EnsureCapacityForWrite <> eNone then exit; + { Register as a writer the first time we extend this file (DirLoc is now valid) } + if not ofi^.Registered then + fatRegisterWriteHandle(FAT32GetVolumeCtx(ofi^.Volume), ofi); + newEnd := offset + byteCount; needClusters := fatClustersForBytes(newEnd, ofi^.VolumeInfo^.BytesPerCluster); targetClusters := needClusters; @@ -1778,13 +1835,78 @@ end; procedure FAT32CloseFile(ctx : pointer); var - ofi : PFATOpenFile; - sectorBuf : puint32; - rawDir : PDirectory; + ofi : PFATOpenFile; + sectorBuf : puint32; + rawDir : PDirectory; + keepClusters : uint32; + walked : uint32; + ri : uint32; + tailCluster : uint32; + nextCluster : uint32; + runOff : uint32; + volCtx : PFATVolumeCtx; + oldCount : uint32; + isLastWriter : boolean; begin if ctx = nil then exit; ofi := PFATOpenFile(ctx); + { Determine whether this is the last writer for this file. + Unregister unconditionally so the slot is freed even when we skip truncation. } + isLastWriter := true; + if ofi^.Registered then begin + volCtx := FAT32GetVolumeCtx(ofi^.Volume); + oldCount := fatUnregisterWriteHandle(volCtx, ofi); + isLastWriter := (oldCount <= 1); + end; + + { Truncate pre-allocated tail clusters back to ByteSize before flushing. + Only done when this is the last writer — another open handle may still + be expanding the file and relies on the pre-allocated chain. } + if isLastWriter and (ofi^.VolumeInfo <> nil) and (ofi^.FatDirty) and + (ofi^.Volume <> nil) and (ofi^.AllocClusters > 0) then begin + keepClusters := fatClustersForBytes(ofi^.ByteSize, ofi^.VolumeInfo^.BytesPerCluster); + if keepClusters < ofi^.AllocClusters then begin + { Walk RunMap to find the last cluster we are keeping (1-based) } + walked := 0; + tailCluster := 0; + nextCluster := 0; + if keepClusters = 0 then begin + { File has no data — free the entire chain } + if ofi^.RunMap.Count > 0 then begin + nextCluster := ofi^.RunMap.Runs[0].StartCluster; + fatFreeClusterChain(ofi^.Volume, ofi^.VolumeInfo, nextCluster); + end; + ofi^.FirstCluster := 0; + end else begin + ri := 0; + while ri < ofi^.RunMap.Count do begin + if walked + ofi^.RunMap.Runs[ri].ClusterCount >= keepClusters then begin + { The new tail is inside this run } + runOff := keepClusters - walked - 1; + tailCluster := ofi^.RunMap.Runs[ri].StartCluster + runOff; + { First cluster to free is the one after the new tail } + if runOff + 1 < ofi^.RunMap.Runs[ri].ClusterCount then + nextCluster := ofi^.RunMap.Runs[ri].StartCluster + runOff + 1 + else if ri + 1 < ofi^.RunMap.Count then + nextCluster := ofi^.RunMap.Runs[ri + 1].StartCluster + else + nextCluster := 0; + break; + end; + walked := walked + ofi^.RunMap.Runs[ri].ClusterCount; + ri := ri + 1; + end; + if tailCluster >= 2 then + writeFat(ofi^.Volume, tailCluster, $0FFFFFF8, @ofi^.VolumeInfo^.BootRecord); + if nextCluster >= 2 then + fatFreeClusterChain(ofi^.Volume, ofi^.VolumeInfo, nextCluster); + end; + ofi^.AllocClusters := keepClusters; + ofi^.FatDirty := true; + end; + end; + if ofi^.FatDirty and (ofi^.Volume <> nil) then FAT32Flush(ofi^.Volume); diff --git a/src/driver/storage/fs/driver.storage.fs.fat32.pas b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.pas similarity index 93% rename from src/driver/storage/fs/driver.storage.fs.fat32.pas rename to src/driver/storage/fs/fat32/driver.storage.fs.fat32.pas index f1b68838..dcd461b4 100644 --- a/src/driver/storage/fs/driver.storage.fs.fat32.pas +++ b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.pas @@ -14,7 +14,8 @@ implementation uses io.syslog, - driver.storage.fs.fat32.old, + core.util, + driver.storage.fs.fat32.vol, driver.storage.fs.fat32.core, driver.storage.fs.fat32.transfer; @@ -23,13 +24,13 @@ var procedure FAT32ReleaseVolumeState(volume : PStorage_Volume); begin - FAT32LegacyReleaseVolumeCtx(volume); FAT32ReleaseVolumeCtx(volume); end; procedure init; begin io.syslog.logln('FAT32', 'init: registering FAT32 filesystem'); + memset(uint32(@filesystem), 0, sizeof(TFilesystem)); filesystem.sName := 'FAT32'; filesystem.system_id := $0B; filesystem.readDirCallback := @FAT32ReadDirectory; @@ -52,6 +53,7 @@ begin filesystem.readDirAsyncCallback := nil; filesystem.formatParamFlags := FS_FORMAT_PARAM_CLUSTER_SIZE; + FAT32VolInit(@filesystem); driver.storage.fs.mgr.register_filesystem(@filesystem); end; diff --git a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.types.pas b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.types.pas index 1681410a..38e2bd83 100644 --- a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.types.pas +++ b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.types.pas @@ -67,17 +67,16 @@ type end; const - FAT_CACHE_SETS = 512; - FAT_CACHE_WAYS = 2; + FAT_CACHE_SETS = 1024; + FAT_CACHE_WAYS = 4; FAT_CACHE_LINES = FAT_CACHE_SETS * FAT_CACHE_WAYS; FAT_TRANSFER_POOL_CAPACITY = 64; FAT_ALLOC_HINT_SLOTS = 8; - FAT_DIR_HINT_SLOTS = 16; - FAT_WRITE_PREALLOC_BYTES = 16 * 1024 * 1024; + FAT_DIR_HINT_SLOTS = 8; FAT_WRITE_PREALLOC_MAX_CLUSTERS = 2048; FAT_WRITE_PREALLOC_MIN_CLUSTERS = 64; - FAT_MAX_IO_SECTORS = 2048; - + FAT_MAX_IO_SECTORS = 2048 * 2; + FAT_OPEN_HANDLE_SLOTS = 65000; type TFATCacheLine = record SectorIdx : uint32; @@ -162,6 +161,13 @@ type Exists : boolean; FatDirty : boolean; MetaDirty : boolean; + Registered : boolean; { true once registered in the volume write-ref table } + end; + + TFATWriteRef = record + SectorLBA : uint32; + EntryIdx : uint32; + Count : uint32; { number of write handles with this dir entry open } end; TFATTransferMode = (ftmRead, ftmWrite); @@ -215,6 +221,7 @@ type HintStamp : uint32; AllocHints : array[0..FAT_ALLOC_HINT_SLOTS - 1] of TFATAllocHint; DirHints : array[0..FAT_DIR_HINT_SLOTS - 1] of TFATDirHint; + WriteRefs : array[0..FAT_OPEN_HANDLE_SLOTS - 1] of TFATWriteRef; end; implementation diff --git a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.vol.pas b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.vol.pas new file mode 100644 index 00000000..dd6d7269 --- /dev/null +++ b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.vol.pas @@ -0,0 +1,660 @@ +unit driver.storage.fs.fat32.vol; + +{ + Volume lifecycle functions for the FAT32 filesystem driver. + Handles volume detection, identification, and formatting. +} + +interface + +uses + driver.storage.types, + driver.storage.fs.fat32.types; + +procedure FAT32VolInit(fs : PFilesystem); +procedure detect_volumes(disk : PStorage_Device); +function identify_volume(volume : PStorage_Volume) : boolean; +procedure create_volume(volume : PStorage_Volume; sectors : uint32; start : uint32; config : PFSFormatParams); +procedure create_volume_async(volume : PStorage_Volume; sectors : uint32; start : uint32; + config : PFSFormatParams; callback : TIOCallback; callbackData : pointer); + +implementation + +uses + io.syslog, + debug.tracer, + memory.heap, + core.util, arch.x86.util, + driver.storage.mgr, + driver.storage.vol.mgr, + driver.storage.fs.fat32.core; + +{ ===================== Types ===================== } + +type + TFmtStep = (fmtBootSector, fmtZeroFAT, fmtFATEntries, fmtRootDir, fmtSystemDir, fmtDone); + + PFmtContext = ^TFmtContext; + TFmtContext = record + Volume : PStorage_Volume; + Disk : PStorage_Device; + SectorStart : uint32; + RootCluster : uint32; + Callback : TIOCallback; + CallbackData : pointer; + Buffer : puint32; + ZeroBuffer : puint32; + SPC : uint32; + FATSize : uint32; + FATStart : uint32; + DataStart : uint32; + BatchSize : uint32; + BatchPos : uint32; + Step : TFmtStep; + end; + +{ ===================== Module state ===================== } + +var + fat32FilesystemPtr : PFilesystem = nil; + +{ ===================== Private helpers ===================== } + +function bootSectorHasSignature(buffer : puint32) : boolean; +begin + bootSectorHasSignature := (puint8(uint32(buffer) + 510)^ = $55) and + (puint8(uint32(buffer) + 511)^ = $AA); +end; + +function clusterToLBA(dataStart : uint32; spc : uint32; cluster : uint32) : uint32; +begin + if cluster < 2 then + clusterToLBA := dataStart + else + clusterToLBA := dataStart + ((cluster - 2) * spc); +end; + +function fatDefaultSPC(sectors : uint32; sectorSize : uint32) : uint32; +begin + if sectorSize = 0 then begin + fatDefaultSPC := 1; + exit; + end; + fatDefaultSPC := 4096 div sectorSize; + if fatDefaultSPC = 0 then + fatDefaultSPC := 1; + if fatDefaultSPC > 64 then + fatDefaultSPC := 64; +end; + +function fatComputeFATSize(sectors : uint32; spc : uint32; sectorSize : uint32) : uint32; +var + clusterCount : uint32; + fatBytes : uint32; +begin + if spc = 0 then spc := 1; + if sectorSize = 0 then begin + fatComputeFATSize := 0; + exit; + end; + clusterCount := (sectors + spc - 1) div spc; + fatBytes := clusterCount * 4; + fatComputeFATSize := (fatBytes + sectorSize - 1) div sectorSize; + if fatComputeFATSize = 0 then + fatComputeFATSize := 1; +end; + +function fatFormatConfigSPC(config : PFSFormatParams; sectors : uint32; sectorSize : uint32) : uint32; +begin + fatFormatConfigSPC := 0; + if (config <> nil) and + (config^.Version = FS_FORMAT_PARAMS_VERSION_1) and + ((config^.Flags and FS_FORMAT_PARAM_CLUSTER_SIZE) <> 0) then + fatFormatConfigSPC := config^.ClusterSize; + if fatFormatConfigSPC = 0 then + fatFormatConfigSPC := fatDefaultSPC(sectors, sectorSize); + if fatFormatConfigSPC = 0 then + fatFormatConfigSPC := 1; +end; + +{ Counts free FAT clusters by scanning the FAT directly (no cache). + Used during detect/identify to compute freeSectors. } +function countFreeFATClusters(volume : PStorage_Volume; bootRecord : PBootRecord) : uint32; +var + fatStart : uint32; + maxCluster : uint32; + entriesPerSect : uint32; + sectorIdx : uint32; + entryIdx : uint32; + clusterNum : uint32; + freeCount : uint32; + fatSectors : uint32; + fatBuffer : puint32; +begin + countFreeFATClusters := 0; + if (bootRecord^.sectorSize = 0) or (bootRecord^.FATSize = 0) then exit; + + fatStart := volume^.sectorStart + bootRecord^.rsvSectors; + maxCluster := (bootRecord^.FATSize * bootRecord^.sectorSize) div 4; + entriesPerSect := bootRecord^.sectorSize div 4; + fatSectors := bootRecord^.FATSize; + + fatBuffer := puint32(kalloc(bootRecord^.sectorSize)); + if fatBuffer = nil then exit; + freeCount := 0; + + if fatSectors > 0 then + for sectorIdx := 0 to fatSectors - 1 do begin + if sectorIdx * entriesPerSect >= maxCluster then break; + driver.storage.mgr.storage_read(volume^.device, fatStart + sectorIdx, 1, fatBuffer); + if entriesPerSect > 0 then + for entryIdx := 0 to entriesPerSect - 1 do begin + clusterNum := sectorIdx * entriesPerSect + entryIdx; + if clusterNum < 2 then continue; + if clusterNum >= maxCluster then break; + if fatBuffer[entryIdx] = 0 then + freeCount := freeCount + 1; + end; + end; + + kfree(fatBuffer); + countFreeFATClusters := freeCount; +end; + +{ ===================== Async format state machine ===================== } + +procedure fmt_run_next(ctx : PFmtContext); forward; + +procedure fmt_step_complete(error : TError; userdata : pointer); +var + ctx : PFmtContext; +begin + ctx := PFmtContext(userdata); + if error <> eNone then begin + io.syslog.logln('FAT32', 'fmt_step_complete: I/O error — aborting format'); + if ctx^.Buffer <> nil then kfree(ctx^.Buffer); + if ctx^.ZeroBuffer <> nil then kfree(ctx^.ZeroBuffer); + if ctx^.Callback <> nil then + ctx^.Callback(error, ctx^.CallbackData); + kfree(puint32(ctx)); + exit; + end; + + case ctx^.Step of + fmtBootSector: begin + ctx^.Step := fmtZeroFAT; + ctx^.BatchPos := 0; + end; + fmtZeroFAT: begin + ctx^.BatchPos := ctx^.BatchPos + ctx^.BatchSize; + end; + fmtFATEntries: begin + ctx^.Step := fmtRootDir; + end; + fmtRootDir: begin + ctx^.Step := fmtSystemDir; + end; + fmtSystemDir: begin + ctx^.Step := fmtDone; + end; + end; + fmt_run_next(ctx); +end; + +procedure fmt_run_next(ctx : PFmtContext); +var + writeCount : uint32; +begin + case ctx^.Step of + fmtBootSector: begin + io.syslog.logln('FAT32', 'fmt: fmtBootSector'); + driver.storage.mgr.storage_write_async(ctx^.Disk, ctx^.SectorStart, 1, + ctx^.Buffer, @fmt_step_complete, pointer(ctx)); + end; + + fmtZeroFAT: begin + { Transition to next step once the FAT is fully zeroed } + if ctx^.BatchPos >= ctx^.FATSize then begin + io.syslog.logln('FAT32', 'fmt: FAT zero complete, moving to fmtFATEntries'); + kfree(ctx^.ZeroBuffer); + ctx^.ZeroBuffer := nil; + kfree(ctx^.Buffer); + ctx^.Buffer := puint32(kalloc(ctx^.Disk^.sectorSize)); + memset(uint32(ctx^.Buffer), 0, ctx^.Disk^.sectorSize); + { FAT entries: [0]=media end-of-partition, [1]=EOC, [2]=root cluster EOC, [3]=SYSTEM EOC } + puint32(ctx^.Buffer)[0] := $0FFFFFF8; + puint32(ctx^.Buffer)[1] := $0FFFFFFF; + puint32(ctx^.Buffer)[2] := $0FFFFFF8; + puint32(ctx^.Buffer)[3] := $0FFFFFF8; + ctx^.Step := fmtFATEntries; + fmt_run_next(ctx); + exit; + end; + if (ctx^.FATSize - ctx^.BatchPos) >= ctx^.BatchSize then + writeCount := ctx^.BatchSize + else + writeCount := ctx^.FATSize - ctx^.BatchPos; + driver.storage.mgr.storage_write_async(ctx^.Disk, + ctx^.FATStart + ctx^.BatchPos, writeCount, ctx^.ZeroBuffer, + @fmt_step_complete, pointer(ctx)); + end; + + fmtFATEntries: begin + io.syslog.logln('FAT32', 'fmt: fmtFATEntries'); + driver.storage.mgr.storage_write_async(ctx^.Disk, ctx^.FATStart, 1, + ctx^.Buffer, @fmt_step_complete, pointer(ctx)); + end; + + fmtRootDir: begin + io.syslog.logln('FAT32', 'fmt: fmtRootDir'); + memset(uint32(ctx^.Buffer), 0, ctx^.Disk^.sectorSize); + { '.' entry — volume label } + PDirectory(ctx^.Buffer)[0].fileName[0] := '.'; + PDirectory(ctx^.Buffer)[0].fileName[1] := ' '; + PDirectory(ctx^.Buffer)[0].fileName[2] := ' '; + PDirectory(ctx^.Buffer)[0].fileName[3] := ' '; + PDirectory(ctx^.Buffer)[0].fileName[4] := ' '; + PDirectory(ctx^.Buffer)[0].fileName[5] := ' '; + PDirectory(ctx^.Buffer)[0].fileName[6] := ' '; + PDirectory(ctx^.Buffer)[0].fileName[7] := ' '; + PDirectory(ctx^.Buffer)[0].attributes := $08; + PDirectory(ctx^.Buffer)[0].clusterLow := ctx^.RootCluster; + { '..' entry } + PDirectory(ctx^.Buffer)[1].fileName[0] := '.'; + PDirectory(ctx^.Buffer)[1].fileName[1] := '.'; + PDirectory(ctx^.Buffer)[1].fileName[2] := ' '; + PDirectory(ctx^.Buffer)[1].fileName[3] := ' '; + PDirectory(ctx^.Buffer)[1].fileName[4] := ' '; + PDirectory(ctx^.Buffer)[1].fileName[5] := ' '; + PDirectory(ctx^.Buffer)[1].fileName[6] := ' '; + PDirectory(ctx^.Buffer)[1].fileName[7] := ' '; + PDirectory(ctx^.Buffer)[1].attributes := $10; + PDirectory(ctx^.Buffer)[1].clusterLow := ctx^.RootCluster; + { Pre-allocated SYSTEM dir entry (cluster 3) } + PDirectory(ctx^.Buffer)[2].fileName[0] := 'S'; + PDirectory(ctx^.Buffer)[2].fileName[1] := 'Y'; + PDirectory(ctx^.Buffer)[2].fileName[2] := 'S'; + PDirectory(ctx^.Buffer)[2].fileName[3] := 'T'; + PDirectory(ctx^.Buffer)[2].fileName[4] := 'E'; + PDirectory(ctx^.Buffer)[2].fileName[5] := 'M'; + PDirectory(ctx^.Buffer)[2].fileName[6] := ' '; + PDirectory(ctx^.Buffer)[2].fileName[7] := ' '; + PDirectory(ctx^.Buffer)[2].attributes := $10; + PDirectory(ctx^.Buffer)[2].clusterLow := 3; + driver.storage.mgr.storage_write_async(ctx^.Disk, + clusterToLBA(ctx^.DataStart, ctx^.SPC, ctx^.RootCluster), 1, ctx^.Buffer, + @fmt_step_complete, pointer(ctx)); + end; + + fmtSystemDir: begin + io.syslog.logln('FAT32', 'fmt: fmtSystemDir'); + memset(uint32(ctx^.Buffer), 0, ctx^.Disk^.sectorSize); + PDirectory(ctx^.Buffer)[0].fileName[0] := '.'; + PDirectory(ctx^.Buffer)[0].fileName[1] := ' '; + PDirectory(ctx^.Buffer)[0].fileName[2] := ' '; + PDirectory(ctx^.Buffer)[0].fileName[3] := ' '; + PDirectory(ctx^.Buffer)[0].fileName[4] := ' '; + PDirectory(ctx^.Buffer)[0].fileName[5] := ' '; + PDirectory(ctx^.Buffer)[0].fileName[6] := ' '; + PDirectory(ctx^.Buffer)[0].fileName[7] := ' '; + PDirectory(ctx^.Buffer)[0].attributes := $10; + PDirectory(ctx^.Buffer)[0].clusterLow := 3; + PDirectory(ctx^.Buffer)[1].fileName[0] := '.'; + PDirectory(ctx^.Buffer)[1].fileName[1] := '.'; + PDirectory(ctx^.Buffer)[1].fileName[2] := ' '; + PDirectory(ctx^.Buffer)[1].fileName[3] := ' '; + PDirectory(ctx^.Buffer)[1].fileName[4] := ' '; + PDirectory(ctx^.Buffer)[1].fileName[5] := ' '; + PDirectory(ctx^.Buffer)[1].fileName[6] := ' '; + PDirectory(ctx^.Buffer)[1].fileName[7] := ' '; + PDirectory(ctx^.Buffer)[1].attributes := $10; + PDirectory(ctx^.Buffer)[1].clusterLow := ctx^.RootCluster; + driver.storage.mgr.storage_write_async(ctx^.Disk, + clusterToLBA(ctx^.DataStart, ctx^.SPC, 3), 1, ctx^.Buffer, + @fmt_step_complete, pointer(ctx)); + end; + + fmtDone: begin + io.syslog.logln('FAT32', 'fmt: fmtDone — format complete'); + kfree(ctx^.Buffer); + ctx^.Buffer := nil; + if ctx^.Callback <> nil then + ctx^.Callback(eNone, ctx^.CallbackData); + kfree(puint32(ctx)); + end; + end; +end; + +{ ===================== Exported procedures ===================== } + +procedure FAT32VolInit(fs : PFilesystem); +begin + fat32FilesystemPtr := fs; +end; + +procedure detect_volumes(disk : PStorage_Device); +var + buffer : puint32; + bufSize : uint32; + volume : PStorage_Volume; + br : PBootRecord; +begin + push_trace('driver.storage.fs.fat32.detect_volumes'); + if fat32FilesystemPtr = nil then exit; + + bufSize := disk^.sectorSize; + if bufSize < 512 then bufSize := 512; + buffer := puint32(kalloc(bufSize)); + if buffer = nil then exit; + memset(uint32(buffer), 0, bufSize); + + if disk^.dispatchRead = nil then begin + io.syslog.logln('FAT32', 'detect_volumes: device has no read dispatch'); + kfree(buffer); + exit; + end; + + driver.storage.mgr.storage_read(disk, 0, 1, buffer); + + br := PBootRecord(buffer); + if bootSectorHasSignature(buffer) and (br^.bsignature = $29) then begin + io.syslog.logln('FAT32', 'detect_volumes: volume found'); + volume := PStorage_Volume(kalloc(sizeof(TStorage_Volume))); + if volume = nil then begin + kfree(buffer); + exit; + end; + memset(uint32(volume), 0, sizeof(TStorage_Volume)); + volume^.device := disk; + volume^.sectorStart := 0; + volume^.sectorSize := br^.sectorSize; + volume^.sectorCount := disk^.maxSectorCount; + volume^.filesystem := fat32FilesystemPtr; + volume^.freeSectors := countFreeFATClusters(volume, br) * br^.spc; + volume^.isBootDrive := false; + driver.storage.vol.mgr.register_volume(disk, volume); + end; + + kfree(buffer); +end; + +function identify_volume(volume : PStorage_Volume) : boolean; +var + buffer : puint32; + br : PBootRecord; + bufSize : uint32; + fatNameOk : boolean; + spc : uint8; + i : uint32; +begin + push_trace('driver.storage.fs.fat32.identify_volume'); + identify_volume := false; + if volume = nil then exit; + if volume^.device = nil then exit; + if volume^.device^.dispatchRead = nil then exit; + + bufSize := volume^.device^.sectorSize; + if bufSize < 512 then bufSize := 512; + buffer := puint32(kalloc(bufSize)); + if buffer = nil then exit; + memset(uint32(buffer), 0, bufSize); + + driver.storage.mgr.storage_read(volume^.device, volume^.sectorStart, 1, buffer); + br := PBootRecord(buffer); + + fatNameOk := true; + for i := 0 to 4 do + if br^.identString[i] <> 'FAT32'[i + 1] then begin + fatNameOk := false; + break; + end; + + spc := br^.spc; + + if bootSectorHasSignature(buffer) and + (br^.bsignature = $29) and + fatNameOk and + (br^.sectorSize = volume^.device^.sectorSize) and + (br^.sectorSize >= 512) and + (spc <> 0) and + ((spc and (spc - 1)) = 0) and + (br^.rsvSectors > 0) and + (br^.FATSize > 0) and + (br^.numFats > 0) and + (br^.rootCluster >= 2) then begin + io.syslog.logln('FAT32', 'identify_volume: FAT32 signature matched'); + identify_volume := true; + volume^.freeSectors := countFreeFATClusters(volume, br) * br^.spc; + end else + io.syslog.logln('FAT32', 'identify_volume: not a FAT32 volume'); + + kfree(buffer); +end; + +procedure create_volume(volume : PStorage_Volume; sectors : uint32; start : uint32; config : PFSFormatParams); +var + buffer : puint32; + zeroBuffer : puint32; + bootRecord : PBootRecord; + dataStart : uint32; + fatStart : uint32; + FATSize : uint32; + batchSize : uint32; + batchPos : uint32; + rootCluster: uint32; + spc : uint32; + disk : PStorage_Device; + status : uint32; +begin + push_trace('driver.storage.fs.fat32.create_volume'); + io.syslog.logln('FAT32', 'create_volume (sync): enter'); + + disk := volume^.device; + rootCluster := 2; + spc := fatFormatConfigSPC(config, sectors, disk^.sectorSize); + + buffer := puint32(kalloc(disk^.sectorSize + 512)); + if buffer = nil then exit; + memset(uint32(buffer), 0, disk^.sectorSize); + + bootRecord := PBootRecord(buffer); + FATSize := fatComputeFATSize(sectors, spc, disk^.sectorSize); + + bootRecord^.jmp2boot := $0; + bootRecord^.OEMName[0] := 'A'; + bootRecord^.OEMName[1] := 'S'; + bootRecord^.OEMName[2] := 'U'; + bootRecord^.OEMName[3] := 'R'; + bootRecord^.OEMName[4] := 'O'; + bootRecord^.OEMName[5] := ' '; + bootRecord^.OEMName[6] := 'V'; + bootRecord^.OEMName[7] := '1'; + bootRecord^.sectorSize := disk^.sectorSize; + bootRecord^.spc := uint8(spc); + bootRecord^.rsvSectors := 32; + bootRecord^.numFats := 1; + bootRecord^.mediaDescp := $F8; + bootRecord^.hiddenSectors := start; + bootRecord^.manySectors := sectors; + bootRecord^.FATSize := FATSize; + bootRecord^.rootCluster := rootCluster; + bootRecord^.FSInfoCluster := 0; + bootRecord^.driveNumber := $80; + bootRecord^.volumeID := 62; + bootRecord^.bsignature := $29; + bootRecord^.identString[0] := 'F'; + bootRecord^.identString[1] := 'A'; + bootRecord^.identString[2] := 'T'; + bootRecord^.identString[3] := '3'; + bootRecord^.identString[4] := '2'; + bootRecord^.identString[5] := ' '; + bootRecord^.identString[6] := ' '; + bootRecord^.identString[7] := ' '; + + puint8(uint32(buffer) + 510)^ := $55; + puint8(uint32(buffer) + 511)^ := $AA; + + driver.storage.mgr.storage_write(disk, start, 1, buffer); + io.syslog.logln('FAT32', 'create_volume (sync): boot sector written'); + + fatStart := start + 32; + dataStart := fatStart + FATSize; + + if FATSize > 128 then + batchSize := 128 + else + batchSize := FATSize; + + zeroBuffer := puint32(kalloc(disk^.sectorSize * batchSize)); + if zeroBuffer = nil then begin + kfree(buffer); + exit; + end; + memset(uint32(zeroBuffer), 0, disk^.sectorSize * batchSize); + + batchPos := 0; + while batchPos < FATSize do begin + if (FATSize - batchPos) >= batchSize then + driver.storage.mgr.storage_write(disk, fatStart + batchPos, batchSize, zeroBuffer) + else + driver.storage.mgr.storage_write(disk, fatStart + batchPos, FATSize - batchPos, zeroBuffer); + batchPos := batchPos + batchSize; + end; + + kfree(zeroBuffer); + io.syslog.logln('FAT32', 'create_volume (sync): FAT zeroed'); + + memset(uint32(buffer), 0, disk^.sectorSize); + puint32(buffer)[0] := $0FFFFFF8; { media descriptor / reserved } + puint32(buffer)[1] := $0FFFFFFF; { end-of-chain marker } + puint32(buffer)[2] := $0FFFFFF8; { root cluster 2 — end-of-chain } + + driver.storage.mgr.storage_write(disk, fatStart, 1, buffer); + io.syslog.logln('FAT32', 'create_volume (sync): FAT entries written'); + + { Write root dir: '.' and '..' entries } + memset(uint32(buffer), 0, disk^.sectorSize); + PDirectory(buffer)[0].fileName[0] := '.'; + PDirectory(buffer)[0].fileName[1] := ' '; + PDirectory(buffer)[0].fileName[2] := ' '; + PDirectory(buffer)[0].fileName[3] := ' '; + PDirectory(buffer)[0].fileName[4] := ' '; + PDirectory(buffer)[0].fileName[5] := ' '; + PDirectory(buffer)[0].fileName[6] := ' '; + PDirectory(buffer)[0].fileName[7] := ' '; + PDirectory(buffer)[0].attributes := $08; + PDirectory(buffer)[0].clusterLow := uint16(rootCluster); + PDirectory(buffer)[1].fileName[0] := '.'; + PDirectory(buffer)[1].fileName[1] := '.'; + PDirectory(buffer)[1].fileName[2] := ' '; + PDirectory(buffer)[1].fileName[3] := ' '; + PDirectory(buffer)[1].fileName[4] := ' '; + PDirectory(buffer)[1].fileName[5] := ' '; + PDirectory(buffer)[1].fileName[6] := ' '; + PDirectory(buffer)[1].fileName[7] := ' '; + PDirectory(buffer)[1].attributes := $10; + PDirectory(buffer)[1].clusterLow := uint16(rootCluster); + driver.storage.mgr.storage_write(disk, clusterToLBA(dataStart, spc, rootCluster), 1, buffer); + io.syslog.logln('FAT32', 'create_volume (sync): root dir written'); + + kfree(buffer); + + { Create the SYSTEM directory via the normal FAT32 path } + status := 0; + FAT32CreateDirectory(volume, '', 'SYSTEM', $10, @status); + io.syslog.logln('FAT32', 'create_volume (sync): done'); +end; + +procedure create_volume_async(volume : PStorage_Volume; sectors : uint32; start : uint32; + config : PFSFormatParams; callback : TIOCallback; callbackData : pointer); +var + ctx : PFmtContext; + spc : uint32; +begin + push_trace('driver.storage.fs.fat32.create_volume_async'); + io.syslog.logln('FAT32', 'create_volume_async: enter'); + + ctx := PFmtContext(kalloc(sizeof(TFmtContext))); + if ctx = nil then begin + io.syslog.logln('FAT32', 'create_volume_async: OOM allocating context'); + if callback <> nil then callback(eOutOfMemory, callbackData); + exit; + end; + memset(uint32(ctx), 0, sizeof(TFmtContext)); + + ctx^.Volume := volume; + ctx^.Disk := volume^.device; + ctx^.SectorStart := start; + ctx^.RootCluster := 2; + ctx^.Callback := callback; + ctx^.CallbackData := callbackData; + + spc := fatFormatConfigSPC(config, sectors, ctx^.Disk^.sectorSize); + ctx^.SPC := spc; + ctx^.FATSize := fatComputeFATSize(sectors, spc, ctx^.Disk^.sectorSize); + ctx^.FATStart := start + 32; + ctx^.DataStart := ctx^.FATStart + ctx^.FATSize; + + ctx^.Buffer := puint32(kalloc(ctx^.Disk^.sectorSize + 512)); + if ctx^.Buffer = nil then begin + if callback <> nil then callback(eOutOfMemory, callbackData); + kfree(puint32(ctx)); + exit; + end; + memset(uint32(ctx^.Buffer), 0, ctx^.Disk^.sectorSize); + + { Write boot sector fields } + PBootRecord(ctx^.Buffer)^.jmp2boot := $0; + PBootRecord(ctx^.Buffer)^.OEMName[0] := 'A'; + PBootRecord(ctx^.Buffer)^.OEMName[1] := 'S'; + PBootRecord(ctx^.Buffer)^.OEMName[2] := 'U'; + PBootRecord(ctx^.Buffer)^.OEMName[3] := 'R'; + PBootRecord(ctx^.Buffer)^.OEMName[4] := 'O'; + PBootRecord(ctx^.Buffer)^.OEMName[5] := ' '; + PBootRecord(ctx^.Buffer)^.OEMName[6] := 'V'; + PBootRecord(ctx^.Buffer)^.OEMName[7] := '1'; + PBootRecord(ctx^.Buffer)^.sectorSize := ctx^.Disk^.sectorSize; + PBootRecord(ctx^.Buffer)^.spc := uint8(spc); + PBootRecord(ctx^.Buffer)^.rsvSectors := 32; + PBootRecord(ctx^.Buffer)^.numFats := 1; + PBootRecord(ctx^.Buffer)^.mediaDescp := $F8; + PBootRecord(ctx^.Buffer)^.hiddenSectors := start; + PBootRecord(ctx^.Buffer)^.manySectors := sectors; + PBootRecord(ctx^.Buffer)^.FATSize := ctx^.FATSize; + PBootRecord(ctx^.Buffer)^.rootCluster := ctx^.RootCluster; + PBootRecord(ctx^.Buffer)^.FSInfoCluster := 0; + PBootRecord(ctx^.Buffer)^.driveNumber := $80; + PBootRecord(ctx^.Buffer)^.volumeID := 62; + PBootRecord(ctx^.Buffer)^.bsignature := $29; + PBootRecord(ctx^.Buffer)^.identString[0] := 'F'; + PBootRecord(ctx^.Buffer)^.identString[1] := 'A'; + PBootRecord(ctx^.Buffer)^.identString[2] := 'T'; + PBootRecord(ctx^.Buffer)^.identString[3] := '3'; + PBootRecord(ctx^.Buffer)^.identString[4] := '2'; + PBootRecord(ctx^.Buffer)^.identString[5] := ' '; + PBootRecord(ctx^.Buffer)^.identString[6] := ' '; + PBootRecord(ctx^.Buffer)^.identString[7] := ' '; + puint8(uint32(ctx^.Buffer) + 510)^ := $55; + puint8(uint32(ctx^.Buffer) + 511)^ := $AA; + + if ctx^.FATSize > 128 then + ctx^.BatchSize := 128 + else + ctx^.BatchSize := ctx^.FATSize; + ctx^.BatchPos := 0; + + ctx^.ZeroBuffer := puint32(kalloc(ctx^.Disk^.sectorSize * ctx^.BatchSize)); + if ctx^.ZeroBuffer = nil then begin + kfree(ctx^.Buffer); + if callback <> nil then callback(eOutOfMemory, callbackData); + kfree(puint32(ctx)); + exit; + end; + memset(uint32(ctx^.ZeroBuffer), 0, ctx^.Disk^.sectorSize * ctx^.BatchSize); + + io.syslog.logln('FAT32', 'create_volume_async: kicking off fmtBootSector'); + ctx^.Step := fmtBootSector; + fmt_run_next(ctx); +end; + +end. -- 2.54.0 From 7e99bc2db1e48fa15f6be46fd39d6e8a6695773a Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 24 Mar 2026 18:10:04 +0000 Subject: [PATCH 08/21] fat --- .../fs/fat32/driver.storage.fs.fat32.core.pas | 71 +++++++++++++------ 1 file changed, 51 insertions(+), 20 deletions(-) diff --git a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.core.pas b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.core.pas index 2d2ef953..177a40d7 100644 --- a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.core.pas +++ b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.core.pas @@ -61,6 +61,7 @@ var function readFat(volume : PStorage_Volume; cluster : uint32; bootRecord : PBootRecord) : uint32; forward; procedure writeFat(volume : PStorage_Volume; cluster : uint32; value : uint32; bootRecord : PBootRecord); forward; procedure fatFreeClusterChain(volume : PStorage_Volume; info : PFATVolumeInfo; startCluster : uint32); forward; +function fatInitDirectoryCluster(volume : PStorage_Volume; info : PFATVolumeInfo; cluster : uint32; parentCluster : uint32) : boolean; forward; function isEndOfChain(value : uint32) : boolean; begin @@ -1650,13 +1651,14 @@ var needClusters : uint32; targetClusters : uint32; reserve : uint32; - extraClusters : uint32; - lastCluster : uint32; - firstNew : uint32; - tempRuns : PLinkedListBase; - tempRun : PFATTempRun; - i : uint32; - lastTail : uint32; + extraClusters : uint32; + lastCluster : uint32; + firstNew : uint32; + tempRuns : PLinkedListBase; + tempRun : PFATTempRun; + i : uint32; + lastTail : uint32; + prevLastCluster : uint32; begin FAT32EnsureCapacityForWrite := eNone; if (ofi = nil) or (ofi^.VolumeInfo = nil) then begin @@ -1696,6 +1698,10 @@ begin if fatFindContiguousFreeRange(ofi^.Volume, ofi^.VolumeInfo, extraClusters, firstNew) then begin fatLinkContiguousRange(ofi^.Volume, ofi^.VolumeInfo, lastCluster, firstNew, extraClusters, lastTail); if not runMapAppend(ofi^.RunMap, firstNew, extraClusters) then begin + { Undo: free the newly linked clusters and restore the previous chain tail to EOC } + fatFreeClusterChain(ofi^.Volume, ofi^.VolumeInfo, firstNew); + if lastCluster >= 2 then + writeFat(ofi^.Volume, lastCluster, $0FFFFFF8, @ofi^.VolumeInfo^.BootRecord); FAT32EnsureCapacityForWrite := eOutOfMemory; exit; end; @@ -1709,10 +1715,21 @@ begin end; for i := 0 to LL_Size(tempRuns) - 1 do begin tempRun := PFATTempRun(LL_Get(tempRuns, i)); + prevLastCluster := lastCluster; fatLinkContiguousRange(ofi^.Volume, ofi^.VolumeInfo, lastCluster, tempRun^.StartCluster, tempRun^.ClusterCount, lastTail); lastCluster := lastTail; if not runMapAppend(ofi^.RunMap, tempRun^.StartCluster, tempRun^.ClusterCount) then begin + { Undo: free the current run and restore the previous tail to EOC } + fatFreeClusterChain(ofi^.Volume, ofi^.VolumeInfo, tempRun^.StartCluster); + if prevLastCluster >= 2 then + writeFat(ofi^.Volume, prevLastCluster, $0FFFFFF8, @ofi^.VolumeInfo^.BootRecord); + { Sync AllocClusters with what actually made it into the RunMap } + ofi^.AllocClusters := ofi^.RunMap.TotalClusters; + if ofi^.AllocClusters > 0 then begin + ofi^.FatDirty := true; + ofi^.MetaDirty := true; + end; LL_Free(tempRuns); FAT32EnsureCapacityForWrite := eOutOfMemory; exit; @@ -1810,6 +1827,13 @@ begin ofi^.DirLoc.ParentCluster := parentCluster; sectorBuf := puint32(kalloc(info^.BootRecord.sectorSize)); + if (sectorBuf = nil) and (info^.BootRecord.sectorSize > 0) then begin + { OOM: cannot probe the directory — fail rather than silently treating an existing file as new } + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + FAT32CloseFile(ofi); + exit; + end; if sectorBuf <> nil then begin if locateDirEntry(volume, info, parentCluster, cleanName, extPart, ofi^.DirLoc, sectorBuf) then begin ofi^.Exists := true; @@ -1836,7 +1860,6 @@ end; procedure FAT32CloseFile(ctx : pointer); var ofi : PFATOpenFile; - sectorBuf : puint32; rawDir : PDirectory; keepClusters : uint32; walked : uint32; @@ -1910,16 +1933,14 @@ begin if ofi^.FatDirty and (ofi^.Volume <> nil) then FAT32Flush(ofi^.Volume); - if ofi^.MetaDirty and ofi^.DirLoc.Valid then begin - sectorBuf := puint32(kalloc(ofi^.VolumeInfo^.BootRecord.sectorSize)); - if sectorBuf <> nil then begin - driver.storage.mgr.storage_read(ofi^.Volume^.device, ofi^.DirLoc.SectorLBA, 1, sectorBuf); - rawDir := @PDirectory(sectorBuf)[ofi^.DirLoc.EntryIdx]; - rawDir^.byteSize := ofi^.ByteSize; - setDirFirstCluster(rawDir, ofi^.FirstCluster); - driver.storage.mgr.storage_write(ofi^.Volume^.device, ofi^.DirLoc.SectorLBA, 1, sectorBuf); - kfree(sectorBuf); - end; + { Use the already-allocated ScratchSector (same size as a disk sector) to avoid + a kalloc in close; an allocation failure here would silently lose metadata. } + if ofi^.MetaDirty and ofi^.DirLoc.Valid and (ofi^.ScratchSector <> nil) then begin + driver.storage.mgr.storage_read(ofi^.Volume^.device, ofi^.DirLoc.SectorLBA, 1, ofi^.ScratchSector); + rawDir := @PDirectory(ofi^.ScratchSector)[ofi^.DirLoc.EntryIdx]; + rawDir^.byteSize := ofi^.ByteSize; + setDirFirstCluster(rawDir, ofi^.FirstCluster); + driver.storage.mgr.storage_write(ofi^.Volume^.device, ofi^.DirLoc.SectorLBA, 1, ofi^.ScratchSector); end; runMapFree(ofi^.RunMap); @@ -2070,13 +2091,14 @@ begin statusOut := eNone; end; -procedure fatInitDirectoryCluster(volume : PStorage_Volume; info : PFATVolumeInfo; cluster : uint32; parentCluster : uint32); +function fatInitDirectoryCluster(volume : PStorage_Volume; info : PFATVolumeInfo; cluster : uint32; parentCluster : uint32) : boolean; var buffer : puint32; ds : uint32; clusterLBA : uint32; dir : PDirectory; begin + fatInitDirectoryCluster := false; if (volume = nil) or (info = nil) or (cluster < 2) then exit; buffer := puint32(kalloc(info^.BootRecord.sectorSize)); if buffer = nil then exit; @@ -2113,6 +2135,7 @@ begin end; kfree(buffer); + fatInitDirectoryCluster := true; end; function FAT32ReadDirectory(volume : PStorage_Volume; directory : pchar; statusOut : puint32) : PLinkedListBase; @@ -2300,7 +2323,15 @@ begin LL_Free(tempRuns); end; - fatInitDirectoryCluster(volume, info, cluster, parentCluster); + if not fatInitDirectoryCluster(volume, info, cluster, parentCluster) then begin + { OOM in fatInitDirectoryCluster: free the allocated cluster and bail out } + fatFreeClusterChain(volume, info, cluster); + if statusOut <> nil then statusOut^ := ord(eOutOfMemory); + kfree(sectorBuf); + if namePart <> nil then kfree(void(namePart)); + if extPart <> nil then kfree(void(extPart)); + exit; + end; rawDir := @PDirectory(sectorBuf)[loc.EntryIdx]; memset(uint32(rawDir), 0, sizeof(TDirectory)); -- 2.54.0 From d1ecd3fc41718dd1b79abdbd96727e25821f217b Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 31 Oct 2026 16:30:40 +0000 Subject: [PATCH 09/21] fix(storage): harden disk operations, add volume labels, and stabilize AHCI discovery --- src/app/app.diskutil.pas | 500 +++++++---- src/app/app.partcmd.pas | 25 +- src/arch/x86/system.pas | 13 + .../ctl/ahci/driver.storage.ctl.ahci.pas | 281 +++---- .../storage/driver.storage.metatest.pas | 204 +++++ src/driver/storage/driver.storage.mgr.pas | 112 ++- src/driver/storage/driver.storage.types.pas | 15 +- src/driver/storage/driver.storage.vfs.pas | 30 +- .../storage/fs/driver.storage.fs.flatfs.pas | 129 ++- .../storage/fs/driver.storage.fs.iso9660.pas | 11 + .../fs/fat32/driver.storage.fs.fat32.core.pas | 18 +- .../fat32/driver.storage.fs.fat32.labels.pas | 205 +++++ .../fs/fat32/driver.storage.fs.fat32.pas | 8 +- .../fat32/driver.storage.fs.fat32.types.pas | 22 + .../fs/fat32/driver.storage.fs.fat32.vol.pas | 690 ++++----------- .../storage/vol/driver.storage.vol.mbr.pas | 4 +- .../storage/vol/driver.storage.vol.mgr.pas | 785 ++++++++---------- src/test/test.pas | 4 + 18 files changed, 1563 insertions(+), 1493 deletions(-) create mode 100644 src/driver/storage/driver.storage.metatest.pas create mode 100644 src/driver/storage/fs/fat32/driver.storage.fs.fat32.labels.pas diff --git a/src/app/app.diskutil.pas b/src/app/app.diskutil.pas index 4a96e032..719c887b 100644 --- a/src/app/app.diskutil.pas +++ b/src/app/app.diskutil.pas @@ -14,6 +14,7 @@ unit app.diskutil; interface procedure init(); +procedure UnitTest; implementation @@ -63,6 +64,7 @@ var fmt_cluster_input : Plv_obj; fmt_filecount_input: Plv_obj; fmt_label_input : Plv_obj; + label_target_id, label_target_index : uint32; fmt_param_error_lbl: Plv_obj; add_textarea : Plv_obj; add_err_lbl : Plv_obj; @@ -75,12 +77,15 @@ var storage_dirty_flag : uint32; storage_watch_id : uint32; ui_session_id : uint32; + operation_pending : boolean; + done_kind, done_device : uint32; type PFmtUICallbackCtx = ^TFmtUICallbackCtx; TFmtUICallbackCtx = record SessionID : uint32; VolumeIdx : uint32; + Kind, DeviceIdx : uint32; end; { ============================================================ @@ -100,41 +105,84 @@ procedure rebuild_fmt_param_ui(fs : PFilesystem); forward; { ============================================================ Helpers — size formatting ============================================================ } -function formatSizeStr(bytes: uint32): pchar; +{ All transient text belongs to gfxd and is released on its next UI tick. + LVGL copies labels/options; no widget retains these pointers. A header in the + same allocation handles nested concatenations without leaking intermediates. } +type + PUIText = ^TUIText; + TUIText = record Next : PUIText; end; +var TextTemps : PUIText = nil; + +procedure releaseTextTemps; +var item : PUIText; begin - if bytes >= 1073741824 then - formatSizeStr := stringConcat(intToString(bytes div 1073741824), ' GB') - else if bytes >= 1048576 then - formatSizeStr := stringConcat(intToString(bytes div 1048576), ' MB') - else if bytes >= 1024 then - formatSizeStr := stringConcat(intToString(bytes div 1024), ' KB') - else - formatSizeStr := stringConcat(intToString(bytes), ' B'); + while TextTemps <> nil do begin + item := TextTemps; TextTemps := item^.Next; kfree(void(item)); + end; end; -function formatSizePrecise(bytes: uint32): pchar; +function uiConcat(a, b : pchar) : pchar; +var item : PUIText; n, m : uint32; dest : pchar; +begin + uiConcat := ''; + n := stringSize(a); m := stringSize(b); + item := PUIText(kalloc(sizeof(TUIText)+n+m+1)); + if item = nil then exit; + item^.Next := TextTemps; TextTemps := item; + dest := pchar(uint32(item)+sizeof(TUIText)); + if n > 0 then memcpy(uint32(a), uint32(dest), n); + if m > 0 then memcpy(uint32(b), uint32(dest)+n, m); + dest[n+m] := #0; uiConcat := dest; +end; + +function uiInt(value : uint64) : pchar; +var digits : array[0..20] of char; pos : uint32; +begin + pos := 20; digits[pos] := #0; + repeat + Dec(pos); digits[pos] := char(ord('0') + value mod 10); value := value div 10; + until value = 0; + uiInt := uiConcat(@digits[pos], ''); +end; + +procedure operation_done(error : TError; userdata : pointer); forward; +function begin_partition_ui(kind : uint32) : PFmtUICallbackCtx; forward; + +function formatSizeStr(bytes: uint64): pchar; +begin + if bytes >= 1073741824 then + formatSizeStr := uiConcat(uiInt(bytes div 1073741824), ' GB') + else if bytes >= 1048576 then + formatSizeStr := uiConcat(uiInt(bytes div 1048576), ' MB') + else if bytes >= 1024 then + formatSizeStr := uiConcat(uiInt(bytes div 1024), ' KB') + else + formatSizeStr := uiConcat(uiInt(bytes), ' B'); +end; + +function formatSizePrecise(bytes: uint64): pchar; var - whole, frac : uint32; + whole, frac : uint64; begin if bytes >= 1073741824 then begin whole := bytes div 1073741824; frac := ((bytes mod 1073741824) * 10) div 1073741824; if frac > 0 then - formatSizePrecise := stringConcat(stringConcat(intToString(whole), stringConcat('.', intToString(frac))), ' GB') + formatSizePrecise := uiConcat(uiConcat(uiInt(whole), uiConcat('.', uiInt(frac))), ' GB') else - formatSizePrecise := stringConcat(intToString(whole), ' GB'); + formatSizePrecise := uiConcat(uiInt(whole), ' GB'); end else if bytes >= 1048576 then begin whole := bytes div 1048576; frac := ((bytes mod 1048576) * 10) div 1048576; if frac > 0 then - formatSizePrecise := stringConcat(stringConcat(intToString(whole), stringConcat('.', intToString(frac))), ' MB') + formatSizePrecise := uiConcat(uiConcat(uiInt(whole), uiConcat('.', uiInt(frac))), ' MB') else - formatSizePrecise := stringConcat(intToString(whole), ' MB'); + formatSizePrecise := uiConcat(uiInt(whole), ' MB'); end else if bytes >= 1024 then begin whole := bytes div 1024; - formatSizePrecise := stringConcat(intToString(whole), ' KB'); + formatSizePrecise := uiConcat(uiInt(whole), ' KB'); end else - formatSizePrecise := stringConcat(intToString(bytes), ' B'); + formatSizePrecise := uiConcat(uiInt(bytes), ' B'); end; function controllerStr(ct: TControllerType): pchar; @@ -316,7 +364,7 @@ begin end; { Add a usage bar (colored bar showing used vs total) } -procedure addUsageBar(parent: Plv_obj; row_w: sint32; used, total: uint32); +procedure addUsageBar(parent: Plv_obj; row_w: sint32; used, total: uint64); var bar : Plv_obj; pct : sint32; @@ -352,75 +400,32 @@ end; Returns 0 on invalid input. ============================================================ } function parseSize(s: pchar; sectorSize: uint32): uint32; -var - len : uint32; - last : char; - prev : char; - numEnd : uint32; - value : uint32; - bytes : uint32; +var i, digit : uint32; value, limit, multiplier, bytes : uint64; begin parseSize := 0; - if s = nil then exit; - len := stringSize(s); - if len = 0 then exit; - - last := s[len - 1]; - - if (last = 'B') or (last = 'b') then begin - if len >= 3 then - prev := s[len - 2] - else - prev := #0; - - if (prev = 'G') or (prev = 'g') then begin - numEnd := len - 2; - s[numEnd] := #0; - if not isNumericStr(s) then begin s[numEnd] := prev; exit; end; - value := stringToInt(s); - s[numEnd] := prev; - if value = 0 then exit; - bytes := value * 1073741824; - end else if (prev = 'M') or (prev = 'm') then begin - numEnd := len - 2; - s[numEnd] := #0; - if not isNumericStr(s) then begin s[numEnd] := prev; exit; end; - value := stringToInt(s); - s[numEnd] := prev; - if value = 0 then exit; - bytes := value * 1048576; - end else if (prev = 'K') or (prev = 'k') then begin - numEnd := len - 2; - s[numEnd] := #0; - if not isNumericStr(s) then begin s[numEnd] := prev; exit; end; - value := stringToInt(s); - s[numEnd] := prev; - if value = 0 then exit; - bytes := value * 1024; - end else begin - { Just 'B' suffix — plain bytes } - numEnd := len - 1; - s[numEnd] := #0; - if not isNumericStr(s) then begin s[numEnd] := last; exit; end; - value := stringToInt(s); - s[numEnd] := last; - if value = 0 then exit; - bytes := value; - end; - end else begin - { Plain number (bytes) } - if not isNumericStr(s) then exit; - bytes := stringToInt(s); - if bytes = 0 then exit; + if (s = nil) or (sectorSize = 0) then exit; + limit := uint64($FFFFFFFF) * sectorSize; + i := 0; value := 0; + while (s[i] >= '0') and (s[i] <= '9') do begin + digit := ord(s[i])-ord('0'); + if value > (limit-digit) div 10 then exit; + value := value*10+digit; Inc(i); end; - - if sectorSize = 0 then exit; - parseSize := bytes div sectorSize; + if (i = 0) or (value = 0) then exit; + multiplier := 1; + case s[i] of + 'K','k': begin multiplier := 1024; Inc(i); end; + 'M','m': begin multiplier := 1048576; Inc(i); end; + 'G','g': begin multiplier := 1073741824; Inc(i); end; + end; + if (s[i] = 'B') or (s[i] = 'b') then Inc(i); + if s[i] <> #0 then exit; + if value > limit div multiplier then exit; + bytes := value*multiplier; + if (bytes mod sectorSize) <> 0 then exit; + parseSize := uint32(bytes div sectorSize); end; -{ ============================================================ - Close any active message box - ============================================================ } procedure closeMsgBox; begin if active_mbox <> nil then begin @@ -433,6 +438,7 @@ begin fmt_param_area := nil; fmt_cluster_input := nil; fmt_filecount_input := nil; + fmt_label_input := nil; fmt_param_error_lbl := nil; end; @@ -452,10 +458,11 @@ var lbl : Plv_obj; begin if fmt_param_area = nil then exit; - lv_obj_clean(fmt_param_area); fmt_cluster_input := nil; fmt_filecount_input := nil; + fmt_label_input := nil; fmt_param_error_lbl := nil; + lv_obj_clean(fmt_param_area); if fs = nil then exit; if fs^.formatParamFlags = 0 then begin @@ -484,6 +491,15 @@ begin lv_textarea_set_placeholder_text(fmt_filecount_input, '1000'); lv_obj_set_width(fmt_filecount_input, 220); end; + if (fs^.formatParamFlags and FS_FORMAT_PARAM_LABEL) <> 0 then begin + lbl := lv_label_create(fmt_param_area); + lv_label_set_text(lbl, 'Volume label (up to 11 ASCII characters)'); + fmt_label_input := lv_textarea_create(fmt_param_area); + lv_textarea_set_one_line(fmt_label_input,true); + lv_textarea_set_max_length(fmt_label_input,11); + lv_textarea_set_placeholder_text(fmt_label_input,'Optional'); + lv_obj_set_width(fmt_label_input,260); + end; end; procedure fmt_fs_change_cb(e: Plv_event); cdecl; @@ -514,6 +530,7 @@ var part : TPartition_table; mbr_rec : PMaster_Boot_Record; needsInit : boolean; + cbCtx : PFmtUICallbackCtx; begin code := lv_event_get_code(e); if code <> LV_EVENT_CLICKED then exit; @@ -568,8 +585,7 @@ begin { If disk needs init, do it now (async write, but cache updated synchronously) } if needsInit then begin - driver.storage.vol.mgr.init_disk_async(device, nil, nil); - { Cache is now fresh mbr with $AA55 signature, all slots empty } + { Blank MBR and first partition are committed in one worker write. } slot := 0; lba := 1; end else begin @@ -592,11 +608,9 @@ begin memset(uint32(@part), 0, sizeof(TPartition_table)); driver.storage.vol.mbr.setup_partition(@part, lba, sectors); - driver.storage.vol.mgr.add_partition_async(device, uint32(slot), part, nil, nil); - - closeMsgBox; - refreshSidebar; - showDeviceDetail(sel_dev_idx); + cbCtx := begin_partition_ui(1); + if cbCtx = nil then exit; + driver.storage.vol.mgr.add_partition_async(device, uint32(slot), part, @operation_done, cbCtx); end; procedure add_part_cancel_cb(e: Plv_event); cdecl; @@ -611,22 +625,16 @@ end; Dialog callbacks — Delete Partition ============================================================ } procedure del_part_ok_cb(e: Plv_event); cdecl; -var - code : uint32; - device : PStorage_Device; +var device : PStorage_Device; cbCtx : PFmtUICallbackCtx; slot : uint32; begin - code := lv_event_get_code(e); - if code <> LV_EVENT_CLICKED then exit; - + if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit; + if operation_pending then exit; device := driver.storage.mgr.get_device(sel_dev_idx); - if device = nil then begin closeMsgBox; exit; end; - - if (pending_del_slot >= 0) and (pending_del_slot <= 3) then - driver.storage.vol.mgr.remove_partition_async(device, uint32(pending_del_slot), nil, nil); - - closeMsgBox; - refreshSidebar; - showDeviceDetail(sel_dev_idx); + if (device = nil) or (pending_del_slot < 0) or (pending_del_slot > 3) then exit; + slot := uint32(pending_del_slot); + cbCtx := begin_partition_ui(2); + if cbCtx = nil then exit; + driver.storage.vol.mgr.remove_partition_async(device, slot, @operation_done, cbCtx); end; procedure del_part_cancel_cb(e: Plv_event); cdecl; @@ -656,7 +664,7 @@ begin slot := uint32(lv_event_get_user_data(e)); pending_del_slot := sint32(slot); - txt := stringConcat('Delete partition in slot ', intToString(slot)); + txt := uiConcat('Delete partition in slot ', uiInt(slot)); device := driver.storage.mgr.get_device(sel_dev_idx); if device <> nil then begin @@ -664,13 +672,13 @@ begin if mbr_rec <> nil then begin part := mbr_rec^.partition[slot]; if part.sector_count > 0 then begin - txt := stringConcat(txt, ' ('); - txt := stringConcat(txt, formatSizeStr(part.sector_count * device^.sectorSize)); - txt := stringConcat(txt, ')'); + txt := uiConcat(txt, ' ('); + txt := uiConcat(txt, formatSizeStr(uint64(part.sector_count) * device^.sectorSize)); + txt := uiConcat(txt, ')'); end; end; end; - txt := stringConcat(txt, '?'); + txt := uiConcat(txt, '?'); closeMsgBox; mbox := lv_msgbox_create(lv_layer_top); @@ -691,17 +699,21 @@ end; { Async completion callback — fired from ISR context when format finishes. Only sets completion flags; LVGL work happens later on the UI poll timer. } -procedure fmt_done_cb(error : TError; userdata : pointer); +procedure operation_done(error : TError; userdata : pointer); var ctx : PFmtUICallbackCtx; + flags : uint32; begin ctx := PFmtUICallbackCtx(userdata); if ctx <> nil then begin + asm pushfd; cli; pop flags end; if ctx^.SessionID = ui_session_id then begin fmt_done_error := error; fmt_done_vol_idx := ctx^.VolumeIdx; + done_kind := ctx^.Kind; done_device := ctx^.DeviceIdx; puint32(@fmt_done_flag)^ := 1; end; + if (flags and $200) <> 0 then asm sti end; kfree(void(ctx)); end; end; @@ -715,6 +727,7 @@ procedure fmt_poll_cb(tmr: Plv_timer); cdecl; var mbox : Plv_obj; begin + releaseTextTemps; if puint32(@storage_dirty_flag)^ <> 0 then begin puint32(@storage_dirty_flag)^ := 0; refreshSidebar; @@ -736,24 +749,49 @@ begin closeMsgBox; refreshSidebar; - showVolumeDetail(fmt_done_vol_idx); + operation_pending := false; + if done_kind = 0 then showVolumeDetail(fmt_done_vol_idx) + else showDeviceDetail(done_device); if fmt_done_error <> eNone then begin mbox := lv_msgbox_create(lv_layer_top); active_mbox := mbox; - lv_msgbox_add_title(mbox, 'Format Failed'); - lv_msgbox_add_text(mbox, 'The volume was formatted but could not be verified.'); + lv_msgbox_add_title(mbox, 'Storage Operation Failed'); + lv_msgbox_add_text(mbox, 'The operation did not complete. Check the error before retrying.'); lv_msgbox_add_text(mbox, formatErrorName(fmt_done_error)); - lv_msgbox_add_footer_button(mbox, 'OK'); + lv_obj_add_event_cb(lv_msgbox_add_footer_button(mbox, 'OK'), @add_part_cancel_cb, LV_EVENT_CLICKED, nil); end else begin mbox := lv_msgbox_create(lv_layer_top); active_mbox := mbox; - lv_msgbox_add_title(mbox, 'Format Complete'); - lv_msgbox_add_text(mbox, 'The volume is ready.'); - lv_msgbox_add_footer_button(mbox, 'OK'); + lv_msgbox_add_title(mbox, 'Storage Operation Complete'); + lv_msgbox_add_text(mbox, 'The requested changes have been written successfully.'); + lv_obj_add_event_cb(lv_msgbox_add_footer_button(mbox, 'OK'), @add_part_cancel_cb, LV_EVENT_CLICKED, nil); end; end; +function begin_partition_ui(kind : uint32) : PFmtUICallbackCtx; +var ctx : PFmtUICallbackCtx; +begin + begin_partition_ui := nil; + if operation_pending then exit; + ctx := PFmtUICallbackCtx(kalloc(sizeof(TFmtUICallbackCtx))); + if ctx = nil then begin + closeMsgBox; + active_mbox := lv_msgbox_create(lv_layer_top); + lv_msgbox_add_title(active_mbox, 'Out of memory'); + lv_obj_add_event_cb(lv_msgbox_add_footer_button(active_mbox, 'OK'), @add_part_cancel_cb, LV_EVENT_CLICKED, nil); + exit; + end; + ctx^.SessionID := ui_session_id; ctx^.VolumeIdx := sel_vol_idx; + ctx^.Kind := kind; ctx^.DeviceIdx := sel_dev_idx; + closeMsgBox; + active_mbox := lv_msgbox_create(lv_layer_top); + lv_msgbox_add_title(active_mbox, 'Updating partition table'); + lv_msgbox_add_text(active_mbox, 'The operation is running in the background.'); + operation_pending := true; + begin_partition_ui := ctx; +end; + procedure fmt_ok_cb(e: Plv_event); cdecl; var code : uint32; @@ -765,7 +803,9 @@ var paramPtr : PFSFormatParams; text : pchar; value : uint32; + normalized : TVolumeLabel; begin + if operation_pending then exit; code := lv_event_get_code(e); if code <> LV_EVENT_CLICKED then exit; @@ -776,7 +816,7 @@ begin active_mbox := lv_msgbox_create(lv_layer_top); lv_msgbox_add_title(active_mbox, 'Format Busy'); lv_msgbox_add_text(active_mbox, 'This volume is busy. Wait for the current operation to finish.'); - lv_msgbox_add_footer_button(active_mbox, 'OK'); + lv_obj_add_event_cb(lv_msgbox_add_footer_button(active_mbox, 'OK'), @add_part_cancel_cb, LV_EVENT_CLICKED, nil); exit; end; @@ -792,7 +832,7 @@ begin if fmt_cluster_input <> nil then begin text := lv_textarea_get_text(fmt_cluster_input); if (text <> nil) and (text^ <> #0) then begin - if not isNumericStr(text) then begin + if (stringSize(text) > 5) or (not isNumericStr(text)) then begin set_fmt_param_error('Cluster size must be a number.'); exit; end; @@ -812,7 +852,7 @@ begin if fmt_filecount_input <> nil then begin text := lv_textarea_get_text(fmt_filecount_input); if (text <> nil) and (text^ <> #0) then begin - if not isNumericStr(text) then begin + if (stringSize(text) > 5) or (not isNumericStr(text)) then begin set_fmt_param_error('File count must be a number.'); exit; end; @@ -828,23 +868,40 @@ begin end; end; + if (fmt_label_input <> nil) and ((fs^.formatParamFlags and FS_FORMAT_PARAM_LABEL) <> 0) then begin + text := lv_textarea_get_text(fmt_label_input); + if (fs^.validateLabelCallback = nil) then exit; + if fs^.validateLabelCallback(text,normalized) <> eNone then begin + set_fmt_param_error('Use up to 11 ASCII characters without punctuation such as / or :'); exit; + end; + memcpy(uint32(@normalized[0]),uint32(@params.LabelText[0]),12); + params.Flags := params.Flags or FS_FORMAT_PARAM_LABEL; paramPtr := @params; + end; + closeMsgBox; cbCtx := PFmtUICallbackCtx(kalloc(sizeof(TFmtUICallbackCtx))); - if cbCtx = nil then exit; + if cbCtx = nil then begin + active_mbox := lv_msgbox_create(lv_layer_top); + lv_msgbox_add_title(active_mbox, 'Out of memory'); + lv_obj_add_event_cb(lv_msgbox_add_footer_button(active_mbox, 'OK'), @add_part_cancel_cb, LV_EVENT_CLICKED, nil); + exit; + end; cbCtx^.SessionID := ui_session_id; cbCtx^.VolumeIdx := sel_vol_idx; + cbCtx^.Kind := 0; cbCtx^.DeviceIdx := sel_dev_idx; { Submit async format — returns immediately, fmt_done_cb only flips a flag. } if not driver.storage.vol.mgr.format_volume_async(vol^.device, sel_vol_idx, fs^.sName, paramPtr, - @fmt_done_cb, cbCtx) then begin + @operation_done, cbCtx) then begin kfree(void(cbCtx)); closeMsgBox; active_mbox := lv_msgbox_create(lv_layer_top); lv_msgbox_add_title(active_mbox, 'Format Failed'); - lv_msgbox_add_text(active_mbox, 'Could not start the format operation.'); - lv_msgbox_add_footer_button(active_mbox, 'OK'); + lv_msgbox_add_text(active_mbox, 'Cannot start: device busy, unsupported geometry/options, or insufficient memory.'); + lv_obj_add_event_cb(lv_msgbox_add_footer_button(active_mbox, 'OK'), @add_part_cancel_cb, LV_EVENT_CLICKED, nil); end else begin + operation_pending := true; active_mbox := lv_msgbox_create(lv_layer_top); lv_msgbox_add_title(active_mbox, 'Formatting'); lv_msgbox_add_text(active_mbox, 'Formatting is running in the background.'); @@ -936,9 +993,9 @@ begin active_mbox := mbox; lv_msgbox_add_title(mbox, 'Add Partition'); - infoTxt := stringConcat('Free space: ', formatSizePrecise(freeSec * device^.sectorSize)); - infoTxt := stringConcat(infoTxt, ' | Free slots: '); - infoTxt := stringConcat(infoTxt, intToString(freeSlotCnt)); + infoTxt := uiConcat('Free space: ', formatSizePrecise(uint64(freeSec) * device^.sectorSize)); + infoTxt := uiConcat(infoTxt, ' | Free slots: '); + infoTxt := uiConcat(infoTxt, uiInt(freeSlotCnt)); lv_msgbox_add_text(mbox, infoTxt); lv_msgbox_add_text(mbox, 'Enter size (e.g. 100MB, 2GB) or leave blank for all free space:'); @@ -946,7 +1003,7 @@ begin body := lv_msgbox_get_content(mbox); add_textarea := lv_textarea_create(body); lv_textarea_set_one_line(add_textarea, true); - lv_textarea_set_placeholder_text(add_textarea, formatSizeStr(freeSec * device^.sectorSize)); + lv_textarea_set_placeholder_text(add_textarea, formatSizeStr(uint64(freeSec) * device^.sectorSize)); lv_obj_set_width(add_textarea, 280); add_err_lbl := nil; @@ -1016,7 +1073,7 @@ begin body := lv_msgbox_get_content(mbox); addStatusLabel(body, - stringConcat('Volume size: ', formatSizePrecise(vol^.sectorCount * vol^.sectorSize)), + uiConcat('Volume size: ', formatSizePrecise(uint64(vol^.sectorCount) * vol^.sectorSize)), 160, 170, 190); { Build newline-separated options for dropdown } @@ -1027,8 +1084,8 @@ begin if opts = nil then opts := fs^.sName else begin - opts := stringConcat(opts, #10); - opts := stringConcat(opts, fs^.sName); + opts := uiConcat(opts, #10); + opts := uiConcat(opts, fs^.sName); end; end; end; @@ -1064,7 +1121,6 @@ begin if detail <> nil then lv_obj_clean(detail); sel_mode := SEL_NONE; - pending_del_slot := -1; end; { Find the volume that corresponds to a partition on a device } @@ -1076,7 +1132,7 @@ var begin findVolumeForPartition := nil; volCnt := driver.storage.vol.mgr.get_volume_count(); - for i := 0 to volCnt - 1 do begin + if volCnt > 0 then for i := 0 to volCnt - 1 do begin vol := driver.storage.vol.mgr.get_volume(i); if vol = nil then continue; if (vol^.device = device) and (vol^.sectorStart = lbaStart) then begin @@ -1100,8 +1156,8 @@ var sizeStr : pchar; vol : PStorage_Volume; freeStr : pchar; - totalB : uint32; - freeB : uint32; + totalB : uint64; + freeB : uint64; begin row := lv_obj_create(parent); lv_obj_remove_style_all(row); @@ -1119,20 +1175,20 @@ begin { Slot number badge } lbl := lv_label_create(row); - lv_label_set_text(lbl, intToString(slot)); + lv_label_set_text(lbl, uiInt(slot)); lv_obj_set_style_text_color(lbl, lv_color_make(100, 160, 255), 0); lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0); lv_obj_set_width(lbl, 20); { System ID } lbl := lv_label_create(row); - lv_label_set_text(lbl, stringConcat('ID:', intToString(part.system_id))); + lv_label_set_text(lbl, uiConcat('ID:', uiInt(part.system_id))); lv_obj_set_style_text_color(lbl, lv_color_make(160, 170, 190), 0); lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0); lv_obj_set_width(lbl, 55); { Size } - totalB := part.sector_count * sectorSize; + totalB := uint64(part.sector_count) * sectorSize; sizeStr := formatSizeStr(totalB); lbl := lv_label_create(row); lv_label_set_text(lbl, sizeStr); @@ -1143,8 +1199,8 @@ begin { Free space — look up the corresponding volume } vol := findVolumeForPartition(device, part.LBA_start); if vol <> nil then begin - freeB := vol^.freeSectors * vol^.sectorSize; - freeStr := stringConcat(formatSizeStr(freeB), ' free'); + freeB := uint64(vol^.freeSectors) * vol^.sectorSize; + freeStr := uiConcat(formatSizeStr(freeB), ' free'); lbl := lv_label_create(row); lv_label_set_text(lbl, freeStr); lv_obj_set_style_text_color(lbl, lv_color_make(100, 180, 130), 0); @@ -1183,9 +1239,9 @@ var btn : Plv_obj; dw : sint32; i : uint32; - totalBytes : uint32; - usedSectors : uint32; - freeBytes : uint32; + totalBytes : uint64; + usedSectors : uint64; + freeBytes : uint64; volCnt : uint32; devVolCnt : uint32; vol : PStorage_Volume; @@ -1194,8 +1250,8 @@ var lbl : Plv_obj; sizeStr : pchar; freeStr : pchar; - totalB : uint32; - freeB : uint32; + totalB : uint64; + freeB : uint64; begin clearDetail; sel_mode := SEL_DEVICE; @@ -1211,18 +1267,18 @@ begin { ---- Device Info ---- } addSectionHeader(detail, 'Device Info'); - addInfoRow(detail, dw, 'Device Index', intToString(devIdx)); + addInfoRow(detail, dw, 'Device Index', uiInt(devIdx)); addInfoRow(detail, dw, 'Controller', controllerStr(device^.controller)); addInfoRow(detail, dw, 'Writable', boolStr(device^.writable)); - addInfoRow(detail, dw, 'Sector Size', stringConcat(intToString(device^.sectorSize), ' bytes')); - totalBytes := device^.maxSectorCount * device^.sectorSize; + addInfoRow(detail, dw, 'Sector Size', uiConcat(uiInt(device^.sectorSize), ' bytes')); + totalBytes := uint64(device^.maxSectorCount) * device^.sectorSize; addInfoRow(detail, dw, 'Total Size', formatSizePrecise(totalBytes)); { Compute free space from in-memory volume data (no I/O) } usedSectors := 0; devVolCnt := 0; volCnt := driver.storage.vol.mgr.get_volume_count(); - for i := 0 to volCnt - 1 do begin + if volCnt > 0 then for i := 0 to volCnt - 1 do begin vol := driver.storage.vol.mgr.get_volume(i); if (vol <> nil) and (vol^.device = device) then begin devVolCnt := devVolCnt + 1; @@ -1230,11 +1286,11 @@ begin end; end; if device^.maxSectorCount > usedSectors then - freeBytes := (device^.maxSectorCount - usedSectors) * device^.sectorSize + freeBytes := uint64(device^.maxSectorCount - usedSectors) * device^.sectorSize else freeBytes := 0; addInfoRow(detail, dw, 'Free Space', formatSizePrecise(freeBytes)); - addInfoRow(detail, dw, 'Volumes', intToString(devVolCnt)); + addInfoRow(detail, dw, 'Volumes', uiInt(devVolCnt)); { Usage bar } if totalBytes > 0 then @@ -1248,7 +1304,7 @@ begin if devVolCnt = 0 then addStatusLabel(detail, 'No volumes found.', 120, 125, 145) else begin - for i := 0 to volCnt - 1 do begin + if volCnt > 0 then for i := 0 to volCnt - 1 do begin vol := driver.storage.vol.mgr.get_volume(i); if (vol = nil) or (vol^.device <> device) then continue; @@ -1272,20 +1328,21 @@ begin else fsName := 'Unknown'; lbl := lv_label_create(row); - lv_label_set_text(lbl, fsName); + if vol^.labelText[0] <> #0 then lv_label_set_text(lbl,@vol^.labelText[0]) + else lv_label_set_text(lbl, fsName); lv_obj_set_style_text_color(lbl, lv_color_make(100, 160, 255), 0); lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0); lv_obj_set_width(lbl, 70); { LBA start } lbl := lv_label_create(row); - lv_label_set_text(lbl, stringConcat('LBA:', intToString(vol^.sectorStart))); + lv_label_set_text(lbl, uiConcat('LBA:', uiInt(vol^.sectorStart))); lv_obj_set_style_text_color(lbl, lv_color_make(160, 170, 190), 0); lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0); lv_obj_set_width(lbl, 80); { Size } - totalB := vol^.sectorCount * vol^.sectorSize; + totalB := uint64(vol^.sectorCount) * vol^.sectorSize; sizeStr := formatSizeStr(totalB); lbl := lv_label_create(row); lv_label_set_text(lbl, sizeStr); @@ -1294,8 +1351,8 @@ begin lv_obj_set_width(lbl, 70); { Free space } - freeB := vol^.freeSectors * vol^.sectorSize; - freeStr := stringConcat(formatSizeStr(freeB), ' free'); + freeB := uint64(vol^.freeSectors) * vol^.sectorSize; + freeStr := uiConcat(formatSizeStr(freeB), ' free'); lbl := lv_label_create(row); lv_label_set_text(lbl, freeStr); lv_obj_set_style_text_color(lbl, lv_color_make(100, 180, 130), 0); @@ -1327,13 +1384,65 @@ end; { ============================================================ Show volume detail — volume info + format action ============================================================ } +procedure label_save_cb(e : Plv_event); cdecl; +var vol : PStorage_Volume; ctx : PFmtUICallbackCtx; value : TVolumeLabel; err : TError; +begin + if (lv_event_get_code(e) <> LV_EVENT_CLICKED) or operation_pending then exit; + vol := driver.storage.vol.mgr.get_volume(label_target_index); + if (vol = nil) or (vol^.id <> label_target_id) or (vol^.filesystem = nil) then begin + lv_label_set_text(add_err_lbl,'The selected volume is no longer available.'); exit; + end; + if vol^.filesystem^.validateLabelCallback = nil then exit; + err := vol^.filesystem^.validateLabelCallback(lv_textarea_get_text(fmt_label_input),value); + if err <> eNone then begin + lv_label_set_text(add_err_lbl,'Invalid label. Use ASCII letters, digits, spaces, _ or -.'); exit; + end; + ctx := PFmtUICallbackCtx(kalloc(sizeof(TFmtUICallbackCtx))); + if ctx = nil then begin lv_label_set_text(add_err_lbl,'Out of memory.'); exit; end; + ctx^.SessionID := ui_session_id; ctx^.VolumeIdx := label_target_index; + ctx^.Kind := 0; ctx^.DeviceIdx := sel_dev_idx; + err := driver.storage.vol.mgr.set_volume_label_async(vol^.device,label_target_index,@value[0],@operation_done,ctx); + if err <> eNone then begin + kfree(void(ctx)); lv_label_set_text(add_err_lbl,formatErrorName(err)); exit; + end; + operation_pending := true; + closeMsgBox; + active_mbox := lv_msgbox_create(lv_layer_top); + lv_msgbox_add_title(active_mbox,'Saving volume label'); + lv_msgbox_add_text(active_mbox,'Updating the label on disk.'); +end; + +procedure label_edit_cb(e : Plv_event); cdecl; +var vol : PStorage_Volume; content : Plv_obj; +begin + if (lv_event_get_code(e) <> LV_EVENT_CLICKED) or operation_pending then exit; + vol := driver.storage.vol.mgr.get_volume(sel_vol_idx); + if (vol = nil) or (vol^.filesystem = nil) then exit; + if vol^.filesystem^.setLabelCallback = nil then exit; + closeMsgBox; + label_target_id := vol^.id; label_target_index := sel_vol_idx; + active_mbox := lv_msgbox_create(lv_layer_top); + lv_msgbox_add_title(active_mbox,'Rename Volume'); + lv_msgbox_add_text(active_mbox,'Up to 11 ASCII characters, stored in uppercase. Leave blank to remove the label.'); + content := lv_msgbox_get_content(active_mbox); + fmt_label_input := lv_textarea_create(content); + lv_textarea_set_one_line(fmt_label_input,true); + lv_textarea_set_max_length(fmt_label_input,11); + lv_textarea_set_text(fmt_label_input,@vol^.labelText[0]); + lv_obj_set_width(fmt_label_input,300); + add_err_lbl := lv_label_create(content); lv_label_set_text(add_err_lbl,''); + lv_obj_set_style_text_color(add_err_lbl,lv_color_make(230,80,80),0); + lv_obj_add_event_cb(lv_msgbox_add_footer_button(active_mbox,'Save'),@label_save_cb,LV_EVENT_CLICKED,nil); + lv_obj_add_event_cb(lv_msgbox_add_footer_button(active_mbox,'Cancel'),@add_part_cancel_cb,LV_EVENT_CLICKED,nil); +end; + procedure showVolumeDetail(volIdx: uint32); var vol : PStorage_Volume; dw : sint32; - totalBytes : uint32; - freeBytes : uint32; - usedBytes : uint32; + totalBytes : uint64; + freeBytes : uint64; + usedBytes : uint64; fsName : pchar; btn_row : Plv_obj; btn : Plv_obj; @@ -1354,17 +1463,20 @@ begin dw := WIN_W - SIDEBAR_W - 20; addSectionHeader(detail, 'Volume Info'); - addInfoRow(detail, dw, 'Volume Index', intToString(volIdx)); + addInfoRow(detail, dw, 'Volume Index', uiInt(volIdx)); + if vol^.labelText[0] <> #0 then addInfoRow(detail,dw,'Label',@vol^.labelText[0]) + else addInfoRow(detail,dw,'Label','Unlabelled'); { Find parent device index } devIdx := 0; + if driver.storage.mgr.get_device_count() > 0 then for i := 0 to driver.storage.mgr.get_device_count() - 1 do begin if driver.storage.mgr.get_device(i) = vol^.device then begin devIdx := i; break; end; end; - addInfoRow(detail, dw, 'Parent Device', stringConcat('Disk ', intToString(devIdx))); + addInfoRow(detail, dw, 'Parent Device', uiConcat('Disk ', uiInt(devIdx))); if vol^.filesystem <> nil then fsName := vol^.filesystem^.sName @@ -1372,12 +1484,13 @@ begin fsName := 'Unknown'; addInfoRow(detail, dw, 'Filesystem', fsName); - addInfoRow(detail, dw, 'Sector Start', intToString(vol^.sectorStart)); - addInfoRow(detail, dw, 'Sector Count', intToString(vol^.sectorCount)); - addInfoRow(detail, dw, 'Sector Size', stringConcat(intToString(vol^.sectorSize), ' bytes')); + addInfoRow(detail, dw, 'Sector Start', uiInt(vol^.sectorStart)); + addInfoRow(detail, dw, 'Sector Count', uiInt(vol^.sectorCount)); + addInfoRow(detail, dw, 'Sector Size', uiConcat(uiInt(vol^.sectorSize), ' bytes')); - totalBytes := vol^.sectorCount * vol^.sectorSize; - freeBytes := vol^.freeSectors * vol^.sectorSize; + totalBytes := uint64(vol^.sectorCount) * vol^.sectorSize; + freeBytes := uint64(vol^.freeSectors) * vol^.sectorSize; + if freeBytes > totalBytes then freeBytes := totalBytes; usedBytes := totalBytes - freeBytes; addInfoRow(detail, dw, 'Total Size', formatSizePrecise(totalBytes)); @@ -1386,7 +1499,7 @@ begin if totalBytes > 0 then begin pctUsed := (usedBytes * 100) div totalBytes; - addInfoRow(detail, dw, 'Usage', stringConcat(intToString(pctUsed), '%')); + addInfoRow(detail, dw, 'Usage', uiConcat(uiInt(pctUsed), '%')); addUsageBar(detail, dw, usedBytes, totalBytes); end; @@ -1410,6 +1523,10 @@ begin else begin btn := makeButton(btn_row, 'Format', 100); lv_obj_add_event_cb(btn, @btn_format_cb, LV_EVENT_CLICKED, nil); + if vol^.filesystem <> nil then if vol^.filesystem^.setLabelCallback <> nil then begin + btn := makeButton(btn_row,'Rename Volume',150); + lv_obj_add_event_cb(btn,@label_edit_cb,LV_EVENT_CLICKED,nil); + end; end; end else begin addStatusLabel(detail, 'Device is read-only. Format disabled.', 160, 150, 130); @@ -1470,8 +1587,8 @@ var sizeStr : pchar; begin if sidebar = nil then exit; - lv_obj_clean(sidebar); active_sidebar_btn := nil; + lv_obj_clean(sidebar); { Sidebar title } hdr := lv_label_create(sidebar); @@ -1480,12 +1597,12 @@ begin lv_obj_set_style_text_font(hdr, @lv_font_montserrat_14, 0); devCnt := driver.storage.mgr.get_device_count(); - for i := 0 to devCnt - 1 do begin + if devCnt > 0 then for i := 0 to devCnt - 1 do begin device := driver.storage.mgr.get_device(i); if device = nil then continue; { Section label for this device } - addSectionHeader(sidebar, stringConcat('Disk ', intToString(i))); + addSectionHeader(sidebar, uiConcat('Disk ', uiInt(i))); { Clickable device row } btn := lv_button_create(sidebar); @@ -1506,7 +1623,7 @@ begin lv_obj_set_style_text_color(lbl, lv_color_make(210, 215, 230), 0); lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0); - sizeStr := formatSizeStr(device^.maxSectorCount * device^.sectorSize); + sizeStr := formatSizeStr(uint64(device^.maxSectorCount) * device^.sectorSize); lbl := lv_label_create(btn); lv_label_set_text(lbl, sizeStr); lv_obj_set_style_text_color(lbl, lv_color_make(140, 150, 170), 0); @@ -1520,7 +1637,7 @@ begin { List volumes that belong to this device } volCnt := driver.storage.vol.mgr.get_volume_count(); - for j := 0 to volCnt - 1 do begin + if volCnt > 0 then for j := 0 to volCnt - 1 do begin vol := driver.storage.vol.mgr.get_volume(j); if vol = nil then continue; if vol^.device <> device then continue; @@ -1539,14 +1656,16 @@ begin lv_obj_set_flex_align(btn, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lbl := lv_label_create(btn); - if vol^.filesystem <> nil then + if vol^.labelText[0] <> #0 then + lv_label_set_text(lbl,@vol^.labelText[0]) + else if vol^.filesystem <> nil then lv_label_set_text(lbl, vol^.filesystem^.sName) else lv_label_set_text(lbl, 'Volume'); lv_obj_set_style_text_color(lbl, lv_color_make(185, 195, 215), 0); lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0); - sizeStr := formatSizeStr(vol^.sectorCount * vol^.sectorSize); + sizeStr := formatSizeStr(uint64(vol^.sectorCount) * vol^.sectorSize); lbl := lv_label_create(btn); lv_label_set_text(lbl, sizeStr); lv_obj_set_style_text_color(lbl, lv_color_make(120, 130, 150), 0); @@ -1585,6 +1704,8 @@ procedure onClose(wid: uint32); begin debug.tracer.push_trace('diskutil.onClose'); closeMsgBox; + releaseTextTemps; + operation_pending := false; ui_session_id := ui_session_id + 1; puint32(@fmt_done_flag)^ := 0; puint32(@storage_dirty_flag)^ := 0; @@ -1694,6 +1815,7 @@ begin lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0); { Reset state } + operation_pending := false; sel_mode := SEL_NONE; sel_dev_idx := 0; sel_vol_idx := 0; @@ -1703,6 +1825,7 @@ begin fmt_param_area := nil; fmt_cluster_input := nil; fmt_filecount_input := nil; + fmt_label_input := nil; fmt_param_error_lbl := nil; add_textarea := nil; add_err_lbl := nil; @@ -1712,6 +1835,7 @@ begin fmt_done_vol_idx := 0; storage_dirty_flag := 0; fmt_poll_timer := lv_timer_create(@fmt_poll_cb, 50, nil); + if fmt_poll_timer = nil then begin onClose(win_id); exit; end; storage_watch_id := driver.storage.vol.mgr.watch_lifecycle(@diskutil_storage_event, nil); refreshSidebar; @@ -1729,6 +1853,31 @@ end; { ============================================================ Init — register with the driver.video.desktop program launcher ============================================================ } +procedure UnitTest; +var passed, failed, sectors, sectorSize : uint32; + procedure Assert(ok : boolean; name : pchar); + begin + if ok then Inc(passed) else begin Inc(failed); io.syslog.logln('DISKUTIL FAIL', name); end; + end; +begin + passed := 0; failed := 0; + sectors := 8388608; sectorSize := 512; + Assert(stringEquals(formatSizeStr(uint64(sectors)*sectorSize),'4 GB'),'4GB label'); + sectors := 10485760; + Assert(stringEquals(formatSizePrecise(uint64(sectors)*sectorSize),'5 GB'),'5GB label'); + releaseTextTemps; + Assert(TextTemps = nil,'temporary labels released'); + Assert(parseSize('4GB',512) = 8388608, '4GB does not wrap to zero'); + Assert(parseSize('5GB',512) = 10485760, '5GB does not wrap to 1GB'); + Assert(parseSize('999999999999999999999GB',512) = 0, 'reject decimal overflow'); + Assert(parseSize('2097152GB',512) = 0, 'reject LBA overflow'); + Assert(parseSize('513B',512) = 0, 'reject fractional sectors'); + Assert(parseSize('512B',0) = 0, 'reject zero sector size'); + Assert(parseSize('',512) = 0, 'reject empty size'); + io.syslog.log('DISKUTIL', 'Passed: '); io.syslog.writeint(passed); + io.syslog.writestring(' Failed: '); io.syslog.writeintln(failed); +end; + procedure init(); begin debug.tracer.push_trace('diskutil.init'); @@ -1745,6 +1894,7 @@ begin fmt_param_area := nil; fmt_cluster_input := nil; fmt_filecount_input := nil; + fmt_label_input := nil; fmt_param_error_lbl := nil; add_textarea := nil; add_err_lbl := nil; diff --git a/src/app/app.partcmd.pas b/src/app/app.partcmd.pas index cf25a86d..3364b07e 100644 --- a/src/app/app.partcmd.pas +++ b/src/app/app.partcmd.pas @@ -288,7 +288,9 @@ begin if bootrecord <> nil then begin if bootrecord^.boot_sector <> $AA55 then begin println('No valid mbr found, initialising disk...'); - driver.storage.vol.mgr.init_disk(device); + if driver.storage.vol.mgr.init_disk(device) <> eNone then begin + println('Error: disk initialization failed.'); exit; + end; end; kfree(puint32(bootrecord)); end; @@ -335,7 +337,9 @@ begin driver.storage.vol.mbr.setup_partition(@part, lba, sectors); part.system_id := 0; - driver.storage.vol.mgr.add_partition(device, slot, part); + if driver.storage.vol.mgr.add_partition(device, slot, part) <> eNone then begin + println('Error: partition creation failed.'); exit; + end; print('Partition added in slot '); printint(slot); @@ -407,7 +411,9 @@ begin exit; end; - driver.storage.vol.mgr.remove_partition(device, slot); + if driver.storage.vol.mgr.remove_partition(device, slot) <> eNone then begin + println('Error: partition removal failed.'); exit; + end; print('Partition slot '); printint(slot); @@ -523,11 +529,14 @@ begin exit; end; - vol^.filesystem := fs; - fs^.createCallback(vol, vol^.sectorCount, vol^.sectorStart, nil); - - { Re-probe filesystem so identify picks up the new signature } - driver.storage.fs.mgr.probe_volume(vol); + i := 0; + while i < driver.storage.vol.mgr.get_volume_count() do begin + if driver.storage.vol.mgr.get_volume(i) = vol then break; + Inc(i); + end; + if not driver.storage.vol.mgr.format_volume(device, i, fsName, nil) then begin + println('Error: format failed; the volume was not reported ready.'); exit; + end; print('Partition '); printint(slot); diff --git a/src/arch/x86/system.pas b/src/arch/x86/system.pas index b13b1038..d8d22ea4 100644 --- a/src/arch/x86/system.pas +++ b/src/arch/x86/system.pas @@ -271,6 +271,7 @@ procedure fpc_shortstr_assign(len: longint; sstr, dstr: Pointer); compilerproc; any int64/uint64 arithmetic occurs (e.g. sint32 * uint32 promotion). Must live in the system unit so the compiler can resolve it. } function fpc_mul_int64(f1, f2: int64): int64; compilerproc; +function fpc_mul_dword_to_qword(f1, f2: uint32): qword; compilerproc; function fpc_div_int64(d, n: int64): int64; compilerproc; function fpc_mod_int64(d, n: int64): int64; compilerproc; function fpc_mod_qword(d, n: qword): qword; compilerproc; @@ -523,6 +524,18 @@ begin fpc_mul_int64 := res; end; +function fpc_mul_dword_to_qword(f1, f2: uint32): qword; [public, alias: 'FPC_MUL_DWORD_TO_QWORD']; compilerproc; +var res: qword; +begin + asm + MOV EAX, f1 + MUL f2 + MOV DWORD [res], EAX + MOV DWORD [res+4], EDX + end; + fpc_mul_dword_to_qword := res; +end; + { ---------- 64-bit division / modulo ---------- } { Helper: unsigned 64÷64→64 using shift-subtract (binary long division). diff --git a/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas b/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas index 745d7c2f..83fc73fe 100644 --- a/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas +++ b/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas @@ -46,7 +46,7 @@ var procedure init(); function load(ptr : void) : boolean; procedure check_ports(controller : PAHCI_Controller); -procedure identify_device(controller : PAHCI_Controller; portIndex : uint32; isATAPI : boolean); +function identify_device(controller : PAHCI_Controller; portIndex : uint32; isATAPI : boolean) : boolean; procedure ahci_isr(); function find_cmd_slot(device : PAHCI_Device) : uint32; @@ -95,121 +95,63 @@ begin //TODO check driver.storage.ctl.ide devices in driver.storage.ctl.ide for sata devices end; -procedure stop_port(port : PHBA_Port); -var - timeout : uint32; +{ Poll MMIO through a pointer on every iteration. The PIT runs at ~8 kHz; + 16 psleep ticks give a conservative ~2 ms poll interval, not a CPU-speed loop. } +function wait_register(reg : puint32; mask, expected, attempts : uint32) : boolean; +var n : uint32; begin - // Clear ST (bit 0) first - port^.cmd := port^.cmd and not $1; - - // Wait for CR (bit 15) to clear - timeout := 0; - while (port^.cmd and $8000) <> 0 do begin - timeout := timeout + 1; - if timeout > 100000 then break; - end; - - // Clear FRE (bit 4) - port^.cmd := port^.cmd and not $10; - - // Wait for FR (bit 14) to clear - timeout := 0; - while (port^.cmd and $4000) <> 0 do begin - timeout := timeout + 1; - if timeout > 100000 then break; - end; + n := 0; + repeat + if (reg^ and mask) = expected then begin wait_register := true; exit; end; + if n >= attempts then break; + psleep(16); Inc(n); + until false; + wait_register := false; end; -procedure start_port(port : PHBA_Port); +function stop_port(port : PHBA_Port) : boolean; begin - // Wait for CR (bit 15) to clear before starting - while (port^.cmd and $8000) <> 0 do begin - end; + port^.cmd := port^.cmd and not $1; + stop_port := wait_register(@port^.cmd,$8000,0,250); + if not stop_port then exit; + port^.cmd := port^.cmd and not $10; + stop_port := wait_register(@port^.cmd,$4000,0,250); +end; - // Enable FRE (bit 4) first so port can receive FIS +function start_port(port : PHBA_Port) : boolean; +begin + start_port := wait_register(@port^.cmd,$8000,0,250); + if not start_port then exit; port^.cmd := port^.cmd or $10; - - // Then enable ST (bit 0) to allow command processing port^.cmd := port^.cmd or $1; end; -procedure reset_port(port : PHBA_Port); -var - timeout : uint32; +{ DMA addresses must already be installed before enabling FIS reception. } +function reset_port(port : PHBA_Port) : boolean; begin - // io.syslog.writestringln('driver.storage.ctl.ahci: Performing a full port reset.'); - - // Stop the port: clear ST (bit 0) + reset_port := false; port^.cmd := port^.cmd and not $1; - - // Wait until CR (bit 15) is cleared. - timeout := 0; - while (port^.cmd and $8000) <> 0 do begin - timeout := timeout + 1; - if timeout > 100000 then begin - io.syslog.writestringln('driver.storage.ctl.ahci: Port reset timeout (CR not clearing).'); - break; - end; - end; - - // Initiate COMRESET: set DET=1 in PxSCTL (bits 3:0) + if not wait_register(@port^.cmd,$8000,0,250) then exit; + port^.int_enable := 0; + port^.cmd := port^.cmd or $16; { FRE, power-on and spin-up } port^.sata_ctrl := (port^.sata_ctrl and $FFFFFFF0) or $1; - - // Wait for COMRESET to propagate (spec says >= 1ms) - psleep(2); - - // Clear COMRESET: set DET=0 + psleep(16); { >= 1 ms COMRESET assertion at the ~8 kHz PIT rate } port^.sata_ctrl := port^.sata_ctrl and $FFFFFFF0; - - // Wait for device detection (DET field in SStatus = 3) - timeout := 0; - while (port^.sata_status and $F) <> 3 do begin - timeout := timeout + 1; - if timeout > 100000 then begin - io.syslog.writestringln('driver.storage.ctl.ahci: Port reset timeout (waiting for device ready).'); - break; - end; - end; - - // Clear the SERR register by writing 1s to it. + if not wait_register(@port^.sata_status,$F,3,500) then exit; + if not wait_register(@port^.tfd,$88,0,2500) then exit; port^.sata_error := $FFFFFFFF; - - start_port(port); + port^.int_status := $FFFFFFFF; + reset_port := start_port(port); end; -// procedure reset_port(port : PHBA_Port); -// var -// ssts, serr, timeout : uint32; -// begin -// io.syslog.writestringln('driver.storage.ctl.ahci: Performing a full port reset.'); - -// port^.cmd := port^.cmd and not $1; - -// // Wait until CR (bit 15) is cleared. -// timeout := 0; -// while (port^.cmd and $8000) <> 0 do begin -// timeout := timeout + 1; -// if timeout > 1000000 then begin -// io.syslog.writestringln('driver.storage.ctl.ahci: Port reset timeout.'); -// break; -// end; -// end; - -// port^.sata_ctrl := port^.sata_ctrl and $FFFF0000; -// port^.sata_ctrl := port^.sata_ctrl or $1; -// psleep(10); -// port^.sata_ctrl := port^.sata_ctrl and $FFFF0000; - -// while (port^.sata_status and $F) <> $3 do begin -// //wait for the port to be ready -// end; - -// // Clear the SERR register by writing 1s to it. -// port^.sata_error := $FFFFFFFF; - -// // start_port(port); -// end; - +procedure log_port_failure(portIndex : uint32; port : PHBA_Port; reason : pchar); +begin + io.syslog.log('AHCI','Port '); io.syslog.writeint(portIndex); + io.syslog.writestring(': '); io.syslog.writestring(reason); + io.syslog.writestring(' SSTS='); io.syslog.writehex(port^.sata_status); + io.syslog.writestring(' SIG='); io.syslog.writehex(port^.signature); + io.syslog.writestring(' TFD='); io.syslog.writehexln(port^.tfd); +end; { Check the ports on the controller and setup the command list, FIS, and command table @@ -217,7 +159,9 @@ end; procedure check_ports(controller : PAHCI_Controller); var i : uint32; - ii : uint32; + ii, attempt, signatureWait : uint32; + registered : boolean; + listAlloc, fisAlloc, tableAlloc : puint32; port : PHBA_Port; device : PAHCI_Device; cmd_list_base : puint32; @@ -232,29 +176,18 @@ begin device := PAHCI_Device(@controller^.devices[i]); device^.port := port; - //NEEED TO STOP the port before doing anything - stop_port(port); - reset_port(port); - - { Read device signature AFTER reset so the device has re-sent - its D2H Register FIS with the correct signature. } - case port^.signature of - SATA_SIG_ATA: begin - device^.device_type := SATA; - end; - SATA_SIG_ATAPI: begin - device^.device_type := ATAPI; - end; - SATA_SIG_SEMB: begin - device^.device_type := SEMB; - end; - SATA_SIG_PM: begin - device^.device_type := PM; - end; + port^.int_enable := 0; + if not stop_port(port) then begin log_port_failure(i,port,'cannot stop engine'); continue; end; + { Bring up power before checking for an attached device. } + port^.cmd := port^.cmd or $6; + if not wait_register(@port^.sata_status,$F,3,100) then begin + if (port^.sata_status and $F) = 0 then continue; + { Present but not yet communicating: COMRESET below may recover it. } end; //allocate memory for the command list and ensure it is aligned to 1024 bytes // Need 32 command headers. Allocate extra (alignment-1) bytes for alignment. - cmd_list_base := kalloc(sizeof(THBA_CMD_HEADER) * 32 + 1023); + listAlloc := kalloc(sizeof(THBA_CMD_HEADER) * 32 + 1023); + cmd_list_base := listAlloc; cmd_list_base := puint32((uint32(cmd_list_base) + 1023) and $FFFFFC00); memset(uint32(cmd_list_base), 0, sizeof(THBA_CMD_HEADER) * 32); @@ -265,7 +198,8 @@ begin device^.command_list := PHBA_CMD_HEADER(cmd_list_base); //allocate memory for the FIS and ensure it is aligned to 256 bytes - fis_base := kalloc(sizeof(THBA_FIS) + 255); + fisAlloc := kalloc(sizeof(THBA_FIS) + 255); + fis_base := fisAlloc; fis_base := puint32((uint32(fis_base) + 255) and $FFFFFF00); //set the FIS base address @@ -279,7 +213,8 @@ begin //allocate memory for the command table and ensure it is aligned to 128 bytes // Need 32 command tables. Allocate extra (alignment-1) bytes for alignment. - cmd_table_base := kalloc(sizeof(THBA_CMD_TABLE) * 32 + 127); + tableAlloc := kalloc(sizeof(THBA_CMD_TABLE) * 32 + 127); + cmd_table_base := tableAlloc; cmd_table_base := puint32((uint32(cmd_table_base) + 127) and $FFFFFF80); memset(uint32(cmd_table_base), 0, sizeof(THBA_CMD_TABLE) * 32); @@ -294,14 +229,30 @@ begin command_header^.cmd_table_baseu := 0; end; - //reset the port - reset_port(port); - if (device^.device_type = SATA) then begin - identify_device(controller, i, false); - end else if (device^.device_type = ATAPI) then begin - identify_device(controller, i, true); - end else begin - io.syslog.writestringln('driver.storage.ctl.ahci: Unsupported device type'); + registered := false; + for attempt := 1 to 2 do begin + if not reset_port(port) then begin log_port_failure(i,port,'reset/readiness timeout'); continue; end; + signatureWait := 0; + while (puint32(@port^.signature)^ = 0) or (puint32(@port^.signature)^ = $FFFFFFFF) do begin + if signatureWait >= 500 then break; + psleep(16); Inc(signatureWait); + end; + case puint32(@port^.signature)^ of + SATA_SIG_ATA: begin device^.device_type := SATA; registered := identify_device(controller,i,false); end; + SATA_SIG_ATAPI: begin device^.device_type := ATAPI; registered := identify_device(controller,i,true); end; + else log_port_failure(i,port,'unsupported or missing signature'); + end; + if registered then break; + log_port_failure(i,port,'IDENTIFY failed; retrying detection'); + end; + if not registered then begin + port^.int_enable := 0; + if stop_port(port) then begin + port^.cmdl_basel := 0; port^.fis_basel := 0; + kfree(listAlloc); kfree(fisAlloc); kfree(tableAlloc); + device^.command_list := nil; device^.fis := nil; device^.command_table := nil; + end; + log_port_failure(i,port,'device unavailable after bounded retries'); end; controller^.mio^.int_status := $FFFFFFFF; @@ -309,7 +260,7 @@ begin end; end; -procedure identify_device(controller : PAHCI_Controller; portIndex : uint32; isATAPI : boolean); +function identify_device(controller : PAHCI_Controller; portIndex : uint32; isATAPI : boolean) : boolean; var fis : PHBA_FIS_REG_H2D; cmd_header : PHBA_CMD_HEADER; @@ -327,6 +278,7 @@ var slotCount : uint32; ncqSupported : boolean; begin + identify_device := false; device := PAHCI_Device(@controller^.devices[portIndex]); slotCount := ((controller^.mio^.capabilites shr 8) and $1F) + 1; device^.slotCount := slotCount; @@ -347,9 +299,7 @@ begin cmd_header^.wrt := 0; cmd_header^.prdtl := 1; cmd_header^.clear_busy := 1; - if isATAPI then begin - cmd_header^.atapi := 1; - end; + cmd_header^.atapi := 0; { IDENTIFY PACKET is ATA $A1, not an ATAPI PACKET $A0 command } // Setup the command table (using slot 0) @@ -375,56 +325,19 @@ begin end; fis^.device := $40; - //waut for the port to be ready, bit 7 in the TFD register and bit 3 tfd - while (device^.port^.tfd and $88) <> 0 do begin - // io.syslog.writestring('driver.storage.ctl.ahci: tfd: '); - // io.syslog.writehexln(device^.port^.tfd); + if not wait_register(@device^.port^.tfd,$88,0,2500) then begin + log_port_failure(portIndex,device^.port,'busy before IDENTIFY'); kfree(buffer); exit; end; - - // Issue the command by setting bit 0 in the port's Command Issue register. - cmd := device^.port^.cmd_issue; - cmd := cmd or $1; - device^.port^.cmd_issue := cmd; - - // io.syslog.writestringln('driver.storage.ctl.ahci: Sent identify command'); - - - // Wait for command completion with a timeout. - timeout := 0; - repeat - tfd := device^.port^.tfd; - - if device^.port^.int_status <> 0 then begin - device^.port^.int_status := device^.port^.int_status; - end; - - timeout := timeout + 1; - if timeout > 100 then begin - io.syslog.writestringln('driver.storage.ctl.ahci: IDENTIFY command timeout'); - break; - end; - if (tfd and $1) <> 0 then break; // ERR bit set - psleep(1); - until ((device^.port^.cmd_issue and $1) = 0); // Wait until slot 0 is cleared - - // io.syslog.writestringln('driver.storage.ctl.ahci: Command complete'); - - // Check if the command slot is still set (command didn't complete) - if (device^.port^.cmd_issue and $1) <> 0 then begin - io.syslog.writestringln('driver.storage.ctl.ahci: Error sending identify command'); - io.syslog.writestring('driver.storage.ctl.ahci: Error sata register: '); - io.syslog.writehexln(device^.port^.sata_error); - - { Cancel the pending command by stopping/starting the port so - no DMA fires after we free the buffer. } - stop_port(device^.port); - start_port(device^.port); - - { Re-enable port interrupts and bail — device is non-functional. } - device^.port^.int_enable := $4000000B; - kfree(buffer); + device^.port^.cmd_issue := 1; + if (not wait_register(@device^.port^.cmd_issue,1,0,2500)) or + ((device^.port^.tfd and $1) <> 0) or ((device^.port^.int_status and $40000000) <> 0) then begin + log_port_failure(portIndex,device^.port,'IDENTIFY timeout or task-file error'); + { Free DMA memory only once both engines have stopped. If hardware cannot + stop, retaining this one buffer is safer than a late DMA into freed memory. } + if stop_port(device^.port) then kfree(buffer); exit; end; + device^.port^.int_status := $FFFFFFFF; { Re-enable port interrupts now that IDENTIFY polling is done. $4000000B = bit 0 (D2H Register FIS) | bit 1 (PIO Setup FIS) | bit 3 (Set Device Bits / NCQ) | bit 30 (Task File Error) @@ -488,12 +401,14 @@ begin end; driver.storage.mgr.register_device(storageDev); + kfree(void(storageDev)); { manager owns a copy } + identify_device := true; io.syslog.writestring('driver.storage.ctl.ahci: Registered device on port '); io.syslog.writeint(portIndex); if sec_count > 0 then begin io.syslog.writestring(' with storage manager ('); - io.syslog.writeint((sec_count * storageDev_sectorSize) div 1024 div 1024); + io.syslog.writeint((uint64(sec_count) * storageDev_sectorSize) div 1024 div 1024); io.syslog.writestringln(' MB)'); end else begin io.syslog.writestringln(' with storage manager (no media)'); diff --git a/src/driver/storage/driver.storage.metatest.pas b/src/driver/storage/driver.storage.metatest.pas new file mode 100644 index 00000000..83219061 --- /dev/null +++ b/src/driver/storage/driver.storage.metatest.pas @@ -0,0 +1,204 @@ +unit driver.storage.metatest; + +interface +procedure UnitTest; +procedure init; + +implementation +uses boot.mgr, io.stdio, io.syslog, memory.heap, core.util, core.ds.cfifo, + driver.storage.types, driver.storage.mgr, driver.storage.vol.mbr, + driver.storage.vol.mgr, driver.storage.fs.fat32.types, + driver.storage.fs.fat32.vol, driver.storage.fs.fat32.labels, core.strings; + +type + PMock = ^TMock; + TMock = record + Data : puint8; + Writes, FailWrite, Callbacks : uint32; + Error : TError; + OutOfBounds, FailRead : boolean; + end; + +procedure dispatch(device : PStorage_Device; request : PIORequest); +var m : PMock; offset, size : uint32; +begin + m := PMock(device^.controllerId0); + if (request^.LBA >= device^.maxSectorCount) or + (request^.SectorCount > device^.maxSectorCount-request^.LBA) then begin + m^.OutOfBounds := true; complete_io(request, false, eInvalidArgument); exit; + end; + if (request^.RequestType <> ioWrite) and m^.FailRead then begin complete_io(request, false, eIOError); exit; end; + offset := request^.LBA*512; size := request^.SectorCount*512; + if request^.RequestType = ioWrite then begin + Inc(m^.Writes); + if m^.Writes = m^.FailWrite then begin complete_io(request, false, eIOError); exit; end; + memcpy(uint32(request^.Buffer), uint32(m^.Data)+offset, size); + end else memcpy(uint32(m^.Data)+offset, uint32(request^.Buffer), size); + request^.ByteCount := size; + complete_io(request, true, eNone); +end; + +procedure completion(error : TError; data : pointer); +var m : PMock; +begin m := PMock(data); Inc(m^.Callbacks); m^.Error := error; end; + +{ Only heap-backed synthetic storage is used. Never registers a hardware device, + never accesses a host/guest disk, and every fake request completes inline. } +procedure UnitTest; +var + device : PStorage_Device; + volume, added : PStorage_Volume; + mock : PMock; + br : PBootRecord; + mbr, before : PMaster_Boot_Record; + params : TFSFormatParams; + labelValue : TVolumeLabel; + part : TPartition_table; + passed, failed, oldCount, writes, i : uint32; + err : TError; + procedure Assert(ok : boolean; name : pchar); + begin + if ok then Inc(passed) else begin Inc(failed); io.syslog.logln('STORMETA FAIL', name); end; + end; +begin + passed := 0; failed := 0; + device := PStorage_Device(kalloc(sizeof(TStorage_Device))); + volume := PStorage_Volume(kalloc(sizeof(TStorage_Volume))); + mock := PMock(kalloc(sizeof(TMock))); + br := PBootRecord(kalloc(sizeof(TBootRecord))); + if (device = nil) or (volume = nil) or (mock = nil) or (br = nil) then begin + if device <> nil then kfree(void(device)); + if volume <> nil then kfree(void(volume)); + if mock <> nil then kfree(void(mock)); + if br <> nil then kfree(void(br)); + io.syslog.logln('STORMETA FAIL', 'test allocation'); exit; + end; + memset(uint32(device),0,sizeof(TStorage_Device)); + memset(uint32(volume),0,sizeof(TStorage_Volume)); + memset(uint32(mock),0,sizeof(TMock)); memset(uint32(br),0,sizeof(TBootRecord)); + mock^.Data := puint8(kalloc(256*512)); + memset(uint32(mock^.Data),$CC,256*512); + device^.controllerId0 := uint32(mock); device^.sectorSize := 512; + device^.maxSectorCount := 256; device^.writable := true; device^.maxActive := 1; + device^.dispatchRead := @dispatch; device^.dispatchWrite := @dispatch; + device^.requestQueue := CFIFO_New(sizeof(Pointer),4); + device^.cachedMBR := kalloc(512); + mbr := PMaster_Boot_Record(device^.cachedMBR); + memset(uint32(mbr),0,512); mbr^.boot_sector := $AA55; mbr^.signature := $12345678; + Assert(write_mbr(device,mbr) = eNone,'aliased MBR write succeeds'); + Assert(PMaster_Boot_Record(mock^.Data)^.signature = $12345678,'aliased MBR bytes preserved'); + before := PMaster_Boot_Record(device^.cachedMBR); + mock^.FailWrite := mock^.Writes+1; + Assert(write_mbr(device,before) = eIOError,'write failure propagated'); + Assert(device^.cachedMBR = before,'failed write retains cache'); + mock^.FailWrite := 0; mock^.Callbacks := 0; + write_mbr_async(device,PMaster_Boot_Record(device^.cachedMBR),@completion,mock); + Assert((mock^.Callbacks = 1) and (mock^.Error = eNone),'async MBR callback once'); + Assert(PMaster_Boot_Record(mock^.Data)^.signature = $12345678,'async alias preserved'); + before := PMaster_Boot_Record(device^.cachedMBR); mock^.Callbacks := 0; + mock^.FailWrite := mock^.Writes+1; + write_mbr_async(device,before,@completion,mock); + Assert((mock^.Callbacks = 1) and (mock^.Error = eIOError),'async failure callback once'); + Assert(device^.cachedMBR = before,'async failure retains cache'); + mock^.FailWrite := 0; + volume^.device := device; volume^.sectorStart := 64; volume^.sectorCount := 128; volume^.sectorSize := 512; + memset(uint32(@params),0,sizeof(params)); params.Version := 1; + params.Flags := FS_FORMAT_PARAM_CLUSTER_SIZE; params.ClusterSize := 1; + Assert(validate_format(volume,128,64,@params) = eNone,'valid format geometry'); + params.ClusterSize := 3; + Assert(validate_format(volume,128,64,@params) = eInvalidArgument,'reject non-power-of-two clusters'); + params.ClusterSize := 1; volume^.sectorCount := 33; writes := mock^.Writes; + Assert(create_volume(volume,33,64,@params) = eInvalidArgument,'reject tiny partition'); + Assert(mock^.Writes = writes,'rejected format does not write'); + volume^.sectorCount := 128; + for i := 1 to 6 do begin + mock^.FailWrite := mock^.Writes+i; + Assert(create_volume(volume,128,64,@params) = eIOError,'each failed format write propagated'); + end; + mock^.FailWrite := 0; + Assert(create_volume(volume,128,64,@params) = eNone,'format completes'); + Assert(not mock^.OutOfBounds,'all writes within device'); + Assert((mock^.Data[63*512] = $CC) and (mock^.Data[192*512] = $CC),'neighbor partitions untouched'); + memcpy(uint32(mock^.Data)+64*512,uint32(br),sizeof(TBootRecord)); + Assert(FAT32ClusterLimit(br) = 97,'cluster bound excludes FAT padding'); + Assert(identify_volume(volume),'fresh format identified'); + Assert(volume^.freeSectors = 93,'free space excludes metadata and allocated directories'); + mock^.FailRead := true; + Assert(not identify_volume(volume),'failed probe read cannot report success'); + mock^.FailRead := false; + Assert(normalize_label(' projects ',labelValue) = eNone,'label accepts spaces'); + Assert(stringEquals(@labelValue[0],'PROJECTS'),'label trims and uppercases'); + Assert(normalize_label('ABCDEFGHIJKL',labelValue) = eInvalidArgument,'reject long label'); + Assert(normalize_label('BAD/NAME',labelValue) = eInvalidArgument,'reject invalid label character'); + writes := mock^.Writes; + Assert(write_label(volume,'BAD/NAME') = eInvalidArgument,'invalid rename rejected'); + Assert(mock^.Writes = writes,'invalid label does not write'); + Assert(write_label(volume,'Projects') = eNone,'write volume label'); + volume^.labelText[0] := #0; + Assert(read_label(volume) = eNone,'reload label from disk'); + Assert(stringEquals(@volume^.labelText[0],'PROJECTS'),'label survives cache reset'); + Assert(PDirectory(uint32(mock^.Data)+97*512)^.fileName[0] = 'S','rename preserves SYSTEM directory'); + { Add a valid backup boot sector and verify all three metadata writes. } + PBootRecord(uint32(mock^.Data)+64*512)^.backupCluster := 6; + memcpy(uint32(mock^.Data)+64*512,uint32(mock^.Data)+70*512,512); + for i := 1 to 3 do begin + mock^.FailWrite := mock^.Writes+i; + Assert(write_label(volume,'NEW NAME') = eIOError,'label write failure propagated'); + mock^.FailWrite := 0; + Assert(read_label(volume) = eNone,'label readable after failed rename'); + Assert(stringEquals(@volume^.labelText[0],'PROJECTS'),'failed rename retains previous label'); + end; + Assert(write_label(volume,'ABCDEFGHIJK') = eNone,'maximum length label'); + Assert(PBootRecord(uint32(mock^.Data)+70*512)^.volumeLabel[10] = ord('K'),'backup label updated'); + Assert(write_label(volume,'') = eNone,'clear label'); + volume^.labelText[0] := 'X'; + Assert(read_label(volume) = eNone,'reload cleared label'); + Assert(volume^.labelText[0] = #0,'blank label persists'); + params.Flags := params.Flags or FS_FORMAT_PARAM_LABEL; + memcpy(uint32(pchar('MY DATA')),uint32(@params.LabelText[0]),8); + Assert(create_volume(volume,128,64,@params) = eNone,'format with label'); + Assert(read_label(volume) = eNone,'read format label'); + Assert(stringEquals(@volume^.labelText[0],'MY DATA'),'format label persists'); + Assert(PDirectory(uint32(mock^.Data)+97*512)^.attributes = $08,'format creates root volume label entry'); + Assert(not mock^.OutOfBounds,'label operations stay inside volume'); + oldCount := get_volume_count(); + memset(uint32(@part),0,sizeof(part)); part.LBA_start := 64; part.sector_count := 128; + mock^.FailWrite := mock^.Writes+1; + Assert(add_partition(device,0,part) = eIOError,'failed partition creation reported'); + Assert(get_volume_count() = oldCount,'failed create publishes no volume'); + Assert(PMaster_Boot_Record(device^.cachedMBR)^.partition[0].sector_count = 0,'failed create leaves MBR cache'); + mock^.FailWrite := 0; + Assert(add_partition(device,0,part) = eNone,'partition creation committed'); + added := get_volume(get_volume_count()-1); + added^.lifecycleFlags := STORAGE_VOLUME_FLAG_FORMATTING; + writes := mock^.Writes; + Assert(remove_partition(device,0) = eFileInUse,'delete rejects formatting volume'); + Assert(mock^.Writes = writes,'busy delete leaves disk untouched'); + mock^.Callbacks := 0; + Assert(not format_volume_async(device,get_volume_count()-1,'FAT32',@params,@completion,mock),'busy format rejected'); + Assert(mock^.Callbacks = 0,'false submission never calls callback'); + added^.lifecycleFlags := 0; + mock^.FailWrite := mock^.Writes+1; + Assert(remove_partition(device,0) = eIOError,'delete propagates failed MBR write'); + Assert(get_volume_count() = oldCount+1,'failed delete retains volume'); + mock^.FailWrite := 0; + Assert(remove_partition(device,0) = eNone,'delete commits'); + Assert(get_volume_count() = oldCount,'delete detaches volume after write'); + kfree(device^.cachedMBR); CFIFO_Free(device^.requestQueue); + kfree(void(mock^.Data)); kfree(void(mock)); kfree(void(volume)); kfree(void(device)); kfree(void(br)); + io.syslog.log('STORMETA','Passed: '); io.syslog.writeint(passed); + io.syslog.writestring(' Failed: '); io.syslog.writeintln(failed); +end; + +procedure command(params : PParamList; stdin_buf, stdout_buf, stderr_buf : POutBuf); +begin + UnitTest; + io.stdio.bufWriteStrLn(stdout_buf,'Storage metadata regression results written to serial log.'); +end; + +procedure init; +begin io.stdio.registerCommand('STORMETA',@command,'Run memory-backed storage metadata regression tests'); end; + +initialization + boot.mgr.registerBoot('driver.storage.metatest',@init,'Storage Metadata Tests',BOOT_MGR_BARRIER_MID); +end. diff --git a/src/driver/storage/driver.storage.mgr.pas b/src/driver/storage/driver.storage.mgr.pas index 22bf178e..7c8ef025 100644 --- a/src/driver/storage/driver.storage.mgr.pas +++ b/src/driver/storage/driver.storage.mgr.pas @@ -68,7 +68,7 @@ function get_device_count() : uint32; function get_device(index : uint32) : PStorage_Device; function controller_type_2_string(controllerType : TControllerType) : pchar; function read_mbr(device : PStorage_Device) : PMaster_Boot_Record; -procedure write_mbr(device : PStorage_Device; mbr : PMaster_Boot_Record); +function write_mbr(device : PStorage_Device; mbr : PMaster_Boot_Record) : TError; { Return pointer to cached mbr data (nil if not cached). Caller must NOT free the returned pointer. } @@ -139,6 +139,7 @@ type hardware completion fires, then frees it before forwarding the user's callback. } TMBRWriteAsyncData = record Buffer : pointer; + Device : PStorage_Device; UserCallback : TIOCallback; UserData : pointer; end; @@ -181,17 +182,33 @@ begin alloc_mbr_io_buffer := buf; end; +function claim_mbr(device : PStorage_Device) : boolean; +var flags : uint32; +begin + asm pushfd; cli; pop flags end; + claim_mbr := not device^.mbrWriteBusy; + if claim_mbr then device^.mbrWriteBusy := true; + if (flags and $200) <> 0 then asm sti end; +end; + +procedure publish_mbr(device : PStorage_Device; buffer : pointer); +var old : pointer; +begin + { The DMA buffer becomes the cache only after a successful write. } + old := device^.cachedMBR; + device^.cachedMBR := buffer; + if old <> nil then kfree(old); +end; + procedure mbr_write_async_callback(error : TError; userdata : pointer); -var - ctx : PMBRWriteAsyncData; +var ctx : PMBRWriteAsyncData; begin ctx := PMBRWriteAsyncData(userdata); if ctx = nil then exit; - - if ctx^.Buffer <> nil then - kfree(void(ctx^.Buffer)); - if ctx^.UserCallback <> nil then - ctx^.UserCallback(error, ctx^.UserData); + if error = eNone then publish_mbr(ctx^.Device, ctx^.Buffer) + else kfree(ctx^.Buffer); + ctx^.Device^.mbrWriteBusy := false; + if ctx^.UserCallback <> nil then ctx^.UserCallback(error, ctx^.UserData); kfree(void(ctx)); end; @@ -231,6 +248,8 @@ begin device^.activeCount := 0; if device^.maxActive = 0 then device^.maxActive := 1; device^.cachedMBR := nil; + device^.metadataBusy := false; + device^.mbrWriteBusy := false; device^.isBootDevice := false; { dispatchRead/dispatchWrite: left as-is — driver sets them before calling register_device. memset(0) guarantees nil for unmigrated drivers. } @@ -334,27 +353,21 @@ begin end; end; -procedure write_mbr(device : PStorage_Device; mbr : PMaster_Boot_Record); -var - oldCache : pointer; - cache : puint32; - buf : puint32; +function write_mbr(device : PStorage_Device; mbr : PMaster_Boot_Record) : TError; +var buf : puint32; begin - push_trace('driver.storage.mgr.write_mbr'); - if (device = nil) or (mbr = nil) or (not device^.writable) then exit; - { Update cache } - oldCache := device^.cachedMBR; - cache := puint32(kalloc(sizeof(TMaster_Boot_Record))); - if cache <> nil then - memcpy(uint32(mbr), uint32(cache), sizeof(TMaster_Boot_Record)); - device^.cachedMBR := pointer(cache); - if oldCache <> nil then - kfree(oldCache); - + write_mbr := eInvalidArgument; + if (device = nil) or (mbr = nil) then exit; + if not device^.writable then begin write_mbr := eReadOnly; exit; end; + if not claim_mbr(device) then begin write_mbr := eFileInUse; exit; end; + { Snapshot first: mbr may alias device^.cachedMBR. } buf := alloc_mbr_io_buffer(device, mbr); - if buf = nil then exit; - storage_write(device, 0, 1, buf); - kfree(void(buf)); + if buf = nil then write_mbr := eOutOfMemory + else begin + write_mbr := storage_write(device, 0, 1, buf); + if write_mbr = eNone then publish_mbr(device, buf) else kfree(buf); + end; + device^.mbrWriteBusy := false; end; function get_cached_mbr(device : PStorage_Device) : PMaster_Boot_Record; @@ -365,53 +378,32 @@ end; procedure write_mbr_async(device : PStorage_Device; mbr : PMaster_Boot_Record; callback : TIOCallback; callbackData : pointer); -var - oldCache : pointer; - cache : puint32; - buf : puint32; - ctx : PMBRWriteAsyncData; - err : TError; +var ctx : PMBRWriteAsyncData; buf : puint32; err : TError; begin - push_trace('driver.storage.mgr.write_mbr_async'); - if (device = nil) or (mbr = nil) then begin - if callback <> nil then callback(eInvalidArgument, callbackData); + err := eNone; + if (device = nil) or (mbr = nil) then err := eInvalidArgument + else if not device^.writable then err := eReadOnly + else if not claim_mbr(device) then err := eFileInUse; + if err <> eNone then begin + if callback <> nil then callback(err, callbackData); exit; end; - if not device^.writable then begin - if callback <> nil then callback(eReadOnly, callbackData); - exit; - end; - { Update cache } - oldCache := device^.cachedMBR; - cache := puint32(kalloc(sizeof(TMaster_Boot_Record))); - if cache <> nil then - memcpy(uint32(mbr), uint32(cache), sizeof(TMaster_Boot_Record)); - device^.cachedMBR := pointer(cache); - if oldCache <> nil then - kfree(oldCache); - buf := alloc_mbr_io_buffer(device, mbr); if buf = nil then begin + device^.mbrWriteBusy := false; if callback <> nil then callback(eOutOfMemory, callbackData); exit; end; - ctx := PMBRWriteAsyncData(kalloc(sizeof(TMBRWriteAsyncData))); if ctx = nil then begin - kfree(void(buf)); + kfree(buf); device^.mbrWriteBusy := false; if callback <> nil then callback(eOutOfMemory, callbackData); exit; end; - ctx^.Buffer := pointer(buf); - ctx^.UserCallback := callback; - ctx^.UserData := callbackData; - + ctx^.Buffer := buf; ctx^.Device := device; + ctx^.UserCallback := callback; ctx^.UserData := callbackData; err := storage_write_async(device, 0, 1, buf, @mbr_write_async_callback, ctx); - if err <> eNone then begin - kfree(void(buf)); - kfree(void(ctx)); - if callback <> nil then callback(err, callbackData); - end; + if err <> eNone then mbr_write_async_callback(err, ctx); end; { ========================================================================== } diff --git a/src/driver/storage/driver.storage.types.pas b/src/driver/storage/driver.storage.types.pas index 0a71a094..5027fe2d 100644 --- a/src/driver/storage/driver.storage.types.pas +++ b/src/driver/storage/driver.storage.types.pas @@ -154,7 +154,8 @@ type sleVolumeInvalidated, sleVolumeRemoved, sleDeviceAdded, - sleDeviceRemoved + sleDeviceRemoved, + sleVolumeLabelChanged ); TStorageLifecycleCallback = procedure(event : TStorageLifecycleEvent; @@ -163,6 +164,8 @@ type error : TError; userdata : pointer); + TVolumeLabel = array[0..32] of char; { cached, NUL-terminated display name } + TFSFormatParams = record Version : uint16; Flags : uint16; @@ -186,13 +189,15 @@ type TIOCallback = procedure(error : TError; userdata : pointer); { Filesystem callback core.types } - PPCreateHook = procedure(volume : PStorage_volume; start : uint32; size : uint32; config : PFSFormatParams); + PPCreateHook = function(volume : PStorage_volume; sectors : uint32; start : uint32; config : PFSFormatParams) : TError; PPCreateAsyncHook = procedure(volume : PStorage_volume; start : uint32; size : uint32; config : PFSFormatParams; callback : TIOCallback; callbackData : pointer); PPDetectHook = procedure(disk : PStorage_Device); PPCreateDirHook = procedure(volume : PStorage_volume; directory : pchar; dirname : pchar; attributes : uint32; status : puint32); PPReadDirHook = function(volume : PStorage_volume; directory : pchar; status : puint32) : PLinkedListBase; PPDeleteFileHook = procedure(volume : PStorage_Volume; filePath : pchar; status : puint32); PPDeleteDirHook = procedure(volume : PStorage_Volume; path : pchar; status : puint32); + PPValidateLabelHook = function(text : pchar; var normalized : TVolumeLabel) : TError; + PPSetLabelHook = function(volume : PStorage_Volume; text : pchar) : TError; PPIdentifyHook = function(volume : PStorage_Volume) : boolean; PPRenameFileHook = procedure(volume : PStorage_Volume; filePath : pchar; newName : pchar; status : puint32); { Offset-based read: reads byteCount bytes starting at byte offset into the file. @@ -271,6 +276,8 @@ type maxSectorCount : uint32; sectorSize : uint32; //in bytes start : uint32; //start of device in sectors + metadataBusy : boolean; { volume/partition operation owns device } + mbrWriteBusy : boolean; { serializes staged sector-zero writes } cachedMBR : pointer; { cached mbr data, nil if not yet read } isBootDevice : boolean; { true if this is the device GRUB booted from } end; @@ -280,6 +287,7 @@ type sName : pchar; system_id : uint8; createCallback : PPCreateHook; + validateFormat : PPCreateHook; { pure preflight, before invalidation } detectCallback : PPDetectHook; createDirCallback : PPCreateDirHook; readDirCallback : PPReadDirHook; @@ -304,6 +312,8 @@ type openFileCallback : PPOpenFileHook; closeFileCallback : PPCloseFileHook; formatParamFlags : uint32; + validateLabelCallback : PPValidateLabelHook; + setLabelCallback : PPSetLabelHook; end; { Generic storage volume } @@ -317,6 +327,7 @@ type filesystem : PFilesystem; isBootDrive : boolean; lifecycleFlags : uint32; + labelText : TVolumeLabel; fsPrivate : pointer; { FS-driver private data (e.g. PFATCache for FAT32) } end; diff --git a/src/driver/storage/driver.storage.vfs.pas b/src/driver/storage/driver.storage.vfs.pas index bf676d97..b9521175 100644 --- a/src/driver/storage/driver.storage.vfs.pas +++ b/src/driver/storage/driver.storage.vfs.pas @@ -134,6 +134,7 @@ procedure UnwatchDirectory(WatchID : uint32); { Volume invalidation — must be called before freeing a volume. Closes all open FDs referencing the volume across all processes, evicts directory cache entries, and removes VFS mount points. } +function VolumeHasOpenHandles(vol : PStorage_Volume) : boolean; function InvalidateVolume(vol : PStorage_Volume) : boolean; { VFS Functions } @@ -509,7 +510,7 @@ end; function GetObjectFromPath(path : pchar) : PVFSObject; forward; function GetObjectFromPathEx(path : pchar; var volRelPath : pchar) : PVFSObject; forward; -function volumeHasAsyncHandles(vol : PStorage_Volume) : boolean; +function volumeHasHandles(vol : PStorage_Volume; asyncOnly : boolean) : boolean; var pCount : uint32; pi : uint32; @@ -517,7 +518,7 @@ var pCtx : proc.types.PProcessContext; tbl : PFDTable; begin - volumeHasAsyncHandles := false; + volumeHasHandles := false; if vol = nil then exit; pCount := proc.mgr.processCount(); @@ -530,14 +531,17 @@ begin if tbl = nil then continue; for fi := 0 to MAX_FDS - 1 do begin if tbl^.Entries[fi].InUse and (tbl^.Entries[fi].Volume = vol) - and (tbl^.Entries[fi].AsyncRefs > 0) then begin - volumeHasAsyncHandles := true; + and ((not asyncOnly) or (tbl^.Entries[fi].AsyncRefs > 0)) then begin + volumeHasHandles := true; exit; end; end; end; end; +function VolumeHasOpenHandles(vol : PStorage_Volume) : boolean; +begin VolumeHasOpenHandles := volumeHasHandles(vol,false); end; + function InvalidateVolume(vol : PStorage_Volume) : boolean; var pCount : uint32; @@ -554,7 +558,7 @@ begin debug.tracer.push_trace('driver.storage.vfs.InvalidateVolume.enter'); InvalidateVolume := false; if vol = nil then exit; - if volumeHasAsyncHandles(vol) then begin + if volumeHasHandles(vol,true) then begin debug.tracer.push_trace('driver.storage.vfs.InvalidateVolume.busy'); exit; end; @@ -986,6 +990,7 @@ begin volumeGetDirectories := nil; if vol = nil then exit; + if (vol^.device = nil) or vol^.device^.metadataBusy or driver.storage.vol.mgr.volume_is_busy(vol) then exit; if vol^.filesystem = nil then exit; if vol^.filesystem^.readDirCallback = nil then exit; @@ -1153,6 +1158,15 @@ begin exit; end; + mountObj := PVFSObject(core.ds.hashmap.get(ht, mountName)); + if mountObj <> nil then begin + if (mountObj^.ObjectType = otDRIVE) and (mountObj^.Reference = void(volume)) then begin + mountVolume := pvRegistered; + driver.storage.vol.mgr.volume_set_mounted(volume, true); + end; + kfree(void(parentPath)); STRLL_Free(splitPath); exit; + end; + { Create the mount object storing the volume pointer } mountObj := PVFSObject(kalloc(sizeof(TVFSObject))); @@ -1219,6 +1233,12 @@ begin end; volOut^ := PStorage_Volume(Obj^.Reference); + if (volOut^ = nil) or (volOut^^.device = nil) then begin + if volRel <> nil then kfree(void(volRel)); exit; + end; + if volOut^^.device^.metadataBusy or driver.storage.vol.mgr.volume_is_busy(volOut^) then begin + if volRel <> nil then kfree(void(volRel)); volOut^ := nil; exit; + end; { volRel is the path within the volume, e.g. '/mydir/myfile.txt' } if (volRel = nil) or StringEquals(volRel, '/') then begin diff --git a/src/driver/storage/fs/driver.storage.fs.flatfs.pas b/src/driver/storage/fs/driver.storage.fs.flatfs.pas index 10b53fe2..73052ac0 100644 --- a/src/driver/storage/fs/driver.storage.fs.flatfs.pas +++ b/src/driver/storage/fs/driver.storage.fs.flatfs.pas @@ -63,7 +63,7 @@ var procedure init; -procedure create_volume(volume : PStorage_Volume; sectors : uint32; start : uint32; config : PFSFormatParams); +function create_volume(volume : PStorage_Volume; sectors : uint32; start : uint32; config : PFSFormatParams) : TError; procedure detect_volumes(disk : PStorage_Device); function read_directory(volume : PStorage_Volume; directory : pchar; status : PuInt32) : PLinkedListBase; procedure write_directory(volume : PStorage_Volume; directory : pchar; status : PuInt32); @@ -72,87 +72,55 @@ procedure read_file(volume : PStorage_Volume; fileName : pchar; data : PuInt32; implementation -procedure create_volume(volume : PStorage_Volume; sectors : uint32; start : uint32; config : PFSFormatParams); -var - info : PDisk_Info; - entryTable : PFile_Entry; - i : uint32; - - directories : PLinkedListBase; - status : PuInt32; - - data : pchar; - -const - asuroArray : array[0..7] of char = 'ASURO'; - systemArray : array[0..6] of char = '/system'; - programsArray : array[0..8] of char = '/programs'; - userArray : array[0..4] of char = '/user'; - - testArray : array[0..23] of char = 'ASURO TEST DATA ARRAY'; +function validate_format(volume : PStorage_Volume; sectors, start : uint32; config : PFSFormatParams) : TError; begin - - info := PDisk_Info(Kalloc(512)); - memset(uint32(info), 0, 512); - - info^.jmp2boot := $0; - info^.OEMName := asuroArray; - info^.version := 1; - info^.sectorCount := sectors; - info^.fileCount := 1000; - info^.signature := $0B00B1E5; - - if (config <> nil) and - (config^.Version = FS_FORMAT_PARAMS_VERSION_1) and - ((config^.Flags and FS_FORMAT_PARAM_FILE_COUNT) <> 0) and - (config^.FileCount <> 0) then begin - info^.fileCount := config^.FileCount; + validate_format := eInvalidArgument; + if (volume = nil) or (volume^.device = nil) then exit; + if not volume^.device^.writable then begin validate_format := eReadOnly; exit; end; + { Legacy FlatFS readers use 512-byte sectors and exactly 1000 entries. } + if (volume^.device^.sectorSize <> 512) or (sectors > 65535) or + (sectors <= 126) or (start <> volume^.sectorStart) or + (sectors <> volume^.sectorCount) or (start >= volume^.device^.maxSectorCount) or + (sectors > volume^.device^.maxSectorCount-start) then exit; + if config <> nil then begin + if (config^.Version <> FS_FORMAT_PARAMS_VERSION_1) or + ((config^.Flags and (not FS_FORMAT_PARAM_FILE_COUNT)) <> 0) then exit; + if ((config^.Flags and FS_FORMAT_PARAM_FILE_COUNT) <> 0) and + (config^.FileCount <> 1000) then exit; end; + validate_format := eNone; +end; - driver.storage.mgr.storage_write(volume^.device, start, 1, PuInt32(info));// what happens if buffer is smaller than 512? - - // create file table - entryTable := PFile_Entry(Kalloc(126 * 512)); - memset(uint32(entryTable), 0, 126 * 512); - - //create system folders - entryTable[0].attribues := $10; - memcpy(uint32(@systemArray), uint32(@entryTable[0].name), 7); - entryTable[0].size := 0; - entryTable[0].start := 0; - - entryTable[1].attribues := $10; - memcpy(uint32(@programsArray), uint32(@entryTable[1].name), 9); - entryTable[1].size := 0; - entryTable[1].start := 0; - - entryTable[2].attribues := $10; - memcpy(uint32(@userArray), uint32(@entryTable[2].name), 5); - entryTable[2].size := 0; - entryTable[2].start := 0; - - //write file table - driver.storage.mgr.storage_write(volume^.device, start + 1, ((sizeof(TFile_Entry) * info^.fileCount) div 512), puint32(entryTable)); - //volume^.device^.writeCallback(volume^.device, start + 1, 1, puint32(entryTable)); - - //temp read - status := PuInt32(Kalloc(sizeof(uint32))); - // directories := read_directory(volume, '/', status); - - data := stringNew(1024); - memset(uint32(data), 0, 513); - memcpy(uint32(@testArray), uint32(data), 23); - - write_directory(volume, '/logs', status); - write_directory(volume, '/logs/test', status); - write_file(volume, '/logs/log.txt', PuInt32(data), 1, status); - write_file(volume, '/logs/log2.txt', PuInt32(data), 1, status); - write_file(volume, '/logs/log3.txt', PuInt32(data), 1, status); - read_directory(volume, '/logs', status); - - kfree(puint32(data)); - - read_file(volume, '/logs/log.txt', PuInt32(data), status); +function create_volume(volume : PStorage_Volume; sectors : uint32; start : uint32; config : PFSFormatParams) : TError; +var info : PDisk_Info; entries : PFile_Entry; err : TError; +begin + create_volume := validate_format(volume, sectors, start, config); + if create_volume <> eNone then exit; + info := PDisk_Info(kalloc(512)); entries := PFile_Entry(kalloc(125*512)); + if (info = nil) or (entries = nil) then begin + if info <> nil then kfree(void(info)); + if entries <> nil then kfree(void(entries)); + create_volume := eOutOfMemory; exit; + end; + memset(uint32(info), 0, 512); memset(uint32(entries), 0, 125*512); + err := storage_write(volume^.device, start, 1, puint32(info)); + if err = eNone then begin + entries[0].attribues := $10; + memcpy(uint32(pchar('/system')), uint32(@entries[0].name[0]), 7); + entries[1].attribues := $10; + memcpy(uint32(pchar('/programs')), uint32(@entries[1].name[0]), 9); + entries[2].attribues := $10; + memcpy(uint32(pchar('/user')), uint32(@entries[2].name[0]), 5); + err := storage_write(volume^.device, start+1, 125, puint32(entries)); + end; + if err = eNone then begin + memcpy(uint32(pchar('ASURO ')), uint32(@info^.OEMName[0]), 8); + info^.version := 1; info^.sectorCount := sectors; info^.fileCount := 1000; + info^.signature := $0B00B1E5; + err := storage_write(volume^.device, start, 1, puint32(info)); + end; + kfree(void(entries)); kfree(void(info)); + create_volume := err; end; procedure write_directory(volume : PStorage_Volume; directory : pchar; status : PuInt32); @@ -976,9 +944,10 @@ begin filesystem.readOffsetCallback:= @readFileAtOffset; // filesystem.createDirCallback:= @writeDirectoryGen; filesystem.createcallback:= @create_volume; + filesystem.validateFormat := @validate_format; filesystem.detectcallback:= @detect_volumes; filesystem.identifyCallback:= @identify_volume; - filesystem.formatParamFlags := FS_FORMAT_PARAM_FILE_COUNT; + filesystem.formatParamFlags := 0; { fixed 1000-entry legacy format } // filesystem.writecallback:= @writeFile; // filesystem.readcallback := @readFile; diff --git a/src/driver/storage/fs/driver.storage.fs.iso9660.pas b/src/driver/storage/fs/driver.storage.fs.iso9660.pas index c3f7d767..50ca0aa6 100644 --- a/src/driver/storage/fs/driver.storage.fs.iso9660.pas +++ b/src/driver/storage/fs/driver.storage.fs.iso9660.pas @@ -392,6 +392,15 @@ end; { ==================== Filesystem callbacks ==================== } +procedure cacheVolumeLabel(volume : PStorage_Volume; pvd : PPVD); +var n, i : uint32; +begin + memset(uint32(@volume^.labelText[0]),0,sizeof(volume^.labelText)); + n := 32; + while (n > 0) and ((pvd^.volumeId[n-1] = ' ') or (pvd^.volumeId[n-1] = #0)) do Dec(n); + i := 0; while i < n do begin volume^.labelText[i] := pvd^.volumeId[i]; Inc(i); end; +end; + { Identify: check for 'CD001' at sector 16 } function identify_volume(volume : PStorage_Volume) : boolean; var @@ -405,6 +414,7 @@ begin buf := readSectors(volume^.device, volume^.sectorStart + PVD_SECTOR, 1); if buf = nil then exit; identify_volume := isPVD(buf); + if identify_volume then cacheVolumeLabel(volume,PPVD(buf)); kfree(buf); end; @@ -437,6 +447,7 @@ begin volume^.filesystem := @filesystem; volume^.isBootDrive := disk^.isBootDevice; + cacheVolumeLabel(volume,pvd); driver.storage.vol.mgr.register_volume(disk, volume); kfree(buf); diff --git a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.core.pas b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.core.pas index 177a40d7..01ccea54 100644 --- a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.core.pas +++ b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.core.pas @@ -94,10 +94,7 @@ end; function fatMaxClusterCount(bootRecord : PBootRecord) : uint32; begin - if (bootRecord = nil) or (bootRecord^.sectorSize = 0) then - fatMaxClusterCount := 0 - else - fatMaxClusterCount := (bootRecord^.FATSize * bootRecord^.sectorSize) div 4; + fatMaxClusterCount := FAT32ClusterLimit(bootRecord); end; function fatClustersForBytes(byteCount : uint32; bytesPerCluster : uint32) : uint32; @@ -391,7 +388,9 @@ begin sectorBuf := puint32(kalloc(bufSize)); if sectorBuf = nil then exit; memset(uint32(sectorBuf), 0, bufSize); - driver.storage.mgr.storage_read(volume^.device, volume^.sectorStart, 1, sectorBuf); + if driver.storage.mgr.storage_read(volume^.device, volume^.sectorStart, 1, sectorBuf) <> eNone then begin + kfree(sectorBuf); exit; + end; bootRecord := PBootRecord(sectorBuf)^; kfree(sectorBuf); readBootRecordRaw := true; @@ -699,9 +698,13 @@ begin { Keep runtime addressing compatible with the legacy formatter and metadata paths. } ctx^.Info.DataStart := volume^.sectorStart + ctx^.Info.BootRecord.rsvSectors + - ctx^.Info.BootRecord.FATSize; + uint32(ctx^.Info.BootRecord.numFats) * ctx^.Info.BootRecord.FATSize; ctx^.Info.BytesPerCluster := uint32(ctx^.Info.BootRecord.spc) * uint32(ctx^.Info.BootRecord.sectorSize); ctx^.Info.MaxCluster := fatMaxClusterCount(@ctx^.Info.BootRecord); + if (ctx^.Info.MaxCluster <= 2) or + (ctx^.Info.BootRecord.manySectors > volume^.sectorCount) then begin + fatCtxFree(ctx); exit; + end; ctx^.InfoValid := true; ctx^.Cache.Data := puint32(kalloc(FAT_CACHE_LINES * 512)); @@ -1135,6 +1138,7 @@ begin end; if dir^.fileName[0] = char($E5) then continue; + if (dir^.attributes and $08) <> 0 then continue; { volume labels and LFN entries are not files } if compareByteArray8(dir^.fileName, cleanName) and matchExtension(dir^.fileExtension, ext) then begin findEntryInSector := integer(idx); exit; @@ -2048,7 +2052,7 @@ begin end; if dir^.fileName[0] = char($E5) then continue; - if dir^.attributes = $0F then + if (dir^.attributes and $08) <> 0 then continue; if fatIsDotEntry(dir) then continue; diff --git a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.labels.pas b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.labels.pas new file mode 100644 index 00000000..dc3af03e --- /dev/null +++ b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.labels.pas @@ -0,0 +1,205 @@ +unit driver.storage.fs.fat32.labels; + +interface +uses driver.storage.types; +function normalize_label(text : pchar; var normalized : TVolumeLabel) : TError; +function read_label(volume : PStorage_Volume) : TError; +{ Caller owns the device metadata lock and has invalidated filesystem caches. } +function write_label(volume : PStorage_Volume; text : pchar) : TError; + +implementation +uses driver.storage.fs.fat32.types, driver.storage.mgr, memory.heap, core.util; + +function normalize_label(text : pchar; var normalized : TVolumeLabel) : TError; +var i, n, first : uint32; c : char; +begin + normalize_label := eInvalidArgument; + memset(uint32(@normalized[0]), 0, sizeof(normalized)); + if text = nil then exit; + n := 0; + while (n < 12) and (text[n] <> #0) do Inc(n); + if n > 11 then exit; + while (n > 0) and (text[n-1] = ' ') do Dec(n); + first := 0; while (first < n) and (text[first] = ' ') do Inc(first); + i := first; + while i < n do begin + c := text[i]; + if (ord(c) < 32) or (ord(c) > 126) then exit; + case c of + '"','*','+',',','.','/',':',';','<','=','>','?','[',']','\','|': exit; + end; + if (c >= 'a') and (c <= 'z') then c := char(ord(c)-32); + normalized[i-first] := c; Inc(i); + end; + normalize_label := eNone; +end; + +procedure unpack_label(src : pchar; var dest : TVolumeLabel); +var n, i : uint32; +begin + memset(uint32(@dest[0]), 0, sizeof(dest)); + n := 11; while (n > 0) and ((src[n-1] = ' ') or (src[n-1] = #0)) do Dec(n); + for i := 0 to 10 do if i < n then dest[i] := src[i]; +end; + +procedure pack_label(src : pchar; dest : pchar); +var i : uint32; +begin + for i := 0 to 10 do dest[i] := ' '; + i := 0; while (i < 11) and (src[i] <> #0) do begin dest[i] := src[i]; Inc(i); end; +end; + +function load_boot(volume : PStorage_Volume; buffer : puint32) : TError; +var br : PBootRecord; lim : uint32; +begin + load_boot := eInvalidArgument; + if (volume = nil) or (volume^.device = nil) then exit; + if (volume^.device^.sectorSize <> 512) or (volume^.sectorCount = 0) or + (volume^.sectorStart >= volume^.device^.maxSectorCount) or + (volume^.sectorCount > volume^.device^.maxSectorCount-volume^.sectorStart) then exit; + load_boot := storage_read(volume^.device,volume^.sectorStart,1,buffer); + if load_boot <> eNone then exit; + br := PBootRecord(buffer); lim := FAT32ClusterLimit(br); + if (lim <= 2) or (br^.manySectors = 0) or (br^.manySectors > volume^.sectorCount) or + (br^.rootCluster < 2) or (br^.rootCluster >= lim) or + (puint16(uint32(buffer)+510)^ <> $AA55) then load_boot := eCorruptFilesystem; +end; + +{ Bounded walk of the root FAT chain. A label is authoritative when present. + Also remembers the first reusable slot; never scans beyond the end marker. } +function locate_label(volume : PStorage_Volume; br : PBootRecord; scratch : puint32; + var sector, offset : uint32; var found : boolean) : TError; +var cluster, next, limit, steps, ds, i, lba, dataStart, fatStart : uint32; + freeSector, freeOffset : uint32; entry : PDirectory; +begin + locate_label := eNone; sector := 0; offset := 0; found := false; + freeSector := 0; freeOffset := 0; limit := FAT32ClusterLimit(br); + dataStart := volume^.sectorStart + br^.rsvSectors + br^.numFats*br^.FATSize; + fatStart := volume^.sectorStart + br^.rsvSectors; + if (br^.flags and $80) <> 0 then begin + i := br^.flags and $0F; + if i >= br^.numFats then begin locate_label := eCorruptFilesystem; exit; end; + fatStart := fatStart+i*br^.FATSize; + end; + cluster := br^.rootCluster; steps := 0; + while steps < limit-2 do begin + if (cluster < 2) or (cluster >= limit) then begin locate_label := eCorruptFilesystem; exit; end; + lba := dataStart+(cluster-2)*br^.spc; + for ds := 0 to br^.spc-1 do begin + locate_label := storage_read(volume^.device,lba+ds,1,scratch); + if locate_label <> eNone then exit; + for i := 0 to 15 do begin + entry := @PDirectory(scratch)[i]; + if (entry^.fileName[0] = #0) or (entry^.fileName[0] = char($E5)) then begin + if freeSector = 0 then begin freeSector := lba+ds; freeOffset := i*32; end; + if entry^.fileName[0] = #0 then begin sector := freeSector; offset := freeOffset; exit; end; + end else if (entry^.attributes and $3F) = $08 then begin + sector := lba+ds; offset := i*32; found := true; exit; + end; + end; + end; + locate_label := storage_read(volume^.device,fatStart+cluster div 128,1,scratch); + if locate_label <> eNone then exit; + next := scratch[cluster mod 128] and $0FFFFFFF; + if next >= $0FFFFFF8 then begin sector := freeSector; offset := freeOffset; exit; end; + cluster := next; Inc(steps); + end; + locate_label := eCorruptFilesystem; +end; + +function read_label(volume : PStorage_Volume) : TError; +var mem, boot, scratch : puint32; sector, offset, i : uint32; found, noName : boolean; + value : TVolumeLabel; +begin + mem := puint32(kalloc(1024)); + if mem = nil then begin read_label := eOutOfMemory; exit; end; + boot := mem; scratch := puint32(uint32(mem)+512); + read_label := load_boot(volume,boot); + if read_label = eNone then begin + read_label := locate_label(volume,PBootRecord(boot),scratch,sector,offset,found); + if read_label = eNone then begin + if found then unpack_label(pchar(uint32(scratch)+offset),value) + else begin + unpack_label(pchar(@PBootRecord(boot)^.volumeLabel[0]),value); + noName := true; + for i := 0 to 10 do + if pchar(@PBootRecord(boot)^.volumeLabel[0])[i] <> pchar('NO NAME ')[i] then noName := false; + if noName then value[0] := #0; + end; + volume^.labelText := value; + end; + end; + kfree(mem); +end; + +function write_label(volume : PStorage_Volume; text : pchar) : TError; +var mem, boot, scratch, rootOld, backupOld, bootOld : puint32; + br : PBootRecord; value : TVolumeLabel; sector, offset, backup : uint32; + found, bootWritten, backupWritten, rootWritten : boolean; entry : PDirectory; +begin + write_label := normalize_label(text,value); if write_label <> eNone then exit; + if (volume = nil) or (volume^.device = nil) then begin write_label := eInvalidArgument; exit; end; + if not volume^.device^.writable then begin write_label := eReadOnly; exit; end; + mem := puint32(kalloc(5*512)); + if mem = nil then begin write_label := eOutOfMemory; exit; end; + boot := mem; scratch := puint32(uint32(mem)+512); rootOld := puint32(uint32(mem)+1024); + backupOld := puint32(uint32(mem)+1536); bootOld := puint32(uint32(mem)+2048); + bootWritten := false; backupWritten := false; rootWritten := false; backup := 0; + repeat + write_label := load_boot(volume,boot); if write_label <> eNone then break; + br := PBootRecord(boot); + write_label := locate_label(volume,br,scratch,sector,offset,found); + if write_label <> eNone then break; + if (sector = 0) and (value[0] <> #0) then begin write_label := eDiskFull; break; end; + if sector <> 0 then begin + write_label := storage_read(volume^.device,sector,1,scratch); if write_label <> eNone then break; + memcpy(uint32(scratch),uint32(rootOld),512); + entry := PDirectory(uint32(scratch)+offset); + if value[0] = #0 then begin + if found then entry^.fileName[0] := char($E5); + end else begin + if entry^.fileName[0] = #0 then begin + { Retain an end marker after a newly inserted label. } + if offset = 480 then begin write_label := eDiskFull; break; end; + puint8(uint32(entry)+32)^ := 0; + end; + memset(uint32(entry),0,32); pack_label(@value[0],@entry^.fileName[0]); entry^.attributes := $08; + end; + end; + if (br^.backupCluster <> 0) and (br^.backupCluster <> $FFFF) then begin + if br^.backupCluster >= br^.rsvSectors then begin write_label := eCorruptFilesystem; break; end; + backup := volume^.sectorStart+br^.backupCluster; + write_label := storage_read(volume^.device,backup,1,backupOld); if write_label <> eNone then break; + end; + memcpy(uint32(boot),uint32(bootOld),512); + if value[0] = #0 then pack_label('NO NAME',pchar(@br^.volumeLabel[0])) + else pack_label(@value[0],pchar(@br^.volumeLabel[0])); + bootWritten := true; + write_label := storage_write(volume^.device,volume^.sectorStart,1,boot); + if write_label <> eNone then break; + bootWritten := true; + if backup <> 0 then begin + { Preserve backup-specific bytes; only update its label. } + memcpy(uint32(backupOld),uint32(boot),512); + if value[0] = #0 then pack_label('NO NAME',pchar(@br^.volumeLabel[0])) + else pack_label(@value[0],pchar(@br^.volumeLabel[0])); + backupWritten := true; + write_label := storage_write(volume^.device,backup,1,boot); + if write_label <> eNone then break; + backupWritten := true; + end; + if (sector <> 0) and (found or (value[0] <> #0)) then begin + rootWritten := true; + write_label := storage_write(volume^.device,sector,1,scratch); + end; + until true; + if write_label = eNone then volume^.labelText := value + else begin + { Best-effort rollback on I/O failure; preserve the original error. } + if rootWritten then storage_write(volume^.device,sector,1,rootOld); + if backupWritten then storage_write(volume^.device,backup,1,backupOld); + if bootWritten then storage_write(volume^.device,volume^.sectorStart,1,bootOld); + end; + kfree(mem); +end; +end. diff --git a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.pas b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.pas index dcd461b4..449cd930 100644 --- a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.pas +++ b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.pas @@ -16,6 +16,7 @@ uses io.syslog, core.util, driver.storage.fs.fat32.vol, + driver.storage.fs.fat32.labels, driver.storage.fs.fat32.core, driver.storage.fs.fat32.transfer; @@ -36,7 +37,8 @@ begin filesystem.readDirCallback := @FAT32ReadDirectory; filesystem.createDirCallback := @FAT32CreateDirectory; filesystem.createCallback := @create_volume; - filesystem.createAsyncCallback := @create_volume_async; + filesystem.validateFormat := @validate_format; + filesystem.createAsyncCallback := nil; { volume manager runs checked formatter in a worker } filesystem.detectCallback := @detect_volumes; filesystem.readOffsetCallback := @FAT32ReadFileAtOffset; filesystem.writeOffsetCallback := @FAT32WriteFileAtOffset; @@ -51,7 +53,9 @@ begin filesystem.closeFileCallback := @FAT32CloseFile; filesystem.createDirAsyncCallback := nil; filesystem.readDirAsyncCallback := nil; - filesystem.formatParamFlags := FS_FORMAT_PARAM_CLUSTER_SIZE; + filesystem.formatParamFlags := FS_FORMAT_PARAM_CLUSTER_SIZE or FS_FORMAT_PARAM_LABEL; + filesystem.validateLabelCallback := @normalize_label; + filesystem.setLabelCallback := @write_label; FAT32VolInit(@filesystem); driver.storage.fs.mgr.register_filesystem(@filesystem); diff --git a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.types.pas b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.types.pas index 38e2bd83..e4eef98c 100644 --- a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.types.pas +++ b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.types.pas @@ -224,6 +224,28 @@ type WriteRefs : array[0..FAT_OPEN_HANDLE_SLOTS - 1] of TFATWriteRef; end; +{ Exclusive cluster bound, constrained by both data area and FAT capacity. } +function FAT32ClusterLimit(br : PBootRecord) : uint32; + implementation +function FAT32ClusterLimit(br : PBootRecord) : uint32; +var overhead, total, capacity, limit : uint64; +begin + FAT32ClusterLimit := 0; + if br = nil then exit; + if (br^.sectorSize <> 512) or (br^.spc = 0) or (br^.spc > 128) or + ((br^.spc and (br^.spc - 1)) <> 0) or (br^.rsvSectors = 0) or + (br^.numFats = 0) or (br^.FATSize = 0) then exit; + total := br^.manySectors; + if total = 0 then total := br^.numSectors; + overhead := uint64(br^.rsvSectors) + uint64(br^.numFats) * br^.FATSize; + if overhead >= total then exit; + limit := (total - overhead) div br^.spc + 2; + capacity := uint64(br^.FATSize) * (br^.sectorSize div 4); + if (limit > capacity) or (limit > $0FFFFFF0) then exit; + FAT32ClusterLimit := uint32(limit); +end; + + end. diff --git a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.vol.pas b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.vol.pas index dd6d7269..4c1021df 100644 --- a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.vol.pas +++ b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.vol.pas @@ -1,335 +1,88 @@ unit driver.storage.fs.fat32.vol; -{ - Volume lifecycle functions for the FAT32 filesystem driver. - Handles volume detection, identification, and formatting. -} - interface - -uses - driver.storage.types, - driver.storage.fs.fat32.types; - +uses driver.storage.types, driver.storage.fs.fat32.types; procedure FAT32VolInit(fs : PFilesystem); procedure detect_volumes(disk : PStorage_Device); -function identify_volume(volume : PStorage_Volume) : boolean; -procedure create_volume(volume : PStorage_Volume; sectors : uint32; start : uint32; config : PFSFormatParams); -procedure create_volume_async(volume : PStorage_Volume; sectors : uint32; start : uint32; - config : PFSFormatParams; callback : TIOCallback; callbackData : pointer); +function identify_volume(volume : PStorage_Volume) : boolean; +function validate_format(volume : PStorage_Volume; sectors, start : uint32; config : PFSFormatParams) : TError; +function create_volume(volume : PStorage_Volume; sectors, start : uint32; config : PFSFormatParams) : TError; implementation +uses io.syslog, debug.tracer, memory.heap, core.util, arch.x86.util, + driver.storage.mgr, driver.storage.vol.mgr, driver.storage.fs.fat32.labels; +var fat32FilesystemPtr : PFilesystem = nil; -uses - io.syslog, - debug.tracer, - memory.heap, - core.util, arch.x86.util, - driver.storage.mgr, - driver.storage.vol.mgr, - driver.storage.fs.fat32.core; - -{ ===================== Types ===================== } - -type - TFmtStep = (fmtBootSector, fmtZeroFAT, fmtFATEntries, fmtRootDir, fmtSystemDir, fmtDone); - - PFmtContext = ^TFmtContext; - TFmtContext = record - Volume : PStorage_Volume; - Disk : PStorage_Device; - SectorStart : uint32; - RootCluster : uint32; - Callback : TIOCallback; - CallbackData : pointer; - Buffer : puint32; - ZeroBuffer : puint32; - SPC : uint32; - FATSize : uint32; - FATStart : uint32; - DataStart : uint32; - BatchSize : uint32; - BatchPos : uint32; - Step : TFmtStep; - end; - -{ ===================== Module state ===================== } - -var - fat32FilesystemPtr : PFilesystem = nil; - -{ ===================== Private helpers ===================== } +procedure FAT32VolInit(fs : PFilesystem); +begin fat32FilesystemPtr := fs; end; function bootSectorHasSignature(buffer : puint32) : boolean; begin - bootSectorHasSignature := (puint8(uint32(buffer) + 510)^ = $55) and - (puint8(uint32(buffer) + 511)^ = $AA); + bootSectorHasSignature := (puint8(uint32(buffer)+510)^ = $55) and + (puint8(uint32(buffer)+511)^ = $AA); end; -function clusterToLBA(dataStart : uint32; spc : uint32; cluster : uint32) : uint32; +function formatSPC(config : PFSFormatParams) : uint32; begin - if cluster < 2 then - clusterToLBA := dataStart - else - clusterToLBA := dataStart + ((cluster - 2) * spc); + formatSPC := 8; + if (config <> nil) and ((config^.Flags and FS_FORMAT_PARAM_CLUSTER_SIZE) <> 0) then + formatSPC := config^.ClusterSize; end; -function fatDefaultSPC(sectors : uint32; sectorSize : uint32) : uint32; +function formatFATSize(sectors, spc : uint32) : uint32; begin - if sectorSize = 0 then begin - fatDefaultSPC := 1; - exit; + { Conservative size includes reserved FAT entries; uint64 prevents wrap. } + formatFATSize := uint32(((uint64(sectors - 32) div spc + 2) * 4 + 511) div 512); +end; + +function validate_format(volume : PStorage_Volume; sectors, start : uint32; config : PFSFormatParams) : TError; +var spc, fat : uint32; dataClusters : uint64; labelValue : TVolumeLabel; +begin + validate_format := eInvalidArgument; + if (volume = nil) or (volume^.device = nil) then exit; + if not volume^.device^.writable then begin validate_format := eReadOnly; exit; end; + { Current FAT cache addresses 512-byte sectors. Reject unsupported geometry. } + if volume^.device^.sectorSize <> 512 then exit; + if (sectors <> volume^.sectorCount) or (start <> volume^.sectorStart) or + (start >= volume^.device^.maxSectorCount) or + (sectors > volume^.device^.maxSectorCount - start) or (sectors <= 32) then exit; + if config <> nil then begin + if (config^.Version <> FS_FORMAT_PARAMS_VERSION_1) or + ((config^.Flags and (not (FS_FORMAT_PARAM_CLUSTER_SIZE or FS_FORMAT_PARAM_LABEL))) <> 0) then exit; end; - fatDefaultSPC := 4096 div sectorSize; - if fatDefaultSPC = 0 then - fatDefaultSPC := 1; - if fatDefaultSPC > 64 then - fatDefaultSPC := 64; + if (config <> nil) and ((config^.Flags and FS_FORMAT_PARAM_LABEL) <> 0) then + if normalize_label(@config^.LabelText[0],labelValue) <> eNone then exit; + spc := formatSPC(config); + if (spc = 0) or (spc > 128) or ((spc and (spc-1)) <> 0) then exit; + fat := formatFATSize(sectors, spc); + if uint64(32) + fat + uint64(spc)*2 > sectors then exit; + dataClusters := (uint64(sectors) - 32 - fat) div spc; + if dataClusters + 2 > $0FFFFFF0 then exit; + validate_format := eNone; end; -function fatComputeFATSize(sectors : uint32; spc : uint32; sectorSize : uint32) : uint32; -var - clusterCount : uint32; - fatBytes : uint32; +function countFreeFATClusters(volume : PStorage_Volume; br : PBootRecord) : uint32; +var limit, sectorIdx, entryIdx, clusterNum, count : uint32; buffer : puint32; begin - if spc = 0 then spc := 1; - if sectorSize = 0 then begin - fatComputeFATSize := 0; - exit; + countFreeFATClusters := $FFFFFFFF; + limit := FAT32ClusterLimit(br); + if limit <= 2 then exit; + buffer := puint32(kalloc(512)); + if buffer = nil then exit; + count := 0; sectorIdx := 0; + while sectorIdx * 128 < limit do begin + if storage_read(volume^.device, volume^.sectorStart + br^.rsvSectors + sectorIdx, 1, buffer) <> eNone then begin + kfree(buffer); exit; + end; + for entryIdx := 0 to 127 do begin + clusterNum := sectorIdx * 128 + entryIdx; + if clusterNum >= limit then break; + if (clusterNum >= 2) and ((buffer[entryIdx] and $0FFFFFFF) = 0) then Inc(count); + end; + Inc(sectorIdx); end; - clusterCount := (sectors + spc - 1) div spc; - fatBytes := clusterCount * 4; - fatComputeFATSize := (fatBytes + sectorSize - 1) div sectorSize; - if fatComputeFATSize = 0 then - fatComputeFATSize := 1; -end; - -function fatFormatConfigSPC(config : PFSFormatParams; sectors : uint32; sectorSize : uint32) : uint32; -begin - fatFormatConfigSPC := 0; - if (config <> nil) and - (config^.Version = FS_FORMAT_PARAMS_VERSION_1) and - ((config^.Flags and FS_FORMAT_PARAM_CLUSTER_SIZE) <> 0) then - fatFormatConfigSPC := config^.ClusterSize; - if fatFormatConfigSPC = 0 then - fatFormatConfigSPC := fatDefaultSPC(sectors, sectorSize); - if fatFormatConfigSPC = 0 then - fatFormatConfigSPC := 1; -end; - -{ Counts free FAT clusters by scanning the FAT directly (no cache). - Used during detect/identify to compute freeSectors. } -function countFreeFATClusters(volume : PStorage_Volume; bootRecord : PBootRecord) : uint32; -var - fatStart : uint32; - maxCluster : uint32; - entriesPerSect : uint32; - sectorIdx : uint32; - entryIdx : uint32; - clusterNum : uint32; - freeCount : uint32; - fatSectors : uint32; - fatBuffer : puint32; -begin - countFreeFATClusters := 0; - if (bootRecord^.sectorSize = 0) or (bootRecord^.FATSize = 0) then exit; - - fatStart := volume^.sectorStart + bootRecord^.rsvSectors; - maxCluster := (bootRecord^.FATSize * bootRecord^.sectorSize) div 4; - entriesPerSect := bootRecord^.sectorSize div 4; - fatSectors := bootRecord^.FATSize; - - fatBuffer := puint32(kalloc(bootRecord^.sectorSize)); - if fatBuffer = nil then exit; - freeCount := 0; - - if fatSectors > 0 then - for sectorIdx := 0 to fatSectors - 1 do begin - if sectorIdx * entriesPerSect >= maxCluster then break; - driver.storage.mgr.storage_read(volume^.device, fatStart + sectorIdx, 1, fatBuffer); - if entriesPerSect > 0 then - for entryIdx := 0 to entriesPerSect - 1 do begin - clusterNum := sectorIdx * entriesPerSect + entryIdx; - if clusterNum < 2 then continue; - if clusterNum >= maxCluster then break; - if fatBuffer[entryIdx] = 0 then - freeCount := freeCount + 1; - end; - end; - - kfree(fatBuffer); - countFreeFATClusters := freeCount; -end; - -{ ===================== Async format state machine ===================== } - -procedure fmt_run_next(ctx : PFmtContext); forward; - -procedure fmt_step_complete(error : TError; userdata : pointer); -var - ctx : PFmtContext; -begin - ctx := PFmtContext(userdata); - if error <> eNone then begin - io.syslog.logln('FAT32', 'fmt_step_complete: I/O error — aborting format'); - if ctx^.Buffer <> nil then kfree(ctx^.Buffer); - if ctx^.ZeroBuffer <> nil then kfree(ctx^.ZeroBuffer); - if ctx^.Callback <> nil then - ctx^.Callback(error, ctx^.CallbackData); - kfree(puint32(ctx)); - exit; - end; - - case ctx^.Step of - fmtBootSector: begin - ctx^.Step := fmtZeroFAT; - ctx^.BatchPos := 0; - end; - fmtZeroFAT: begin - ctx^.BatchPos := ctx^.BatchPos + ctx^.BatchSize; - end; - fmtFATEntries: begin - ctx^.Step := fmtRootDir; - end; - fmtRootDir: begin - ctx^.Step := fmtSystemDir; - end; - fmtSystemDir: begin - ctx^.Step := fmtDone; - end; - end; - fmt_run_next(ctx); -end; - -procedure fmt_run_next(ctx : PFmtContext); -var - writeCount : uint32; -begin - case ctx^.Step of - fmtBootSector: begin - io.syslog.logln('FAT32', 'fmt: fmtBootSector'); - driver.storage.mgr.storage_write_async(ctx^.Disk, ctx^.SectorStart, 1, - ctx^.Buffer, @fmt_step_complete, pointer(ctx)); - end; - - fmtZeroFAT: begin - { Transition to next step once the FAT is fully zeroed } - if ctx^.BatchPos >= ctx^.FATSize then begin - io.syslog.logln('FAT32', 'fmt: FAT zero complete, moving to fmtFATEntries'); - kfree(ctx^.ZeroBuffer); - ctx^.ZeroBuffer := nil; - kfree(ctx^.Buffer); - ctx^.Buffer := puint32(kalloc(ctx^.Disk^.sectorSize)); - memset(uint32(ctx^.Buffer), 0, ctx^.Disk^.sectorSize); - { FAT entries: [0]=media end-of-partition, [1]=EOC, [2]=root cluster EOC, [3]=SYSTEM EOC } - puint32(ctx^.Buffer)[0] := $0FFFFFF8; - puint32(ctx^.Buffer)[1] := $0FFFFFFF; - puint32(ctx^.Buffer)[2] := $0FFFFFF8; - puint32(ctx^.Buffer)[3] := $0FFFFFF8; - ctx^.Step := fmtFATEntries; - fmt_run_next(ctx); - exit; - end; - if (ctx^.FATSize - ctx^.BatchPos) >= ctx^.BatchSize then - writeCount := ctx^.BatchSize - else - writeCount := ctx^.FATSize - ctx^.BatchPos; - driver.storage.mgr.storage_write_async(ctx^.Disk, - ctx^.FATStart + ctx^.BatchPos, writeCount, ctx^.ZeroBuffer, - @fmt_step_complete, pointer(ctx)); - end; - - fmtFATEntries: begin - io.syslog.logln('FAT32', 'fmt: fmtFATEntries'); - driver.storage.mgr.storage_write_async(ctx^.Disk, ctx^.FATStart, 1, - ctx^.Buffer, @fmt_step_complete, pointer(ctx)); - end; - - fmtRootDir: begin - io.syslog.logln('FAT32', 'fmt: fmtRootDir'); - memset(uint32(ctx^.Buffer), 0, ctx^.Disk^.sectorSize); - { '.' entry — volume label } - PDirectory(ctx^.Buffer)[0].fileName[0] := '.'; - PDirectory(ctx^.Buffer)[0].fileName[1] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[2] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[3] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[4] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[5] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[6] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[7] := ' '; - PDirectory(ctx^.Buffer)[0].attributes := $08; - PDirectory(ctx^.Buffer)[0].clusterLow := ctx^.RootCluster; - { '..' entry } - PDirectory(ctx^.Buffer)[1].fileName[0] := '.'; - PDirectory(ctx^.Buffer)[1].fileName[1] := '.'; - PDirectory(ctx^.Buffer)[1].fileName[2] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[3] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[4] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[5] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[6] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[7] := ' '; - PDirectory(ctx^.Buffer)[1].attributes := $10; - PDirectory(ctx^.Buffer)[1].clusterLow := ctx^.RootCluster; - { Pre-allocated SYSTEM dir entry (cluster 3) } - PDirectory(ctx^.Buffer)[2].fileName[0] := 'S'; - PDirectory(ctx^.Buffer)[2].fileName[1] := 'Y'; - PDirectory(ctx^.Buffer)[2].fileName[2] := 'S'; - PDirectory(ctx^.Buffer)[2].fileName[3] := 'T'; - PDirectory(ctx^.Buffer)[2].fileName[4] := 'E'; - PDirectory(ctx^.Buffer)[2].fileName[5] := 'M'; - PDirectory(ctx^.Buffer)[2].fileName[6] := ' '; - PDirectory(ctx^.Buffer)[2].fileName[7] := ' '; - PDirectory(ctx^.Buffer)[2].attributes := $10; - PDirectory(ctx^.Buffer)[2].clusterLow := 3; - driver.storage.mgr.storage_write_async(ctx^.Disk, - clusterToLBA(ctx^.DataStart, ctx^.SPC, ctx^.RootCluster), 1, ctx^.Buffer, - @fmt_step_complete, pointer(ctx)); - end; - - fmtSystemDir: begin - io.syslog.logln('FAT32', 'fmt: fmtSystemDir'); - memset(uint32(ctx^.Buffer), 0, ctx^.Disk^.sectorSize); - PDirectory(ctx^.Buffer)[0].fileName[0] := '.'; - PDirectory(ctx^.Buffer)[0].fileName[1] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[2] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[3] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[4] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[5] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[6] := ' '; - PDirectory(ctx^.Buffer)[0].fileName[7] := ' '; - PDirectory(ctx^.Buffer)[0].attributes := $10; - PDirectory(ctx^.Buffer)[0].clusterLow := 3; - PDirectory(ctx^.Buffer)[1].fileName[0] := '.'; - PDirectory(ctx^.Buffer)[1].fileName[1] := '.'; - PDirectory(ctx^.Buffer)[1].fileName[2] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[3] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[4] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[5] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[6] := ' '; - PDirectory(ctx^.Buffer)[1].fileName[7] := ' '; - PDirectory(ctx^.Buffer)[1].attributes := $10; - PDirectory(ctx^.Buffer)[1].clusterLow := ctx^.RootCluster; - driver.storage.mgr.storage_write_async(ctx^.Disk, - clusterToLBA(ctx^.DataStart, ctx^.SPC, 3), 1, ctx^.Buffer, - @fmt_step_complete, pointer(ctx)); - end; - - fmtDone: begin - io.syslog.logln('FAT32', 'fmt: fmtDone — format complete'); - kfree(ctx^.Buffer); - ctx^.Buffer := nil; - if ctx^.Callback <> nil then - ctx^.Callback(eNone, ctx^.CallbackData); - kfree(puint32(ctx)); - end; - end; -end; - -{ ===================== Exported procedures ===================== } - -procedure FAT32VolInit(fs : PFilesystem); -begin - fat32FilesystemPtr := fs; + kfree(buffer); + countFreeFATClusters := count; end; procedure detect_volumes(disk : PStorage_Device); @@ -338,6 +91,7 @@ var bufSize : uint32; volume : PStorage_Volume; br : PBootRecord; + freeClusters : uint32; begin push_trace('driver.storage.fs.fat32.detect_volumes'); if fat32FilesystemPtr = nil then exit; @@ -354,10 +108,11 @@ begin exit; end; - driver.storage.mgr.storage_read(disk, 0, 1, buffer); + if storage_read(disk, 0, 1, buffer) <> eNone then begin kfree(buffer); exit; end; br := PBootRecord(buffer); - if bootSectorHasSignature(buffer) and (br^.bsignature = $29) then begin + if bootSectorHasSignature(buffer) and (br^.bsignature = $29) and + (FAT32ClusterLimit(br) > 2) and (br^.manySectors <= disk^.maxSectorCount) then begin io.syslog.logln('FAT32', 'detect_volumes: volume found'); volume := PStorage_Volume(kalloc(sizeof(TStorage_Volume))); if volume = nil then begin @@ -370,8 +125,11 @@ begin volume^.sectorSize := br^.sectorSize; volume^.sectorCount := disk^.maxSectorCount; volume^.filesystem := fat32FilesystemPtr; - volume^.freeSectors := countFreeFATClusters(volume, br) * br^.spc; + freeClusters := countFreeFATClusters(volume, br); + if freeClusters = $FFFFFFFF then begin kfree(void(volume)); kfree(buffer); exit; end; + volume^.freeSectors := freeClusters * br^.spc; volume^.isBootDrive := false; + if read_label(volume) <> eNone then begin kfree(void(volume)); kfree(buffer); exit; end; driver.storage.vol.mgr.register_volume(disk, volume); end; @@ -384,6 +142,7 @@ var br : PBootRecord; bufSize : uint32; fatNameOk : boolean; + freeClusters : uint32; spc : uint8; i : uint32; begin @@ -399,7 +158,7 @@ begin if buffer = nil then exit; memset(uint32(buffer), 0, bufSize); - driver.storage.mgr.storage_read(volume^.device, volume^.sectorStart, 1, buffer); + if storage_read(volume^.device, volume^.sectorStart, 1, buffer) <> eNone then begin kfree(buffer); exit; end; br := PBootRecord(buffer); fatNameOk := true; @@ -421,240 +180,97 @@ begin (br^.rsvSectors > 0) and (br^.FATSize > 0) and (br^.numFats > 0) and - (br^.rootCluster >= 2) then begin + (br^.rootCluster >= 2) and (br^.rootCluster < FAT32ClusterLimit(br)) and + (br^.manySectors <= volume^.sectorCount) then begin io.syslog.logln('FAT32', 'identify_volume: FAT32 signature matched'); - identify_volume := true; - volume^.freeSectors := countFreeFATClusters(volume, br) * br^.spc; + freeClusters := countFreeFATClusters(volume, br); + if freeClusters <> $FFFFFFFF then begin + identify_volume := read_label(volume) = eNone; + volume^.freeSectors := freeClusters * br^.spc; + end; end else io.syslog.logln('FAT32', 'identify_volume: not a FAT32 volume'); kfree(buffer); end; -procedure create_volume(volume : PStorage_Volume; sectors : uint32; start : uint32; config : PFSFormatParams); +{ Every write is checked; a valid boot signature is published last. } +function create_volume(volume : PStorage_Volume; sectors, start : uint32; config : PFSFormatParams) : TError; var - buffer : puint32; - zeroBuffer : puint32; - bootRecord : PBootRecord; - dataStart : uint32; - fatStart : uint32; - FATSize : uint32; - batchSize : uint32; - batchPos : uint32; - rootCluster: uint32; - spc : uint32; - disk : PStorage_Device; - status : uint32; + boot, buffer : puint32; + br : PBootRecord; + dir : PDirectory; + spc, fat, fatStart, dataStart, pos, batch, i : uint32; + err : TError; + labelValue : TVolumeLabel; + function writeSectors(lba, count : uint32; data : puint32) : boolean; + begin + if (lba < start) or (lba - start >= sectors) or (count > sectors - (lba - start)) then + err := eInvalidArgument + else err := storage_write(volume^.device, lba, count, data); + writeSectors := err = eNone; + end; begin - push_trace('driver.storage.fs.fat32.create_volume'); - io.syslog.logln('FAT32', 'create_volume (sync): enter'); - - disk := volume^.device; - rootCluster := 2; - spc := fatFormatConfigSPC(config, sectors, disk^.sectorSize); - - buffer := puint32(kalloc(disk^.sectorSize + 512)); - if buffer = nil then exit; - memset(uint32(buffer), 0, disk^.sectorSize); - - bootRecord := PBootRecord(buffer); - FATSize := fatComputeFATSize(sectors, spc, disk^.sectorSize); - - bootRecord^.jmp2boot := $0; - bootRecord^.OEMName[0] := 'A'; - bootRecord^.OEMName[1] := 'S'; - bootRecord^.OEMName[2] := 'U'; - bootRecord^.OEMName[3] := 'R'; - bootRecord^.OEMName[4] := 'O'; - bootRecord^.OEMName[5] := ' '; - bootRecord^.OEMName[6] := 'V'; - bootRecord^.OEMName[7] := '1'; - bootRecord^.sectorSize := disk^.sectorSize; - bootRecord^.spc := uint8(spc); - bootRecord^.rsvSectors := 32; - bootRecord^.numFats := 1; - bootRecord^.mediaDescp := $F8; - bootRecord^.hiddenSectors := start; - bootRecord^.manySectors := sectors; - bootRecord^.FATSize := FATSize; - bootRecord^.rootCluster := rootCluster; - bootRecord^.FSInfoCluster := 0; - bootRecord^.driveNumber := $80; - bootRecord^.volumeID := 62; - bootRecord^.bsignature := $29; - bootRecord^.identString[0] := 'F'; - bootRecord^.identString[1] := 'A'; - bootRecord^.identString[2] := 'T'; - bootRecord^.identString[3] := '3'; - bootRecord^.identString[4] := '2'; - bootRecord^.identString[5] := ' '; - bootRecord^.identString[6] := ' '; - bootRecord^.identString[7] := ' '; - - puint8(uint32(buffer) + 510)^ := $55; - puint8(uint32(buffer) + 511)^ := $AA; - - driver.storage.mgr.storage_write(disk, start, 1, buffer); - io.syslog.logln('FAT32', 'create_volume (sync): boot sector written'); - - fatStart := start + 32; - dataStart := fatStart + FATSize; - - if FATSize > 128 then - batchSize := 128 - else - batchSize := FATSize; - - zeroBuffer := puint32(kalloc(disk^.sectorSize * batchSize)); - if zeroBuffer = nil then begin - kfree(buffer); - exit; + create_volume := validate_format(volume, sectors, start, config); + if create_volume <> eNone then exit; + memset(uint32(@labelValue[0]),0,sizeof(labelValue)); + if (config <> nil) and ((config^.Flags and FS_FORMAT_PARAM_LABEL) <> 0) then + normalize_label(@config^.LabelText[0],labelValue); + spc := formatSPC(config); fat := formatFATSize(sectors, spc); + fatStart := start + 32; dataStart := fatStart + fat; + boot := puint32(kalloc(512)); buffer := puint32(kalloc(128*512)); + if (boot = nil) or (buffer = nil) then begin + if boot <> nil then kfree(boot); + if buffer <> nil then kfree(buffer); + create_volume := eOutOfMemory; exit; end; - memset(uint32(zeroBuffer), 0, disk^.sectorSize * batchSize); - - batchPos := 0; - while batchPos < FATSize do begin - if (FATSize - batchPos) >= batchSize then - driver.storage.mgr.storage_write(disk, fatStart + batchPos, batchSize, zeroBuffer) - else - driver.storage.mgr.storage_write(disk, fatStart + batchPos, FATSize - batchPos, zeroBuffer); - batchPos := batchPos + batchSize; + memset(uint32(boot), 0, 512); memset(uint32(buffer), 0, 128*512); + br := PBootRecord(boot); + br^.sectorSize := 512; br^.spc := uint8(spc); br^.rsvSectors := 32; + br^.numFats := 1; br^.mediaDescp := $F8; br^.hiddenSectors := start; + br^.manySectors := sectors; br^.FATSize := fat; br^.rootCluster := 2; + br^.driveNumber := $80; br^.volumeID := 62; br^.bsignature := $29; + memcpy(uint32(pchar('ASURO ')), uint32(@br^.OEMName[0]), 8); + memcpy(uint32(pchar('FAT32 ')), uint32(@br^.identString[0]), 8); + memcpy(uint32(pchar('NO NAME ')), uint32(@br^.volumeLabel[0]), 11); + if labelValue[0] <> #0 then begin + memset(uint32(@br^.volumeLabel[0]),ord(' '),11); + i := 0; while (i < 11) and (labelValue[i] <> #0) do begin br^.volumeLabel[i] := ord(labelValue[i]); Inc(i); end; end; - - kfree(zeroBuffer); - io.syslog.logln('FAT32', 'create_volume (sync): FAT zeroed'); - - memset(uint32(buffer), 0, disk^.sectorSize); - puint32(buffer)[0] := $0FFFFFF8; { media descriptor / reserved } - puint32(buffer)[1] := $0FFFFFFF; { end-of-chain marker } - puint32(buffer)[2] := $0FFFFFF8; { root cluster 2 — end-of-chain } - - driver.storage.mgr.storage_write(disk, fatStart, 1, buffer); - io.syslog.logln('FAT32', 'create_volume (sync): FAT entries written'); - - { Write root dir: '.' and '..' entries } - memset(uint32(buffer), 0, disk^.sectorSize); - PDirectory(buffer)[0].fileName[0] := '.'; - PDirectory(buffer)[0].fileName[1] := ' '; - PDirectory(buffer)[0].fileName[2] := ' '; - PDirectory(buffer)[0].fileName[3] := ' '; - PDirectory(buffer)[0].fileName[4] := ' '; - PDirectory(buffer)[0].fileName[5] := ' '; - PDirectory(buffer)[0].fileName[6] := ' '; - PDirectory(buffer)[0].fileName[7] := ' '; - PDirectory(buffer)[0].attributes := $08; - PDirectory(buffer)[0].clusterLow := uint16(rootCluster); - PDirectory(buffer)[1].fileName[0] := '.'; - PDirectory(buffer)[1].fileName[1] := '.'; - PDirectory(buffer)[1].fileName[2] := ' '; - PDirectory(buffer)[1].fileName[3] := ' '; - PDirectory(buffer)[1].fileName[4] := ' '; - PDirectory(buffer)[1].fileName[5] := ' '; - PDirectory(buffer)[1].fileName[6] := ' '; - PDirectory(buffer)[1].fileName[7] := ' '; - PDirectory(buffer)[1].attributes := $10; - PDirectory(buffer)[1].clusterLow := uint16(rootCluster); - driver.storage.mgr.storage_write(disk, clusterToLBA(dataStart, spc, rootCluster), 1, buffer); - io.syslog.logln('FAT32', 'create_volume (sync): root dir written'); - - kfree(buffer); - - { Create the SYSTEM directory via the normal FAT32 path } - status := 0; - FAT32CreateDirectory(volume, '', 'SYSTEM', $10, @status); - io.syslog.logln('FAT32', 'create_volume (sync): done'); -end; - -procedure create_volume_async(volume : PStorage_Volume; sectors : uint32; start : uint32; - config : PFSFormatParams; callback : TIOCallback; callbackData : pointer); -var - ctx : PFmtContext; - spc : uint32; -begin - push_trace('driver.storage.fs.fat32.create_volume_async'); - io.syslog.logln('FAT32', 'create_volume_async: enter'); - - ctx := PFmtContext(kalloc(sizeof(TFmtContext))); - if ctx = nil then begin - io.syslog.logln('FAT32', 'create_volume_async: OOM allocating context'); - if callback <> nil then callback(eOutOfMemory, callbackData); - exit; - end; - memset(uint32(ctx), 0, sizeof(TFmtContext)); - - ctx^.Volume := volume; - ctx^.Disk := volume^.device; - ctx^.SectorStart := start; - ctx^.RootCluster := 2; - ctx^.Callback := callback; - ctx^.CallbackData := callbackData; - - spc := fatFormatConfigSPC(config, sectors, ctx^.Disk^.sectorSize); - ctx^.SPC := spc; - ctx^.FATSize := fatComputeFATSize(sectors, spc, ctx^.Disk^.sectorSize); - ctx^.FATStart := start + 32; - ctx^.DataStart := ctx^.FATStart + ctx^.FATSize; - - ctx^.Buffer := puint32(kalloc(ctx^.Disk^.sectorSize + 512)); - if ctx^.Buffer = nil then begin - if callback <> nil then callback(eOutOfMemory, callbackData); - kfree(puint32(ctx)); - exit; - end; - memset(uint32(ctx^.Buffer), 0, ctx^.Disk^.sectorSize); - - { Write boot sector fields } - PBootRecord(ctx^.Buffer)^.jmp2boot := $0; - PBootRecord(ctx^.Buffer)^.OEMName[0] := 'A'; - PBootRecord(ctx^.Buffer)^.OEMName[1] := 'S'; - PBootRecord(ctx^.Buffer)^.OEMName[2] := 'U'; - PBootRecord(ctx^.Buffer)^.OEMName[3] := 'R'; - PBootRecord(ctx^.Buffer)^.OEMName[4] := 'O'; - PBootRecord(ctx^.Buffer)^.OEMName[5] := ' '; - PBootRecord(ctx^.Buffer)^.OEMName[6] := 'V'; - PBootRecord(ctx^.Buffer)^.OEMName[7] := '1'; - PBootRecord(ctx^.Buffer)^.sectorSize := ctx^.Disk^.sectorSize; - PBootRecord(ctx^.Buffer)^.spc := uint8(spc); - PBootRecord(ctx^.Buffer)^.rsvSectors := 32; - PBootRecord(ctx^.Buffer)^.numFats := 1; - PBootRecord(ctx^.Buffer)^.mediaDescp := $F8; - PBootRecord(ctx^.Buffer)^.hiddenSectors := start; - PBootRecord(ctx^.Buffer)^.manySectors := sectors; - PBootRecord(ctx^.Buffer)^.FATSize := ctx^.FATSize; - PBootRecord(ctx^.Buffer)^.rootCluster := ctx^.RootCluster; - PBootRecord(ctx^.Buffer)^.FSInfoCluster := 0; - PBootRecord(ctx^.Buffer)^.driveNumber := $80; - PBootRecord(ctx^.Buffer)^.volumeID := 62; - PBootRecord(ctx^.Buffer)^.bsignature := $29; - PBootRecord(ctx^.Buffer)^.identString[0] := 'F'; - PBootRecord(ctx^.Buffer)^.identString[1] := 'A'; - PBootRecord(ctx^.Buffer)^.identString[2] := 'T'; - PBootRecord(ctx^.Buffer)^.identString[3] := '3'; - PBootRecord(ctx^.Buffer)^.identString[4] := '2'; - PBootRecord(ctx^.Buffer)^.identString[5] := ' '; - PBootRecord(ctx^.Buffer)^.identString[6] := ' '; - PBootRecord(ctx^.Buffer)^.identString[7] := ' '; - puint8(uint32(ctx^.Buffer) + 510)^ := $55; - puint8(uint32(ctx^.Buffer) + 511)^ := $AA; - - if ctx^.FATSize > 128 then - ctx^.BatchSize := 128 - else - ctx^.BatchSize := ctx^.FATSize; - ctx^.BatchPos := 0; - - ctx^.ZeroBuffer := puint32(kalloc(ctx^.Disk^.sectorSize * ctx^.BatchSize)); - if ctx^.ZeroBuffer = nil then begin - kfree(ctx^.Buffer); - if callback <> nil then callback(eOutOfMemory, callbackData); - kfree(puint32(ctx)); - exit; - end; - memset(uint32(ctx^.ZeroBuffer), 0, ctx^.Disk^.sectorSize * ctx^.BatchSize); - - io.syslog.logln('FAT32', 'create_volume_async: kicking off fmtBootSector'); - ctx^.Step := fmtBootSector; - fmt_run_next(ctx); + puint8(uint32(boot)+510)^ := $55; puint8(uint32(boot)+511)^ := $AA; + err := eNone; + repeat + { Invalidate the old boot signature before modifying allocation metadata. } + if not writeSectors(start, 32, buffer) then break; + pos := 0; + while (pos < fat) and (err = eNone) do begin + batch := fat-pos; if batch > 128 then batch := 128; + writeSectors(fatStart+pos, batch, buffer); pos := pos+batch; + end; + if err <> eNone then break; + buffer[0] := $0FFFFFF8; buffer[1] := $0FFFFFFF; + buffer[2] := $0FFFFFFF; buffer[3] := $0FFFFFFF; + if not writeSectors(fatStart, 1, buffer) then break; + memset(uint32(buffer), 0, 128*512); + dir := PDirectory(buffer); + if labelValue[0] <> #0 then begin + memcpy(uint32(@br^.volumeLabel[0]),uint32(@dir^.fileName[0]),11); + dir^.attributes := $08; dir := @PDirectory(buffer)[1]; + end; + memcpy(uint32(pchar('SYSTEM ')), uint32(@dir^.fileName[0]), 11); + dir^.attributes := $10; dir^.clusterLow := 3; + if not writeSectors(dataStart, spc, buffer) then break; + memset(uint32(buffer), 0, 128*512); + dir := PDirectory(buffer); + memcpy(uint32(pchar('. ')), uint32(@dir[0].fileName[0]), 11); + dir[0].attributes := $10; dir[0].clusterLow := 3; + memcpy(uint32(pchar('.. ')), uint32(@dir[1].fileName[0]), 11); + dir[1].attributes := $10; dir[1].clusterLow := 0; { parent is root } + if not writeSectors(dataStart+spc, spc, buffer) then break; + writeSectors(start, 1, boot); + until true; + kfree(buffer); kfree(boot); + create_volume := err; end; end. diff --git a/src/driver/storage/vol/driver.storage.vol.mbr.pas b/src/driver/storage/vol/driver.storage.vol.mbr.pas index 3e033403..65061952 100644 --- a/src/driver/storage/vol/driver.storage.vol.mbr.pas +++ b/src/driver/storage/vol/driver.storage.vol.mbr.pas @@ -86,7 +86,7 @@ procedure set_bootable(partition_table : PPartition_table); begin push_trace('driver.storage.vol.mbr.set_bootable'); //set the bootble bit in attributes - partition_table^.attributes := (partition_table^.attributes and $80); + partition_table^.attributes := (partition_table^.attributes or $80); end; { Check a partitions bootable bit } @@ -105,7 +105,7 @@ begin partition_table^.LBA_start := address; partition_table^.sector_count := sectorSize; partition_table^.CHS_start := LBA_2_CHS(address); - partition_table^.CHS_start := LBA_2_CHS(address + sectorSize); + partition_table^.CHS_end := LBA_2_CHS(address + sectorSize - 1); push_trace('driver.storage.vol.mbr.setup_partition.end'); end; diff --git a/src/driver/storage/vol/driver.storage.vol.mgr.pas b/src/driver/storage/vol/driver.storage.vol.mgr.pas index 9f27918c..c00c417e 100644 --- a/src/driver/storage/vol/driver.storage.vol.mgr.pas +++ b/src/driver/storage/vol/driver.storage.vol.mgr.pas @@ -62,9 +62,9 @@ procedure create_volume_from_partition(device : PStorage_Device; sectorStart : u { --- Partition operations (read/write mbr + update volume list) --- } function get_partition(device : PStorage_Device; index : uint32) : TPartition_table; -procedure add_partition(device : PStorage_Device; slot : uint32; partition : TPartition_table); -procedure remove_partition(device : PStorage_Device; slot : uint32); -procedure init_disk(device : PStorage_Device); +function add_partition(device : PStorage_Device; slot : uint32; partition : TPartition_table) : TError; +function remove_partition(device : PStorage_Device; slot : uint32) : TError; +function init_disk(device : PStorage_Device) : TError; procedure discover_volumes(device : PStorage_Device); function find_free_space(device : PStorage_Device; needed : uint32) : uint32; function find_free_slot(device : PStorage_Device) : sint32; @@ -80,7 +80,12 @@ procedure init_disk_async(device : PStorage_Device; { --- Filesystem operations --- } function format_volume(device : PStorage_Device; volIndex : uint32; filesystemName : pchar; config : PFSFormatParams) : boolean; +{ False: no callback, caller retains callbackData. + True: worker owns completion and invokes the callback exactly once. } function format_volume_async(device : PStorage_Device; volIndex : uint32; filesystemName : pchar; config : PFSFormatParams; callback : TIOCallback; callbackData : pointer) : boolean; +{ eNone transfers completion to a worker; other returns never invoke callback. } +function set_volume_label_async(device : PStorage_Device; volIndex : uint32; text : pchar; + callback : TIOCallback; data : pointer) : TError; procedure delete_volume(volume : PStorage_Volume); implementation @@ -91,6 +96,13 @@ uses proc.types; type + PLabelWork = ^TLabelWork; + TLabelWork = record + Volume : PStorage_Volume; + Value : TVolumeLabel; + Callback : TIOCallback; + Data : pointer; + end; PStorageLifecycleWatch = ^TStorageLifecycleWatch; TStorageLifecycleWatch = record Active : boolean; @@ -116,6 +128,7 @@ const var LifecycleWatches : array[0..MAX_STORAGE_LIFECYCLE_WATCHES - 1] of TStorageLifecycleWatch; NextLifecycleWatchID : uint32; + NextVolumeID : uint32; procedure notify_lifecycle(event : TStorageLifecycleEvent; device : PStorage_Device; volume : PStorage_Volume; error : TError); var @@ -152,7 +165,7 @@ begin end; end; -procedure auto_mount_volume(volume : PStorage_Volume); +function auto_mount_volume(volume : PStorage_Volume) : TError; var volIdx : sint32; volNumStr : pchar; @@ -160,13 +173,14 @@ var prefix : pchar; mountPath : pchar; begin + auto_mount_volume := eDeviceNotReady; if volume = nil then exit; if volume^.filesystem = nil then exit; volIdx := find_volume_index(volume); if volIdx < 0 then exit; - volNumStr := intToString(uint32(volIdx)); + volNumStr := intToString(volume^.id); volName := stringConcat('vol', volNumStr); kfree(void(volNumStr)); prefix := stringNew(6); @@ -178,9 +192,12 @@ begin prefix[5] := '/'; mountPath := stringConcat(prefix, volName); - driver.storage.vfs.mountVolume(mountPath, volume); - if volume^.isBootDrive then - driver.storage.vfs.mountVolume('/boot', volume); + if driver.storage.vfs.mountVolume(mountPath, volume) = pvRegistered then begin + auto_mount_volume := eNone; + if volume^.isBootDrive then + if driver.storage.vfs.mountVolume('/boot', volume) <> pvRegistered then + auto_mount_volume := eDeviceNotReady; + end; kfree(void(volName)); kfree(void(prefix)); @@ -201,137 +218,71 @@ begin end; end; -procedure update_partition_system_id_sync(device : PStorage_Device; sectorStart : uint32; systemId : uint8); -var - srcMbr : PMaster_Boot_Record; - tempMbr : PMaster_Boot_Record; - slot : sint32; - freeSrcMbr : boolean; +function claim_metadata(device : PStorage_Device) : boolean; +var flags : uint32; begin - if device = nil then exit; - - srcMbr := driver.storage.mgr.get_cached_mbr(device); - freeSrcMbr := false; - if srcMbr = nil then begin - srcMbr := driver.storage.mgr.read_mbr(device); - freeSrcMbr := srcMbr <> nil; + claim_metadata := false; + if (device = nil) or device^.removed or (not device^.writable) then exit; + asm pushfd; cli; pop flags end; + if not device^.metadataBusy then begin + device^.metadataBusy := true; claim_metadata := true; end; - if srcMbr = nil then exit; - - slot := find_partition_slot_by_start(srcMbr, sectorStart); - if slot < 0 then begin - if freeSrcMbr then kfree(void(srcMbr)); - exit; - end; - - if srcMbr^.partition[slot].system_id = systemId then begin - if freeSrcMbr then kfree(void(srcMbr)); - exit; - end; - - tempMbr := PMaster_Boot_Record(kalloc(sizeof(TMaster_Boot_Record))); - if tempMbr = nil then begin - if freeSrcMbr then kfree(void(srcMbr)); - exit; - end; - memcpy(uint32(srcMbr), uint32(tempMbr), sizeof(TMaster_Boot_Record)); - tempMbr^.partition[slot].system_id := systemId; - driver.storage.mgr.write_mbr(device, tempMbr); - kfree(void(tempMbr)); - - if freeSrcMbr then kfree(void(srcMbr)); + if (flags and $200) <> 0 then asm sti end; end; -procedure update_partition_system_id_async(device : PStorage_Device; sectorStart : uint32; systemId : uint8); -var - srcMbr : PMaster_Boot_Record; - tempMbr : PMaster_Boot_Record; - slot : sint32; - freeSrcMbr : boolean; +function update_partition_system_id(device : PStorage_Device; sectorStart : uint32; systemId : uint8) : TError; +var src, copyMbr : PMaster_Boot_Record; slot : sint32; begin - if device = nil then exit; + update_partition_system_id := eNone; + src := get_cached_mbr(device); + if src = nil then begin update_partition_system_id := eDeviceNotReady; exit; end; + slot := find_partition_slot_by_start(src, sectorStart); + if slot < 0 then exit; { whole-device filesystem or GPT, no MBR type to update } + if src^.partition[slot].system_id = MBR_TYPE_GPT_PROTECTIVE then exit; + if src^.partition[slot].system_id = systemId then exit; + copyMbr := PMaster_Boot_Record(kalloc(sizeof(TMaster_Boot_Record))); + if copyMbr = nil then begin update_partition_system_id := eOutOfMemory; exit; end; + memcpy(uint32(src), uint32(copyMbr), sizeof(TMaster_Boot_Record)); + copyMbr^.partition[slot].system_id := systemId; + update_partition_system_id := write_mbr(device, copyMbr); + kfree(void(copyMbr)); +end; - srcMbr := driver.storage.mgr.get_cached_mbr(device); - freeSrcMbr := false; - if srcMbr = nil then begin - srcMbr := driver.storage.mgr.read_mbr(device); - freeSrcMbr := srcMbr <> nil; +function perform_format(vol : PStorage_Volume; fs : PFilesystem; config : PFSFormatParams) : TError; +var err : TError; +begin + perform_format := eFileInUse; + if not driver.storage.vfs.InvalidateVolume(vol) then exit; + vol^.filesystem := fs; + memset(uint32(@vol^.labelText[0]),0,sizeof(vol^.labelText)); + err := fs^.createCallback(vol, vol^.sectorCount, vol^.sectorStart, config); + { Do not identify/remount a partially formatted volume after an error. } + vol^.filesystem := nil; + if err = eNone then err := update_partition_system_id(vol^.device, vol^.sectorStart, fs^.system_id); + if err = eNone then begin + driver.storage.fs.mgr.probe_volume(vol); + if vol^.filesystem <> fs then err := eCorruptFilesystem + else err := auto_mount_volume(vol); end; - if srcMbr = nil then exit; - - slot := find_partition_slot_by_start(srcMbr, sectorStart); - if slot < 0 then begin - if freeSrcMbr then kfree(void(srcMbr)); - exit; + if err <> eNone then begin + driver.storage.vfs.InvalidateVolume(vol); + vol^.filesystem := nil; end; - - if srcMbr^.partition[slot].system_id = systemId then begin - if freeSrcMbr then kfree(void(srcMbr)); - exit; - end; - - tempMbr := PMaster_Boot_Record(kalloc(sizeof(TMaster_Boot_Record))); - if tempMbr = nil then begin - if freeSrcMbr then kfree(void(srcMbr)); - exit; - end; - memcpy(uint32(srcMbr), uint32(tempMbr), sizeof(TMaster_Boot_Record)); - tempMbr^.partition[slot].system_id := systemId; - driver.storage.mgr.write_mbr_async(device, tempMbr, nil, nil); - kfree(void(tempMbr)); - - if freeSrcMbr then kfree(void(srcMbr)); + perform_format := err; end; procedure format_volume_worker(pctx : proc.types.PProcessContext); -var - ctx : PFormatVolumeAsyncCtx; - err : TError; +var ctx : PFormatVolumeAsyncCtx; err : TError; begin ctx := PFormatVolumeAsyncCtx(pctx^.Local); - if ctx = nil then begin - proc.mgr.proc_exit(1); - exit; - end; - - err := eNone; - if (ctx^.Volume = nil) or (ctx^.Filesystem = nil) then - err := eInvalidArgument - else if ctx^.Volume^.device = nil then - err := eDeviceNotFound - else if not ctx^.Volume^.device^.writable then - err := eReadOnly - else if ctx^.Filesystem^.createCallback = nil then - err := eInvalidArgument; - - if err = eNone then begin - if not driver.storage.vfs.InvalidateVolume(ctx^.Volume) then - err := eFileInUse - else begin - update_partition_system_id_sync(ctx^.Volume^.device, ctx^.Volume^.sectorStart, ctx^.Filesystem^.system_id); - ctx^.Volume^.filesystem := ctx^.Filesystem; - ctx^.Filesystem^.createCallback(ctx^.Volume, ctx^.Volume^.sectorCount, ctx^.Volume^.sectorStart, ctx^.Config); - - { Re-probe the freshly formatted volume so the caller gets a verified - filesystem assignment and refreshed free-space metadata. } - ctx^.Volume^.filesystem := nil; - driver.storage.fs.mgr.probe_volume(ctx^.Volume); - if ctx^.Volume^.filesystem <> ctx^.Filesystem then - err := eUnsupportedFilesystem - else - auto_mount_volume(ctx^.Volume); - end; - end; - + if ctx = nil then begin proc.mgr.proc_exit(1); exit; end; + err := perform_format(ctx^.Volume, ctx^.Filesystem, ctx^.Config); set_volume_flag(ctx^.Volume, STORAGE_VOLUME_FLAG_FORMATTING, false); + ctx^.Volume^.device^.metadataBusy := false; notify_lifecycle(sleVolumeFormatCompleted, ctx^.Volume^.device, ctx^.Volume, err); - - if ctx^.Config <> nil then - kfree(void(ctx^.Config)); - if ctx^.UserCallback <> nil then - ctx^.UserCallback(err, ctx^.UserData); - kfree(void(ctx)); - proc.mgr.proc_exit(0); + if ctx^.Config <> nil then kfree(void(ctx^.Config)); + if ctx^.UserCallback <> nil then ctx^.UserCallback(err, ctx^.UserData); + kfree(void(ctx)); proc.mgr.proc_exit(0); end; { Create a TStorage_Volume from a partition entry and register it } @@ -370,70 +321,15 @@ begin end; end; -{ Remove from both global and device volume core.ds.lists, then free } -procedure remove_volume_by_start(device : PStorage_Device; sectorStart : uint32); -var - i : uint32; - v : PStorage_Volume; - found : PStorage_Volume; +procedure detach_volume(volume : PStorage_Volume); +var idx : sint32; begin - found := nil; - if device = nil then exit; - - i := 0; - while i < DL_Size(volumes) do begin - v := PStorage_Volume(Void(DL_Get(volumes, i))^); - if (v^.device = device) and (v^.sectorStart = sectorStart) then begin - found := v; - break; - end else - i := i + 1; - end; - - if found <> nil then begin - set_volume_flag(found, STORAGE_VOLUME_FLAG_INVALIDATING, true); - notify_lifecycle(sleVolumeInvalidating, device, found, eNone); - if not driver.storage.vfs.InvalidateVolume(found) then begin - set_volume_flag(found, STORAGE_VOLUME_FLAG_INVALIDATING, false); - exit; - end; - set_volume_flag(found, STORAGE_VOLUME_FLAG_INVALIDATING, false); - notify_lifecycle(sleVolumeInvalidated, device, found, eNone); - set_volume_flag(found, STORAGE_VOLUME_FLAG_REMOVED, true); - notify_lifecycle(sleVolumeRemoved, device, found, eNone); - remove_volume_from_device_list(device, found); - DL_Delete(volumes, i); - kfree(puint32(found)); - end; -end; - -{ Remove all volumes belonging to a device } -procedure remove_all_volumes_for_device(device : PStorage_Device); -var - i : uint32; - v : PStorage_Volume; -begin - i := 0; - while i < DL_Size(volumes) do begin - v := PStorage_Volume(Void(DL_Get(volumes, i))^); - if v^.device = device then begin - set_volume_flag(v, STORAGE_VOLUME_FLAG_INVALIDATING, true); - notify_lifecycle(sleVolumeInvalidating, device, v, eNone); - if not driver.storage.vfs.InvalidateVolume(v) then begin - set_volume_flag(v, STORAGE_VOLUME_FLAG_INVALIDATING, false); - i := i + 1; - continue; - end; - set_volume_flag(v, STORAGE_VOLUME_FLAG_INVALIDATING, false); - notify_lifecycle(sleVolumeInvalidated, device, v, eNone); - set_volume_flag(v, STORAGE_VOLUME_FLAG_REMOVED, true); - notify_lifecycle(sleVolumeRemoved, device, v, eNone); - DL_Delete(volumes, i); - remove_volume_from_device_list(device, v); - kfree(puint32(v)); - end else - i := i + 1; - end; + idx := find_volume_index(volume); + if idx < 0 then exit; + notify_lifecycle(sleVolumeRemoved, volume^.device, volume, eNone); + remove_volume_from_device_list(volume^.device, volume); + DL_Delete(volumes, uint32(idx)); + kfree(void(volume)); end; { ===================== Lifecycle ===================== } @@ -444,6 +340,7 @@ begin volumes := DL_New(sizeof(Pointer)); memset(uint32(@LifecycleWatches[0]), 0, sizeof(LifecycleWatches)); NextLifecycleWatchID := 1; + NextVolumeID := 0; end; function watch_lifecycle(callback : TStorageLifecycleCallback; userdata : pointer) : uint32; @@ -522,6 +419,7 @@ var begin push_trace('driver.storage.vol.mgr.register_volume'); if device = nil then exit; + volume^.id := NextVolumeID; Inc(NextVolumeID); volPtr := volume; DL_Add(volumes); DL_Set(volumes, DL_Size(volumes) - 1, puint32(@volPtr)); @@ -571,71 +469,128 @@ begin get_partition := mbr^.partition[index]; end; -{ Write a partition entry to the mbr and create a matching volume } -procedure add_partition(device : PStorage_Device; slot : uint32; partition : TPartition_table); +{ kind: 0=add, 1=remove, 2=initialize. Caller owns metadataBusy. } +function partition_operation(device : PStorage_Device; kind, slot : uint32; const part : TPartition_table) : TError; var - bootrecord : PMaster_Boot_Record; + mbr, src : PMaster_Boot_Record; + newVol, victim, vol : PStorage_Volume; + i, j : uint32; + err : TError; + partEnd, otherEnd : uint64; begin - push_trace('driver.storage.vol.mgr.add_partition'); - if (device = nil) or (slot > 3) or (not device^.writable) then exit; - - { Write to mbr } - bootrecord := driver.storage.mgr.read_mbr(device); - if bootrecord = nil then exit; - bootrecord^.partition[slot] := partition; - driver.storage.mgr.write_mbr(device, bootrecord); - kfree(puint32(bootrecord)); - - { Create volume in memory } - if (partition.LBA_start <> 0) and (partition.sector_count <> 0) then - create_volume_from_partition(device, partition.LBA_start, partition.sector_count); + partition_operation := eInvalidArgument; + if (device = nil) or (not device^.writable) or device^.removed or (slot > 3) then exit; + src := get_cached_mbr(device); + if src = nil then begin partition_operation := eDeviceNotReady; exit; end; + { GPT partition editing is not implemented; never mutate its protective MBR. } + for i := 0 to 3 do + if src^.partition[i].system_id = MBR_TYPE_GPT_PROTECTIVE then begin + partition_operation := eNotSupported; exit; + end; + newVol := nil; victim := nil; + if kind = 0 then begin + if (part.LBA_start = 0) or (part.sector_count = 0) or + (part.LBA_start >= device^.maxSectorCount) or + (part.sector_count > device^.maxSectorCount-part.LBA_start) then exit; + if src^.boot_sector = $AA55 then begin + if (src^.partition[slot].LBA_start <> 0) or (src^.partition[slot].sector_count <> 0) then begin + partition_operation := eAlreadyExists; exit; + end; + partEnd := uint64(part.LBA_start) + part.sector_count; + for i := 0 to 3 do begin + otherEnd := uint64(src^.partition[i].LBA_start) + src^.partition[i].sector_count; + if (src^.partition[i].sector_count <> 0) and + (part.LBA_start < otherEnd) and (src^.partition[i].LBA_start < partEnd) then exit; + end; + end else if (device^.volumes <> nil) and (DL_Size(device^.volumes) <> 0) then begin + partition_operation := eFileInUse; exit; + end; + newVol := PStorage_Volume(kalloc(sizeof(TStorage_Volume))); + if newVol = nil then begin partition_operation := eOutOfMemory; exit; end; + memset(uint32(newVol), 0, sizeof(TStorage_Volume)); + newVol^.device := device; newVol^.sectorStart := part.LBA_start; + newVol^.sectorCount := part.sector_count; newVol^.sectorSize := device^.sectorSize; + end; + mbr := PMaster_Boot_Record(kalloc(sizeof(TMaster_Boot_Record))); + if mbr = nil then begin + if newVol <> nil then kfree(void(newVol)); + partition_operation := eOutOfMemory; exit; + end; + memcpy(uint32(src), uint32(mbr), sizeof(TMaster_Boot_Record)); + if (kind = 2) or (mbr^.boot_sector <> $AA55) then begin + memset(uint32(mbr), 0, sizeof(TMaster_Boot_Record)); mbr^.boot_sector := $AA55; + end; + err := eNone; + if kind = 0 then mbr^.partition[slot] := part + else begin + { Refuse teardown before touching the partition entry. metadataBusy + prevents another format/delete from taking ownership of these objects. } + i := 0; + while i < DL_Size(volumes) do begin + vol := get_volume(i); + if (vol^.device = device) and ((kind = 2) or + (vol^.sectorStart = src^.partition[slot].LBA_start)) then begin + if volume_is_busy(vol) then begin err := eFileInUse; break; end; + if not driver.storage.vfs.InvalidateVolume(vol) then begin err := eFileInUse; break; end; + if kind = 1 then victim := vol; + end; + Inc(i); + end; + if (kind = 1) and (err = eNone) then + memset(uint32(@mbr^.partition[slot]), 0, sizeof(TPartition_table)); + end; + if err = eNone then err := write_mbr(device, mbr); + kfree(void(mbr)); + if err = eNone then begin + if kind = 0 then register_volume(device, newVol) + else if kind = 1 then begin if victim <> nil then detach_volume(victim); end + else begin + i := 0; + while i < DL_Size(volumes) do begin + vol := get_volume(i); + if vol^.device = device then detach_volume(vol) else Inc(i); + end; + end; + end else begin + if newVol <> nil then kfree(void(newVol)); + { Keep objects/cache on failure; remount anything invalidated above. } + i := 0; + while i < DL_Size(volumes) do begin + vol := get_volume(i); + if (vol^.device = device) and (vol^.filesystem <> nil) and + ((vol^.lifecycleFlags and STORAGE_VOLUME_FLAG_MOUNTED) = 0) then auto_mount_volume(vol); + Inc(i); + end; + end; + partition_operation := err; end; -{ Zero a partition entry in the mbr and remove the matching volume } -procedure remove_partition(device : PStorage_Device; slot : uint32); -var - bootrecord : PMaster_Boot_Record; - oldPart : TPartition_table; - emptyPart : TPartition_table; +function add_partition(device : PStorage_Device; slot : uint32; partition : TPartition_table) : TError; begin - push_trace('driver.storage.vol.mgr.remove_partition'); - if (device = nil) or (slot > 3) or (not device^.writable) then exit; - - { Read old entry so we know which volume to remove } - bootrecord := driver.storage.mgr.read_mbr(device); - if bootrecord = nil then exit; - oldPart := bootrecord^.partition[slot]; - - { Zero the slot } - memset(uint32(@emptyPart), 0, sizeof(TPartition_table)); - bootrecord^.partition[slot] := emptyPart; - driver.storage.mgr.write_mbr(device, bootrecord); - kfree(puint32(bootrecord)); - - { Remove the corresponding volume } - if (oldPart.LBA_start <> 0) and (oldPart.sector_count <> 0) then - remove_volume_by_start(device, oldPart.LBA_start); + add_partition := eFileInUse; + if not claim_metadata(device) then exit; + add_partition := partition_operation(device, 0, slot, partition); + device^.metadataBusy := false; end; -{ Wipe mbr (fresh signature) and remove all volumes for this device } -procedure init_disk(device : PStorage_Device); -var - bootrecord : PMaster_Boot_Record; +function remove_partition(device : PStorage_Device; slot : uint32) : TError; +var part : TPartition_table; begin - push_trace('driver.storage.vol.mgr.init_disk'); - if device = nil then exit; - if not device^.writable then exit; + remove_partition := eFileInUse; + if not claim_metadata(device) then exit; + memset(uint32(@part), 0, sizeof(part)); + remove_partition := partition_operation(device, 1, slot, part); + device^.metadataBusy := false; +end; - { Remove all volumes first } - remove_all_volumes_for_device(device); - - { Write clean mbr } - bootrecord := PMaster_Boot_Record(kalloc(sizeof(TMaster_Boot_Record))); - if bootrecord = nil then exit; - memset(uint32(bootrecord), 0, sizeof(TMaster_Boot_Record)); - bootrecord^.boot_sector := $AA55; - driver.storage.mgr.write_mbr(device, bootrecord); - kfree(puint32(bootrecord)); +function init_disk(device : PStorage_Device) : TError; +var part : TPartition_table; +begin + init_disk := eFileInUse; + if not claim_metadata(device) then exit; + memset(uint32(@part), 0, sizeof(part)); + init_disk := partition_operation(device, 2, 0, part); + device^.metadataBusy := false; end; { Read mbr and create volumes for all non-empty partitions. @@ -779,17 +734,24 @@ begin mbr := driver.storage.mgr.get_cached_mbr(device); if mbr = nil then exit; + if device^.maxSectorCount <= 1 then exit; + if mbr^.boot_sector <> $AA55 then begin + if needed <= device^.maxSectorCount-1 then find_free_space := 1; + exit; + end; nextLBA := 1; for i := 0 to 3 do begin if mbr^.partition[i].LBA_start <> 0 then begin + if (mbr^.partition[i].LBA_start >= device^.maxSectorCount) or + (mbr^.partition[i].sector_count > device^.maxSectorCount-mbr^.partition[i].LBA_start) then exit; partEnd := mbr^.partition[i].LBA_start + mbr^.partition[i].sector_count; if partEnd > nextLBA then nextLBA := partEnd; end; end; - if (nextLBA + needed) <= device^.maxSectorCount then + if (nextLBA <= device^.maxSectorCount) and (needed <= device^.maxSectorCount-nextLBA) then find_free_space := nextLBA; end; @@ -804,6 +766,7 @@ begin mbr := driver.storage.mgr.get_cached_mbr(device); if mbr = nil then exit; + if mbr^.boot_sector <> $AA55 then begin find_free_slot := 0; exit; end; for i := 0 to 3 do begin if (mbr^.partition[i].LBA_start = 0) and @@ -831,225 +794,183 @@ end; { ===================== Async partition operations ===================== } -{ Modify cached mbr in-place, create volume in memory, fire-and-forget write. - All in-memory changes happen synchronously so the caller can refresh UI - immediately. The disk write completes asynchronously. } +type + PPartitionWork = ^TPartitionWork; + TPartitionWork = record + Device : PStorage_Device; + Kind, Slot : uint32; + Part : TPartition_table; + Callback : TIOCallback; + Data : pointer; + end; + +procedure partition_worker(pctx : proc.types.PProcessContext); +var ctx : PPartitionWork; err : TError; +begin + ctx := PPartitionWork(pctx^.Local); + err := partition_operation(ctx^.Device, ctx^.Kind, ctx^.Slot, ctx^.Part); + ctx^.Device^.metadataBusy := false; + if ctx^.Callback <> nil then ctx^.Callback(err, ctx^.Data); + kfree(void(ctx)); proc.mgr.proc_exit(0); +end; + +procedure submit_partition(device : PStorage_Device; kind, slot : uint32; const part : TPartition_table; + callback : TIOCallback; data : pointer); +var ctx : PPartitionWork; +begin + if not claim_metadata(device) then begin + if callback <> nil then callback(eFileInUse, data); exit; + end; + ctx := PPartitionWork(kalloc(sizeof(TPartitionWork))); + if ctx <> nil then begin + ctx^.Device := device; ctx^.Kind := kind; ctx^.Slot := slot; + ctx^.Part := part; ctx^.Callback := callback; ctx^.Data := data; + if proc.mgr.create('vol.part', @partition_worker, void(ctx), 1) <> nil then exit; + kfree(void(ctx)); + end; + device^.metadataBusy := false; + if callback <> nil then callback(eOutOfMemory, data); +end; + procedure add_partition_async(device : PStorage_Device; slot : uint32; partition : TPartition_table; callback : TIOCallback; callbackData : pointer); -var - mbr : PMaster_Boot_Record; - vol : PStorage_Volume; -begin - push_trace('driver.storage.vol.mgr.add_partition_async'); - if (device = nil) or (slot > 3) or (not device^.writable) then begin - if callback <> nil then callback(eInvalidArgument, callbackData); - exit; - end; - - mbr := driver.storage.mgr.get_cached_mbr(device); - if mbr = nil then begin - if callback <> nil then callback(eDeviceNotReady, callbackData); - exit; - end; - - { Modify cache in-place } - mbr^.partition[slot] := partition; - - { Create volume in memory — skip probe_volume because - (a) the partition is freshly created / unformatted, and - (b) probe_volume does blocking I/O which is unsafe here. } - if (partition.LBA_start <> 0) and (partition.sector_count <> 0) then begin - vol := PStorage_Volume(kalloc(sizeof(TStorage_Volume))); - if vol <> nil then begin - vol^.device := device; - vol^.sectorStart := partition.LBA_start; - vol^.sectorCount := partition.sector_count; - vol^.sectorSize := device^.sectorSize; - vol^.freeSectors := 0; - vol^.filesystem := nil; - vol^.isBootDrive := false; - register_volume(device, vol); - end; - end; - - { Write cached mbr to disk through the storage manager's staged MBR path. } - driver.storage.mgr.write_mbr_async(device, mbr, callback, callbackData); -end; +begin submit_partition(device, 0, slot, partition, callback, callbackData); end; procedure remove_partition_async(device : PStorage_Device; slot : uint32; callback : TIOCallback; callbackData : pointer); -var - mbr : PMaster_Boot_Record; - oldPart : TPartition_table; - emptyPart : TPartition_table; +var part : TPartition_table; begin - push_trace('driver.storage.vol.mgr.remove_partition_async'); - if (device = nil) or (slot > 3) or (not device^.writable) then begin - if callback <> nil then callback(eInvalidArgument, callbackData); - exit; - end; - - mbr := driver.storage.mgr.get_cached_mbr(device); - if mbr = nil then begin - if callback <> nil then callback(eDeviceNotReady, callbackData); - exit; - end; - - oldPart := mbr^.partition[slot]; - - { Zero the slot in cache } - memset(uint32(@emptyPart), 0, sizeof(TPartition_table)); - mbr^.partition[slot] := emptyPart; - - { Remove the corresponding volume from memory } - if (oldPart.LBA_start <> 0) and (oldPart.sector_count <> 0) then - remove_volume_by_start(device, oldPart.LBA_start); - - { Write cached mbr to disk through the storage manager's staged MBR path. } - driver.storage.mgr.write_mbr_async(device, mbr, callback, callbackData); + memset(uint32(@part), 0, sizeof(part)); + submit_partition(device, 1, slot, part, callback, callbackData); end; -procedure init_disk_async(device : PStorage_Device; - callback : TIOCallback; callbackData : pointer); -var - cache : puint32; +procedure init_disk_async(device : PStorage_Device; callback : TIOCallback; callbackData : pointer); +var part : TPartition_table; begin - push_trace('driver.storage.vol.mgr.init_disk_async'); - if (device = nil) or (not device^.writable) then begin - if callback <> nil then callback(eInvalidArgument, callbackData); - exit; - end; - - { Remove all volumes first } - remove_all_volumes_for_device(device); - - { Create fresh cached mbr } - if device^.cachedMBR <> nil then - kfree(device^.cachedMBR); - cache := puint32(kalloc(sizeof(TMaster_Boot_Record))); - if cache = nil then begin - device^.cachedMBR := nil; - if callback <> nil then callback(eOutOfMemory, callbackData); - exit; - end; - memset(uint32(cache), 0, sizeof(TMaster_Boot_Record)); - PMaster_Boot_Record(cache)^.boot_sector := $AA55; - device^.cachedMBR := pointer(cache); - - { Write to disk through the storage manager's staged MBR path. } - driver.storage.mgr.write_mbr_async(device, PMaster_Boot_Record(cache), callback, callbackData); + memset(uint32(@part), 0, sizeof(part)); + submit_partition(device, 2, 0, part, callback, callbackData); end; { ===================== Filesystem operations ===================== } -function format_volume(device : PStorage_Device; volIndex : uint32; filesystemName : pchar; config : PFSFormatParams) : boolean; -var - vol : PStorage_Volume; - fs : PFilesystem; +function prepare_format(device : PStorage_Device; volIndex : uint32; name : pchar; config : PFSFormatParams; + var vol : PStorage_Volume; var fs : PFilesystem) : boolean; begin - push_trace('driver.storage.vol.mgr.format_volume'); - format_volume := false; - - vol := get_volume(volIndex); - if vol = nil then exit; - if vol^.device <> device then exit; - - fs := driver.storage.fs.mgr.find_filesystem_by_name(filesystemName); - if fs = nil then exit; - if fs^.createCallback = nil then exit; - if volume_is_busy(vol) then exit; - - set_volume_flag(vol, STORAGE_VOLUME_FLAG_FORMATTING, true); - notify_lifecycle(sleVolumeFormatStarted, device, vol, eNone); - if not driver.storage.vfs.InvalidateVolume(vol) then begin - set_volume_flag(vol, STORAGE_VOLUME_FLAG_FORMATTING, false); - notify_lifecycle(sleVolumeFormatCompleted, device, vol, eFileInUse); - exit; + prepare_format := false; + if not claim_metadata(device) then exit; + vol := get_volume(volIndex); fs := driver.storage.fs.mgr.find_filesystem_by_name(name); + if (vol <> nil) and (vol^.device = device) and (fs <> nil) then begin + if (fs^.createCallback <> nil) and (fs^.validateFormat <> nil) and (not volume_is_busy(vol)) then begin + if fs^.validateFormat(vol, vol^.sectorCount, vol^.sectorStart, config) = eNone then begin + set_volume_flag(vol, STORAGE_VOLUME_FLAG_FORMATTING, true); + prepare_format := true; exit; + end; + end; end; - - update_partition_system_id_sync(device, vol^.sectorStart, fs^.system_id); - vol^.filesystem := fs; - fs^.createCallback(vol, vol^.sectorCount, vol^.sectorStart, config); - vol^.filesystem := nil; - driver.storage.fs.mgr.probe_volume(vol); - format_volume := vol^.filesystem = fs; - if format_volume then - auto_mount_volume(vol); - set_volume_flag(vol, STORAGE_VOLUME_FLAG_FORMATTING, false); - if format_volume then - notify_lifecycle(sleVolumeFormatCompleted, device, vol, eNone) - else - notify_lifecycle(sleVolumeFormatCompleted, device, vol, eUnsupportedFilesystem); + device^.metadataBusy := false; end; +function format_volume(device : PStorage_Device; volIndex : uint32; filesystemName : pchar; config : PFSFormatParams) : boolean; +var vol : PStorage_Volume; fs : PFilesystem; err : TError; +begin + format_volume := false; + if not prepare_format(device, volIndex, filesystemName, config, vol, fs) then exit; + notify_lifecycle(sleVolumeFormatStarted, device, vol, eNone); + err := perform_format(vol, fs, config); + set_volume_flag(vol, STORAGE_VOLUME_FLAG_FORMATTING, false); + device^.metadataBusy := false; + notify_lifecycle(sleVolumeFormatCompleted, device, vol, err); + format_volume := err = eNone; +end; + +{ False: caller still owns callbackData and NO callback has fired. + True: worker owns it and invokes exactly one callback. } function format_volume_async(device : PStorage_Device; volIndex : uint32; filesystemName : pchar; config : PFSFormatParams; callback : TIOCallback; callbackData : pointer) : boolean; -var - vol : PStorage_Volume; - fs : PFilesystem; - ctx : PFormatVolumeAsyncCtx; - cfgCopy : PFSFormatParams; +var vol : PStorage_Volume; fs : PFilesystem; ctx : PFormatVolumeAsyncCtx; begin - push_trace('driver.storage.vol.mgr.format_volume_async'); format_volume_async := false; - - vol := get_volume(volIndex); - if vol = nil then exit; - if vol^.device <> device then exit; - - fs := driver.storage.fs.mgr.find_filesystem_by_name(filesystemName); - if fs = nil then exit; - if fs^.createCallback = nil then exit; - if volume_is_busy(vol) then begin - if callback <> nil then callback(eFileInUse, callbackData); - exit; - end; - - cfgCopy := nil; - if config <> nil then begin - cfgCopy := PFSFormatParams(kalloc(sizeof(TFSFormatParams))); - if cfgCopy = nil then begin - if callback <> nil then callback(eOutOfMemory, callbackData); - exit; - end; - memcpy(uint32(config), uint32(cfgCopy), sizeof(TFSFormatParams)); - end; - + if not prepare_format(device, volIndex, filesystemName, config, vol, fs) then exit; ctx := PFormatVolumeAsyncCtx(kalloc(sizeof(TFormatVolumeAsyncCtx))); - if ctx = nil then begin - if cfgCopy <> nil then kfree(void(cfgCopy)); - if callback <> nil then callback(eOutOfMemory, callbackData); - exit; - end; - ctx^.Volume := vol; - ctx^.Filesystem := fs; - ctx^.Config := cfgCopy; - ctx^.UserCallback := callback; - ctx^.UserData := callbackData; - - set_volume_flag(vol, STORAGE_VOLUME_FLAG_FORMATTING, true); - notify_lifecycle(sleVolumeFormatStarted, device, vol, eNone); - - if proc.mgr.create('vol.fmt', @format_volume_worker, void(ctx), 1) = nil then begin - set_volume_flag(vol, STORAGE_VOLUME_FLAG_FORMATTING, false); - notify_lifecycle(sleVolumeFormatCompleted, device, vol, eOutOfMemory); - if cfgCopy <> nil then kfree(void(cfgCopy)); + if ctx <> nil then begin + ctx^.Volume := vol; ctx^.Filesystem := fs; + ctx^.UserCallback := callback; ctx^.UserData := callbackData; + ctx^.Config := nil; + if config <> nil then begin + ctx^.Config := PFSFormatParams(kalloc(sizeof(TFSFormatParams))); + if ctx^.Config <> nil then memcpy(uint32(config), uint32(ctx^.Config), sizeof(TFSFormatParams)); + end; + if (config = nil) or (ctx^.Config <> nil) then begin + notify_lifecycle(sleVolumeFormatStarted, device, vol, eNone); + if proc.mgr.create('vol.fmt', @format_volume_worker, void(ctx), 1) <> nil then begin + format_volume_async := true; exit; + end; + end; + if ctx^.Config <> nil then kfree(void(ctx^.Config)); kfree(void(ctx)); - if callback <> nil then callback(eOutOfMemory, callbackData); - exit; end; + set_volume_flag(vol, STORAGE_VOLUME_FLAG_FORMATTING, false); + device^.metadataBusy := false; + notify_lifecycle(sleVolumeFormatCompleted, device, vol, eOutOfMemory); +end; - format_volume_async := true; +procedure label_worker(pctx : proc.types.PProcessContext); +var ctx : PLabelWork; vol : PStorage_Volume; err, mountErr : TError; wasMounted : boolean; +begin + ctx := PLabelWork(pctx^.Local); vol := ctx^.Volume; + wasMounted := (vol^.lifecycleFlags and STORAGE_VOLUME_FLAG_MOUNTED) <> 0; + err := eFileInUse; + if (not driver.storage.vfs.VolumeHasOpenHandles(vol)) and driver.storage.vfs.InvalidateVolume(vol) then begin + err := vol^.filesystem^.setLabelCallback(vol,@ctx^.Value[0]); + if wasMounted then begin + mountErr := auto_mount_volume(vol); + if err = eNone then err := mountErr; + end; + end; + vol^.device^.metadataBusy := false; + notify_lifecycle(sleVolumeLabelChanged,vol^.device,vol,err); + if ctx^.Callback <> nil then ctx^.Callback(err,ctx^.Data); + kfree(void(ctx)); proc.mgr.proc_exit(0); +end; + +function set_volume_label_async(device : PStorage_Device; volIndex : uint32; text : pchar; + callback : TIOCallback; data : pointer) : TError; +var vol : PStorage_Volume; ctx : PLabelWork; value : TVolumeLabel; +begin + set_volume_label_async := eInvalidArgument; + if device = nil then exit; + if not device^.writable then begin set_volume_label_async := eReadOnly; exit; end; + set_volume_label_async := eFileInUse; + if not claim_metadata(device) then exit; + repeat + vol := get_volume(volIndex); + if (vol = nil) or (vol^.device <> device) then begin set_volume_label_async := eVolumeNotFound; break; end; + if volume_is_busy(vol) then break; + set_volume_label_async := eUnsupportedFilesystem; + if vol^.filesystem = nil then break; + if (vol^.filesystem^.validateLabelCallback = nil) or (vol^.filesystem^.setLabelCallback = nil) then break; + set_volume_label_async := vol^.filesystem^.validateLabelCallback(text,value); + if set_volume_label_async <> eNone then break; + set_volume_label_async := eOutOfMemory; + ctx := PLabelWork(kalloc(sizeof(TLabelWork))); if ctx = nil then break; + ctx^.Volume := vol; ctx^.Value := value; ctx^.Callback := callback; ctx^.Data := data; + if proc.mgr.create('vol.label',@label_worker,void(ctx),1) <> nil then begin + set_volume_label_async := eNone; exit; + end; + kfree(void(ctx)); + until true; + device^.metadataBusy := false; end; procedure delete_volume(volume : PStorage_Volume); -var - device : PStorage_Device; +var device : PStorage_Device; begin - push_trace('driver.storage.vol.mgr.delete_volume'); if volume = nil then exit; device := volume^.device; - remove_volume_by_start(device, volume^.sectorStart); + if not claim_metadata(device) then exit; + if not volume_is_busy(volume) then + if driver.storage.vfs.InvalidateVolume(volume) then detach_volume(volume); + device^.metadataBusy := false; end; initialization diff --git a/src/test/test.pas b/src/test/test.pas index 5b7b787e..09b5d665 100644 --- a/src/test/test.pas +++ b/src/test/test.pas @@ -12,6 +12,7 @@ implementation uses core.strings, + app.diskutil, driver.bus.usb.types, driver.bus.usb.core, driver.bus.usb.uhci, @@ -32,6 +33,7 @@ uses core.ds.prio, driver.storage.vfs, driver.storage.test, + driver.storage.metatest, core.enc.fnv1a, core.enc.djb2, core.ds.bloom, @@ -46,6 +48,7 @@ procedure runAllTests(); begin { Run unit tests } core.strings.UnitTest; + app.diskutil.UnitTest; driver.bus.usb.types.UnitTest; driver.bus.usb.core.UnitTest; driver.bus.usb.uhci.UnitTest; @@ -66,6 +69,7 @@ begin core.ds.prio.UnitTest; driver.storage.vfs.UnitTest; driver.storage.test.UnitTest; + driver.storage.metatest.UnitTest; core.enc.fnv1a.UnitTest; core.enc.djb2.UnitTest; core.ds.bloom.UnitTest; -- 2.54.0 From d8d51ba5df9c3f96ace0e4ba6765d95120a71b58 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 31 Oct 2026 16:39:43 +0000 Subject: [PATCH 10/21] feat(storage): use volume labels for default mount folders --- src/driver/storage/driver.storage.vfs.pas | 138 +++++++++++++----- .../storage/vol/driver.storage.vol.mgr.pas | 38 +---- 2 files changed, 104 insertions(+), 72 deletions(-) diff --git a/src/driver/storage/driver.storage.vfs.pas b/src/driver/storage/driver.storage.vfs.pas index b9521175..c8ae5e45 100644 --- a/src/driver/storage/driver.storage.vfs.pas +++ b/src/driver/storage/driver.storage.vfs.pas @@ -143,6 +143,7 @@ procedure RemoveVirtualTree(Path : pchar); { Volume Mount Functions } function mountVolume(mountPath : pchar; volume : PStorage_Volume) : TRegError; +function mountDefaultVolume(volume : PStorage_Volume) : TError; procedure auto_mount_volumes(); procedure UnitTest; @@ -1185,6 +1186,60 @@ begin debug.tracer.push_trace('driver.storage.vfs.mountVolume.exit'); end; +{ Caller owns the returned name. Keep labels as one path component. } +function defaultVolumeName(volume : PStorage_Volume) : pchar; +var name : array[0..32] of char; n : uint32; number : pchar; +begin + n := 0; + while (n < 32) and (volume^.labelText[n] <> #0) do begin + name[n] := volume^.labelText[n]; + if (ord(name[n]) < 32) or (name[n] = '/') or (name[n] = '\') then name[n] := '_'; + Inc(n); + end; + while (n > 0) and (name[n-1] = ' ') do Dec(n); + name[n] := #0; + if (n = 0) or stringEquals(@name[0],'.') or stringEquals(@name[0],'..') then begin + number := intToString(volume^.id); + defaultVolumeName := stringConcat('vol',number); kfree(void(number)); + end else defaultVolumeName := stringCopy(@name[0]); +end; + +function mountDefaultVolume(volume : PStorage_Volume) : TError; +var base, name, path, number, suffix, tmp : pchar; obj : PVFSObject; attempt : uint32; +begin + mountDefaultVolume := eInvalidArgument; + if volume = nil then exit; + if volume^.filesystem = nil then begin mountDefaultVolume := eUnsupportedFilesystem; exit; end; + base := defaultVolumeName(volume); + number := intToString(volume^.id); suffix := stringConcat('-vol',number); kfree(void(number)); + mountDefaultVolume := eAlreadyExists; + for attempt := 0 to 1023 do begin + if attempt = 0 then name := stringCopy(base) + else begin + name := stringConcat(base,suffix); + if attempt > 1 then begin + number := intToString(attempt); tmp := stringConcat(name,'-'); kfree(void(name)); + name := stringConcat(tmp,number); kfree(void(tmp)); kfree(void(number)); + end; + end; + path := stringConcat('/disk/',name); kfree(void(name)); + obj := GetObjectFromPath(path); + if (obj = nil) or ((obj^.ObjectType = otDRIVE) and (obj^.Reference = void(volume))) then begin + mountDefaultVolume := eDeviceNotReady; + if mountVolume(path,volume) = pvRegistered then begin + mountDefaultVolume := eNone; + io.syslog.writestring('VFS: Mounted '); io.syslog.writestring(volume^.filesystem^.sName); + io.syslog.writestring(' volume at '); io.syslog.writestringln(path); + if volume^.isBootDrive then + if mountVolume('/boot',volume) <> pvRegistered then mountDefaultVolume := eDeviceNotReady; + end; + kfree(void(path)); break; + end; + kfree(void(path)); + end; + kfree(void(base)); kfree(void(suffix)); +end; + { Filesystem Functions } { Return the current process's FD table, or nil if no process is running. } @@ -3402,9 +3457,6 @@ var i : uint32; vol : PStorage_Volume; volName : pchar; - volNumStr : pchar; - mountPath : pchar; - prefix : pchar; readErr : uint32; mntPath : pchar; mntLen : uint32; @@ -3436,40 +3488,13 @@ begin if vol^.filesystem = nil then continue; - { Build mount name: e.g. 'vol0', 'vol1' } - volNumStr := intToString(i); - volName := stringConcat('vol', volNumStr); - kfree(void(volNumStr)); - prefix := stringNew(6); - prefix[0] := '/'; - prefix[1] := 'd'; - prefix[2] := 'i'; - prefix[3] := 's'; - prefix[4] := 'k'; - prefix[5] := '/'; - mountPath := stringConcat(prefix, volName); - - mountVolume(mountPath, vol); - - io.syslog.writestring('VFS: Mounted '); - io.syslog.writestring(vol^.filesystem^.sName); - io.syslog.writestring(' volume at '); - io.syslog.writestringln(mountPath); - - { Auto-mount the boot drive at /boot } - if vol^.isBootDrive then begin - mountVolume('/boot', vol); - io.syslog.writestringln('VFS: Mounted boot volume at /boot'); - end; - - kfree(void(volName)); - kfree(void(prefix)); - kfree(void(mountPath)); + if mountDefaultVolume(vol) <> eNone then + io.syslog.logln('VFS','Could not allocate a default mount name for volume.'); end; { Process mount.asr from the boot volume. Each line has the form: mnt {device} - {device} is replaced with the boot volume's /disk/volN path. + {device} uses the stable /boot alias, including when labels contain spaces. A symlink is created: target -> expanded_source. } for i := 0 to volCount - 1 do begin vol := driver.storage.vol.mgr.get_volume(i); @@ -3479,10 +3504,8 @@ begin if vol^.filesystem^.fileSizeCallback = nil then continue; if vol^.filesystem^.readOffsetCallback = nil then continue; - { Build the boot device path: /disk/vol } - volNumStr := intToString(i); - volName := stringConcat('/disk/vol', volNumStr); - kfree(void(volNumStr)); + { Stable across label changes; safe in the space-delimited mount script. } + volName := stringCopy('/boot'); mntLen := vol^.filesystem^.fileSizeCallback(vol, '', 'MOUNT.ASR'); @@ -3645,6 +3668,8 @@ end; procedure UnitTest; var + mountVolA, mountVolB : PStorage_Volume; + mountFS : PFilesystem; passed, failed : uint32; result : TIsPathValid; errCode : TError; @@ -3699,6 +3724,45 @@ var begin passed := 0; failed := 0; + { Synthetic mount objects only: no filesystem callback performs disk I/O. } + mountVolA := PStorage_Volume(kalloc(sizeof(TStorage_Volume))); + mountVolB := PStorage_Volume(kalloc(sizeof(TStorage_Volume))); + mountFS := PFilesystem(kalloc(sizeof(TFilesystem))); + mountFS^.sName := 'MOUNTTEST'; + mountVolA^.filesystem := mountFS; mountVolB^.filesystem := mountFS; + mountVolA^.id := $FFFFFFFE; mountVolB^.id := $FFFFFFFD; + tmpKey := defaultVolumeName(mountVolA); + Assert(stringEquals(tmpKey,'vol4294967294'),'unlabelled default mount name'); kfree(void(tmpKey)); + memcpy(uint32(pchar('MY DATA')),uint32(@mountVolA^.labelText[0]),8); + tmpKey := defaultVolumeName(mountVolA); + Assert(stringEquals(tmpKey,'MY DATA'),'label spaces preserved'); kfree(void(tmpKey)); + memset(uint32(@mountVolA^.labelText[0]),0,sizeof(mountVolA^.labelText)); + memcpy(uint32(pchar('A/B')),uint32(@mountVolA^.labelText[0]),4); + tmpKey := defaultVolumeName(mountVolA); + Assert(stringEquals(tmpKey,'A_B'),'label cannot create nested mount paths'); kfree(void(tmpKey)); + memset(uint32(@mountVolA^.labelText[0]),0,sizeof(mountVolA^.labelText)); + memcpy(uint32(pchar('..')),uint32(@mountVolA^.labelText[0]),3); + tmpKey := defaultVolumeName(mountVolA); + Assert(stringEquals(tmpKey,'vol4294967294'),'dot-dot label falls back safely'); kfree(void(tmpKey)); + memcpy(uint32(pchar('__MOUNT_TEST__')),uint32(@mountVolA^.labelText[0]),15); + mountVolB^.labelText := mountVolA^.labelText; + Assert(mountDefaultVolume(mountVolA) = eNone,'label mount succeeds'); + obj := GetObjectFromPath('/disk/__MOUNT_TEST__'); + Assert((obj <> nil) and (obj^.Reference = void(mountVolA)),'label mount points to correct volume'); + Assert(mountDefaultVolume(mountVolB) = eNone,'duplicate labels both mount'); + obj := GetObjectFromPath('/disk/__MOUNT_TEST__-vol4294967293'); + Assert((obj <> nil) and (obj^.Reference = void(mountVolB)),'duplicate label gets ID suffix'); + Assert(mountDefaultVolume(mountVolB) = eNone,'duplicate mount is idempotent'); + Assert(InvalidateVolume(mountVolA),'remove old label mount'); + memset(uint32(@mountVolA^.labelText[0]),0,sizeof(mountVolA^.labelText)); + memcpy(uint32(pchar('__RENAMED_TEST__')),uint32(@mountVolA^.labelText[0]),17); + Assert(mountDefaultVolume(mountVolA) = eNone,'renamed volume remounts'); + Assert(GetObjectFromPath('/disk/__MOUNT_TEST__') = nil,'old label mount removed'); + obj := GetObjectFromPath('/disk/__RENAMED_TEST__'); + Assert((obj <> nil) and (obj^.Reference = void(mountVolA)),'renamed mount points to same volume'); + InvalidateVolume(mountVolA); InvalidateVolume(mountVolB); + kfree(void(mountVolA)); kfree(void(mountVolB)); kfree(void(mountFS)); + watchFired := 0; io.syslog.logln('VFS', 'Unit tests starting...'); diff --git a/src/driver/storage/vol/driver.storage.vol.mgr.pas b/src/driver/storage/vol/driver.storage.vol.mgr.pas index c00c417e..737fcabd 100644 --- a/src/driver/storage/vol/driver.storage.vol.mgr.pas +++ b/src/driver/storage/vol/driver.storage.vol.mgr.pas @@ -166,42 +166,10 @@ begin end; function auto_mount_volume(volume : PStorage_Volume) : TError; -var - volIdx : sint32; - volNumStr : pchar; - volName : pchar; - prefix : pchar; - mountPath : pchar; begin - auto_mount_volume := eDeviceNotReady; - if volume = nil then exit; - if volume^.filesystem = nil then exit; - - volIdx := find_volume_index(volume); - if volIdx < 0 then exit; - - volNumStr := intToString(volume^.id); - volName := stringConcat('vol', volNumStr); - kfree(void(volNumStr)); - prefix := stringNew(6); - prefix[0] := '/'; - prefix[1] := 'd'; - prefix[2] := 'i'; - prefix[3] := 's'; - prefix[4] := 'k'; - prefix[5] := '/'; - mountPath := stringConcat(prefix, volName); - - if driver.storage.vfs.mountVolume(mountPath, volume) = pvRegistered then begin - auto_mount_volume := eNone; - if volume^.isBootDrive then - if driver.storage.vfs.mountVolume('/boot', volume) <> pvRegistered then - auto_mount_volume := eDeviceNotReady; - end; - - kfree(void(volName)); - kfree(void(prefix)); - kfree(void(mountPath)); + auto_mount_volume := eVolumeNotFound; + if find_volume_index(volume) < 0 then exit; + auto_mount_volume := driver.storage.vfs.mountDefaultVolume(volume); end; function find_partition_slot_by_start(mbr : PMaster_Boot_Record; sectorStart : uint32) : sint32; -- 2.54.0 From 3d4f846a8f905035715fd994bb98502e79db8b14 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 31 Oct 2026 16:56:18 +0000 Subject: [PATCH 11/21] fix(ahci): harden command submission, DMA ownership, and timeout recovery --- .../ctl/ahci/driver.storage.ctl.ahci.pas | 933 ++++++++---------- .../ahci/driver.storage.ctl.ahci.types.pas | 11 +- .../storage/driver.storage.metatest.pas | 19 +- src/driver/storage/driver.storage.mgr.pas | 33 +- src/driver/storage/driver.storage.types.pas | 1 + src/test/test.pas | 2 + 6 files changed, 475 insertions(+), 524 deletions(-) diff --git a/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas b/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas index 83fc73fe..0e6fe06c 100644 --- a/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas +++ b/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas @@ -43,6 +43,7 @@ var ahciControllers : PDList; page_base : puint32; +procedure UnitTest; procedure init(); function load(ptr : void) : boolean; procedure check_ports(controller : PAHCI_Controller); @@ -68,8 +69,20 @@ function send_read_capacity(device : PAHCI_Device; sectorCount : puint32; blockS implementation +uses arch.x86.bda, arch.x86.isr.tmr0, core.panic, io.stdio, core.strings; + const AHCI_ENABLE_NCQ = true; + AHCI_IO_TICKS = 80000; { about 10 seconds at the ~8 kHz PIT } + AHCI_STOP_TICKS = 8000; + AHCI_FATAL_MASK = $79000000; { TFES, HBFS, HBDS, IFS, OFS } + AHCI_IRQ_MASK = $7900000B; + +var watchdogDivider : uint32 = 0; + +procedure cmd_ahcitest(params : PParamList; stdin_buf, stdout_buf, stderr_buf : POutBuf); forward; +procedure ahci_watchdog(data : void); forward; +procedure fail_controller(controller : PAHCI_Controller; error : TError); forward; function min_u32(a : uint32; b : uint32) : uint32; begin @@ -92,7 +105,8 @@ begin devID.id4:= idANY; devID.ex:= nil; driver.mgr.register_driver('ATA/PI driver.storage.ctl.ahci Driver', @devID, @load); - //TODO check driver.storage.ctl.ide devices in driver.storage.ctl.ide for sata devices + arch.x86.isr.tmr0.hook(uint32(@ahci_watchdog)); + io.stdio.registerCommand('AHCITEST',@cmd_ahcitest,'Run AHCI tests using simulated registers'); end; { Poll MMIO through a pointer on every iteration. The PIT runs at ~8 kHz; @@ -170,11 +184,12 @@ var command_header : PHBA_CMD_HEADER; begin for i:=0 to 31 do begin + if controller^.offline then break; if ((controller^.mio^.ports_implimented shr i) and 1) = 1 then begin port := @controller^.mio^.ports[i]; device := PAHCI_Device(@controller^.devices[i]); - device^.port := port; + device^.port := port; device^.owner := controller; port^.int_enable := 0; if not stop_port(port) then begin log_port_failure(i,port,'cannot stop engine'); continue; end; @@ -225,12 +240,13 @@ begin //set command header locations command_header := PHBA_CMD_HEADER(uint32(cmd_list_base) + (ii * sizeof(THBA_CMD_HEADER))); command_header^.prdtl := 32; - command_header^.cmd_table_base := vtop(uint32(cmd_table_base)) + (ii * sizeof(THBA_CMD_TABLE)); + command_header^.cmd_table_base := vtop(uint32(cmd_table_base) + (ii * sizeof(THBA_CMD_TABLE))); command_header^.cmd_table_baseu := 0; end; registered := false; for attempt := 1 to 2 do begin + if controller^.offline or device^.offline then break; if not reset_port(port) then begin log_port_failure(i,port,'reset/readiness timeout'); continue; end; signatureWait := 0; while (puint32(@port^.signature)^ = 0) or (puint32(@port^.signature)^ = $FFFFFFFF) do begin @@ -247,7 +263,9 @@ begin end; if not registered then begin port^.int_enable := 0; - if stop_port(port) then begin + if not controller^.offline then + if not stop_port(port) then fail_controller(controller,eIOTimeout); + begin port^.cmdl_basel := 0; port^.fis_basel := 0; kfree(listAlloc); kfree(fisAlloc); kfree(tableAlloc); device^.command_list := nil; device^.fis := nil; device^.command_table := nil; @@ -332,9 +350,8 @@ begin if (not wait_register(@device^.port^.cmd_issue,1,0,2500)) or ((device^.port^.tfd and $1) <> 0) or ((device^.port^.int_status and $40000000) <> 0) then begin log_port_failure(portIndex,device^.port,'IDENTIFY timeout or task-file error'); - { Free DMA memory only once both engines have stopped. If hardware cannot - stop, retaining this one buffer is safer than a late DMA into freed memory. } - if stop_port(device^.port) then kfree(buffer); + if not stop_port(device^.port) then fail_controller(controller,eIOTimeout); + kfree(buffer); exit; end; device^.port^.int_status := $FFFFFFFF; @@ -342,13 +359,13 @@ begin { Re-enable port interrupts now that IDENTIFY polling is done. $4000000B = bit 0 (D2H Register FIS) | bit 1 (PIO Setup FIS) | bit 3 (Set Device Bits / NCQ) | bit 30 (Task File Error) Bit 3 is required for NCQ completion notifications via SDB FIS. } - device^.port^.int_enable := $4000000B; + device^.port^.int_enable := AHCI_IRQ_MASK; b8 := puint16(buffer); memcpy(uint32(buffer), uint32(@device^.ata_info[0]), 512); sec_count := (b8[61] shl 16) or b8[60]; ncqDepth := (b8[75] and $1F) + 1; - ncqSupported := ((b8[76] and (1 shl 8)) <> 0) and (not isATAPI) and AHCI_ENABLE_NCQ; + ncqSupported := ((b8[76] and (1 shl 8)) <> 0) and (not isATAPI) and AHCI_ENABLE_NCQ and ((controller^.mio^.capabilites and (1 shl 30)) <> 0); if ncqSupported then begin device^.supportsNCQ := true; device^.queueDepth := min_u32(slotCount, ncqDepth); @@ -357,10 +374,7 @@ begin device^.queueDepth := 1; end; - { Free the 512-byte IDENTIFY buffer before allocating the write-test buffer } kfree(buffer); - buffer := puint32(kalloc(2048)); - memset(uint32(buffer), 0, 2048); { For ATAPI devices, get capacity from SCSI READ CAPACITY instead of IDENTIFY } if isATAPI then begin @@ -419,8 +433,7 @@ begin io.syslog.writestringln(': sector count is 0, skipping registration.'); end; - { Free the write-test buffer allocated above } - kfree(buffer); + end; @@ -461,10 +474,10 @@ var cmd_table_base : puint32; int_no : uInt8; timeout : uint32; - bohc : uint32; + flags : uint32; begin if ahciControllers = nil then begin - ahciControllers := DL_New(SizeOf(TAHCI_Controller)); + ahciControllers := DL_New(SizeOf(Pointer)); end; device := PPCI_Device(ptr); @@ -473,7 +486,9 @@ begin psleep(5); int_no := device^.interrupt_line + 32; - controller := PAHCI_Controller(DL_Add(ahciControllers)); + load := false; + controller := PAHCI_Controller(kalloc(sizeof(TAHCI_Controller))); + if controller = nil then exit; controller^.pci_device := PPCI_Device(device); @@ -485,40 +500,23 @@ begin base := PHBA_Memory(device^.address5); controller^.mio := base; - bohc := controller^.mio^.bohc; - - if (bohc and $1) <> 0 or (bohc and not (1 shl 1)) then begin - - // Set the OS Owned Semaphore (typically bit 1). - bohc := bohc or (1 shl 1); - controller^.mio^.bohc := bohc; - - // Wait until the BIOS Owned Semaphore (bit 0) is cleared. - timeout := 0; - while ((controller^.mio^.bohc and $1) <> 0) and (timeout < 10000) do begin - timeout := timeout + 1; + if (base^.capabilites2 and 1) <> 0 then begin + base^.bohc := base^.bohc or 2; + if not wait_register(@base^.bohc,$11,0,1000) then begin + io.syslog.logln('AHCI','BIOS handoff timed out; controller not started.'); + kfree(void(controller)); exit; end; - - if timeout = 10000 then begin - io.syslog.writestringln('driver.storage.ctl.ahci: BIOS/OS handoff timed out.'); - end else begin - // io.syslog.writestringln('driver.storage.ctl.ahci: BIOS/OS handoff successful.'); - end; - end else begin - // io.syslog.writestringln('driver.storage.ctl.ahci: BIOS not holding controller or handoff already complete.'); end; - - //print ghc - // io.syslog.writestring('driver.storage.ctl.ahci: GHC: '); - // io.syslog.writebin32ln(base^.global_ctrl); - - base^.global_ctrl := base^.global_ctrl or AHCI_CONTROLLER_MODE; - - base^.global_ctrl := base^.global_ctrl or 1; - - while (base^.global_ctrl and 1) <> 0 do begin + base^.global_ctrl := base^.global_ctrl or AHCI_CONTROLLER_MODE or 1; + if not wait_register(@base^.global_ctrl,1,0,500) then begin + io.syslog.logln('AHCI','Controller reset timed out; controller not started.'); + driver.bus.pci.setBusMaster(device^.bus,device^.slot,device^.func,false); + kfree(void(controller)); exit; end; - + { Publish a stable heap pointer, never an interior pointer into a growing DList. } + asm pushfd; cli; pop flags end; + DL_Add(ahciControllers); DL_Set(ahciControllers,DL_Size(ahciControllers)-1,puint32(@controller)); + if (flags and $200) <> 0 then asm sti end; registerISR(int_no, @ahci_isr); @@ -545,472 +543,294 @@ begin base^.int_status := $FFFFFFFF; check_ports(controller); + load := not controller^.offline; end; -procedure ahci_isr(); -var - i : uint32; - j : uint32; - k : uint32; - ctrl_is : uint32; - port_is : uint32; - active : uint32; - slotCount : uint32; - completionCb : TIOCompletion; - completionData : puint32; - port : PHBA_Port; - controller : PAHCI_Controller; - device : PAHCI_Device; - success : boolean; +function now_tick : uint32; +begin now_tick := puint32(@Counters.c32)^; end; + +{ Only complete commands whose DMA ownership has ended. Callback re-entry may + submit more work, so consume a fixed mask rather than rescanning new slots. } +procedure finish_slots(device : PAHCI_Device; mask : uint32; error : TError); +var slot : uint32; cb : TIOCompletion; data : puint32; begin - if ahciControllers = nil then exit; - for i := 0 to DL_Size(ahciControllers) - 1 do begin - controller := PAHCI_Controller(DL_Get(ahciControllers, i)); - ctrl_is := controller^.mio^.int_status; - if ctrl_is = 0 then continue; - - for j := 0 to 31 do begin - if (ctrl_is shr j) and 1 = 0 then continue; - - port := @controller^.mio^.ports[j]; - port_is := port^.int_status; - device := PAHCI_Device(@controller^.devices[j]); - - { success: no ERR bit in TFD, and no Task File Error interrupt } - success := ((port^.tfd and $1) = 0) and ((port_is and $40000000) = 0); - - { Scan all command slots for completed operations } - active := port^.cmd_issue or port^.sata_active; - slotCount := device^.slotCount; - if slotCount = 0 then slotCount := 32; - if slotCount > 32 then slotCount := 32; - if slotCount > 0 then - for k := 0 to slotCount - 1 do begin - if device^.pending[k].inUse then begin - if ((active shr k) and 1) = 0 then begin - completionCb := device^.pending[k].completion; - completionData := device^.pending[k].userdata; - device^.pending[k].inUse := false; - device^.pending[k].completion := nil; - device^.pending[k].userdata := nil; - if completionCb <> nil then - completionCb(success, completionData); - end; - end; - end; - - { Acknowledge port interrupt by writing back the read value } - port^.int_status := port_is; + for slot := 0 to 31 do if (mask and (uint32(1) shl slot)) <> 0 then begin + if device^.pending[slot].inUse and device^.pending[slot].issued then begin + cb := device^.pending[slot].completion; data := device^.pending[slot].userdata; + device^.pending[slot].inUse := false; device^.pending[slot].issued := false; + device^.pending[slot].completion := nil; device^.pending[slot].userdata := nil; + if cb <> nil then cb(error,data); end; - - { Acknowledge controller interrupt } - controller^.mio^.int_status := ctrl_is; end; end; -function find_cmd_slot(device : PAHCI_Device) : uint32; -var - i : uint32; - active : uint32; - slotCount : uint32; +procedure fail_controller(controller : PAHCI_Controller; error : TError); +var j, command : uint32; pci : PPCI_Device; device : PAHCI_Device; begin - active := device^.port^.cmd_issue or device^.port^.sata_active; - slotCount := device^.slotCount; - if slotCount = 0 then slotCount := 32; - if slotCount > 32 then slotCount := 32; - if slotCount > 0 then - for i := 0 to slotCount - 1 do begin - if ((active shr i) and 1 = 0) and (not device^.pending[i].inUse) then begin - find_cmd_slot := i; + if controller = nil then exit; + controller^.offline := true; + pci := controller^.pci_device; + { Last resort: fence DMA for the whole controller before returning buffers. + Write only PCI command bits, not the adjacent write-one-to-clear status. } + driver.bus.pci.requestConfig(pci^.bus,pci^.slot,pci^.func,1); + command := inl($CFC) and $FFFF; + driver.bus.pci.writeConfig(pci^.bus,pci^.slot,pci^.func,1,command and not uint32(4)); + driver.bus.pci.requestConfig(pci^.bus,pci^.slot,pci^.func,1); + command := inl($CFC); + if (command <> $FFFFFFFF) and ((command and 4) <> 0) then + core.panic.panic('AHCI DMA cannot be stopped','PCI bus mastering could not be disabled',nil); + controller^.mio^.global_ctrl := controller^.mio^.global_ctrl and not uint32(2); + { Reject all new work before any callback drains a queued request. } + for j := 0 to 31 do controller^.devices[j].offline := true; + for j := 0 to 31 do begin + device := @controller^.devices[j]; + device^.recovering := false; + if device^.port <> nil then begin + device^.port^.int_enable := 0; + device^.port^.cmd := device^.port^.cmd and not uint32($11); + end; + finish_slots(device,$FFFFFFFF,error); + end; + io.syslog.logln('AHCI','Controller taken offline after DMA stop timeout.'); +end; + +procedure begin_recovery(device : PAHCI_Device; error : TError); +begin + if device^.recovering or device^.offline then exit; + device^.offline := true; device^.recovering := true; + device^.recoveryTick := now_tick; device^.recoveryError := error; + device^.port^.int_enable := 0; + device^.port^.cmd := device^.port^.cmd and not uint32(1); + io.syslog.logln('AHCI','I/O failed or timed out; stopping port before releasing DMA buffers.'); +end; + +{ Nonblocking; called by IRQ and the timer watchdog with interrupts disabled. } +procedure service_port(device : PAHCI_Device; now, status : uint32); +var active, doneMask, pendingMask, slot : uint32; expired : boolean; +begin + if device^.port = nil then exit; + if device^.recovering then begin + if (puint32(@device^.port^.cmd)^ and $8000) = 0 then begin + device^.port^.cmd := device^.port^.cmd and not uint32($10); + if (puint32(@device^.port^.cmd)^ and $4000) = 0 then begin + device^.recovering := false; + device^.port^.int_status := $FFFFFFFF; + finish_slots(device,$FFFFFFFF,device^.recoveryError); exit; end; end; - { All usable slots busy } - find_cmd_slot := $FFFFFFFF; + if uint32(now-device^.recoveryTick) >= AHCI_STOP_TICKS then + fail_controller(PAHCI_Controller(device^.owner),device^.recoveryError); + exit; + end; + if device^.offline then exit; + active := puint32(@device^.port^.cmd_issue)^ or puint32(@device^.port^.sata_active)^; + doneMask := 0; pendingMask := 0; expired := false; + for slot := 0 to 31 do if device^.pending[slot].inUse and device^.pending[slot].issued then begin + pendingMask := pendingMask or (uint32(1) shl slot); + if (active and (uint32(1) shl slot)) = 0 then doneMask := doneMask or (uint32(1) shl slot) + else if uint32(now-device^.pending[slot].started) >= AHCI_IO_TICKS then expired := true; + end; + if pendingMask = 0 then exit; { polling IDENTIFY owns its own recovery } + if (status and AHCI_FATAL_MASK) <> 0 then begin + begin_recovery(device,eIOError); exit; + end; + if (doneMask <> 0) and ((device^.port^.tfd and $1) <> 0) then begin + begin_recovery(device,eIOError); exit; + end; + if expired then begin begin_recovery(device,eIOTimeout); exit; end; + finish_slots(device,doneMask,eNone); end; -{ Shared completion callback used by all sync wrappers. - userdata points to a stack-local boolean; sets it true so the spin loop exits. - Called from IRQ context \u2014 must stay short. } -procedure sync_completion(success : boolean; userdata : puint32); +procedure ahci_isr(); +var i,j,ctrlIS,portIS : uint32; controller : PAHCI_Controller; device : PAHCI_Device; begin - userdata^ := 1; + if (ahciControllers = nil) or (DL_Size(ahciControllers) = 0) then exit; + for i := 0 to DL_Size(ahciControllers)-1 do begin + controller := PAHCI_Controller(puint32(DL_Get(ahciControllers,i))^); + if controller^.offline then continue; + ctrlIS := controller^.mio^.int_status; + if ctrlIS = 0 then continue; + { Acknowledge before callbacks can submit a new command/interrupt. } + for j := 0 to 31 do if (ctrlIS and (uint32(1) shl j)) <> 0 then begin + device := @controller^.devices[j]; + if device^.port = nil then continue; + portIS := device^.port^.int_status; + device^.port^.int_status := portIS; + controller^.mio^.int_status := uint32(1) shl j; + service_port(device,now_tick,portIS); + end; + end; end; -{ ---- Async send functions ---- } +procedure ahci_watchdog(data : void); +var i,j,now : uint32; controller : PAHCI_Controller; +begin + Inc(watchdogDivider); if watchdogDivider < 80 then exit; watchdogDivider := 0; + if (ahciControllers = nil) or (DL_Size(ahciControllers) = 0) then exit; + now := now_tick; + for i := 0 to DL_Size(ahciControllers)-1 do begin + controller := PAHCI_Controller(puint32(DL_Get(ahciControllers,i))^); + if controller^.offline then continue; + for j := 0 to 31 do + if controller^.devices[j].slotCount <> 0 then service_port(@controller^.devices[j],now,controller^.devices[j].port^.int_status); + end; +end; + +{ Atomic reserve: software ownership precedes descriptor construction. } +function find_cmd_slot(device : PAHCI_Device) : uint32; +var flags, slot, active, count : uint32; +begin + find_cmd_slot := $FFFFFFFF; + if (device = nil) or (device^.port = nil) then exit; + asm pushfd; cli; pop flags end; + if not device^.offline then begin + active := device^.port^.cmd_issue or device^.port^.sata_active; + count := device^.slotCount; if count > 32 then count := 32; + if not device^.supportsNCQ then if count > 1 then count := 1; + if count > 0 then for slot := 0 to count-1 do begin + if ((active and (uint32(1) shl slot)) = 0) and (not device^.pending[slot].inUse) then begin + device^.pending[slot].inUse := true; device^.pending[slot].issued := false; + find_cmd_slot := slot; break; + end; + end; + end; + if (flags and $200) <> 0 then asm sti end; +end; + +{ Split at the kernel's 4 MiB virtual mapping boundaries. Every segment gets + its own physical translation, including buffers with an unaligned start. } +function build_prdt(table : PHBA_CMD_TABLE; address, bytes : uint32; var entries : uint32) : TError; +var remaining, chunk : uint32; +begin + build_prdt := eInvalidArgument; entries := 0; + if (bytes = 0) or ((address and 1) <> 0) or ((bytes and 1) <> 0) or (bytes > $FFFFFFFF-address) then exit; + remaining := bytes; + while remaining > 0 do begin + if entries = 32 then exit; + chunk := $400000-(address and $3FFFFF); if chunk > remaining then chunk := remaining; + table^.prdt[entries].dba := vtop(address); table^.prdt[entries].dbau := 0; + table^.prdt[entries].rsv0 := 0; table^.prdt[entries].reserved := 0; + table^.prdt[entries].dbc := chunk-1; table^.prdt[entries].int := 0; + Inc(entries); address := address+chunk; remaining := remaining-chunk; + end; + table^.prdt[entries-1].int := 1; + build_prdt := eNone; +end; + +{ mode: 0 SATA read, 1 SATA write, 2 ATAPI READ(12), 3 READ CAPACITY. } +function submit_command(device : PAHCI_Device; lba : uint64; bytes : uint32; buffer : puint32; + mode : uint32; completion : TIOCompletion; userdata : puint32) : TError; +var slot, entries, sectors, flags : uint32; header : PHBA_CMD_HEADER; table : PHBA_CMD_TABLE; + fis : PHBA_FIS_REG_H2D; err : TError; +begin + submit_command := eInvalidArgument; + if (device = nil) or (buffer = nil) or (bytes = 0) then exit; + if (device^.port = nil) or (device^.command_list = nil) or (device^.command_table = nil) then exit; + if device^.offline then begin submit_command := eDeviceNotReady; exit; end; + if mode < 2 then begin + if ((bytes mod 512) <> 0) or (bytes > 33554432) or (lba > $FFFFFFFFFFFF) then exit; + sectors := bytes div 512; + if uint64(sectors-1) > $FFFFFFFFFFFF-lba then exit; + end else if mode = 2 then begin + if ((bytes mod 2048) <> 0) or (lba > $FFFFFFFF) then exit; + sectors := bytes div 2048; + if uint64(sectors-1) > $FFFFFFFF-lba then exit; + end else begin + if (mode <> 3) or (bytes <> 8) then exit; + sectors := 0; + end; + slot := find_cmd_slot(device); + if slot = $FFFFFFFF then begin submit_command := eNoFreeSlot; exit; end; + header := @device^.command_list[slot]; table := @device^.command_table[slot]; + { Preserve the command-table pointer but reset every per-command header flag. } + puint32(header)^ := 0; header^.prdbc := 0; + header^.cmd_fis_length := sizeof(THBA_FIS_REG_H2D) div 4; + if mode = 1 then header^.wrt := 1; + if mode >= 2 then header^.atapi := 1; + memset(uint32(table),0,128); + err := build_prdt(table,uint32(buffer),bytes,entries); + if err = eNone then begin + header^.prdtl := entries; fis := PHBA_FIS_REG_H2D(@table^.cmd_fis); + fis^.fis_type := uint8(FIS_TYPE_REG_H2D); fis^.c := 1; + if mode < 2 then begin + fis^.device := $40; + fis^.lba0 := lba and $FF; fis^.lba1 := (lba shr 8) and $FF; + fis^.lba2 := (lba shr 16) and $FF; fis^.lba3 := (lba shr 24) and $FF; + fis^.lba4 := (lba shr 32) and $FF; fis^.lba5 := (lba shr 40) and $FF; + if device^.supportsNCQ then begin + if mode = 0 then fis^.command := ATA_CMD_READ_FPDMA_QUEUED else fis^.command := ATA_CMD_WRITE_FPDMA_QUEUED; + fis^.featurel := sectors and $FF; fis^.featureh := (sectors shr 8) and $FF; + fis^.countl := slot shl 3; + end else begin + if mode = 0 then fis^.command := ATA_CMD_READ_DMA_EXT else fis^.command := ATA_CMD_WRITE_DMA_EXT; + fis^.countl := sectors and $FF; fis^.counth := (sectors shr 8) and $FF; + end; + end else begin + fis^.command := ATA_CMD_PACKET; fis^.device := $A0; fis^.featurel := 1; + if mode = 3 then table^.acmd[0] := $25 + else begin + table^.acmd[0] := $A8; + table^.acmd[2] := (lba shr 24) and $FF; table^.acmd[3] := (lba shr 16) and $FF; + table^.acmd[4] := (lba shr 8) and $FF; table^.acmd[5] := lba and $FF; + table^.acmd[6] := (sectors shr 24) and $FF; table^.acmd[7] := (sectors shr 16) and $FF; + table^.acmd[8] := (sectors shr 8) and $FF; table^.acmd[9] := sectors and $FF; + end; + end; + end; + asm pushfd; cli; pop flags end; + if err = eNone then begin + if device^.offline then err := eDeviceNotReady + else if (device^.port^.tfd and $88) <> 0 then err := eDeviceNotReady; + end; + if err = eNone then begin + device^.pending[slot].completion := completion; device^.pending[slot].userdata := userdata; + device^.pending[slot].started := now_tick; device^.pending[slot].issued := true; + { RW1 registers: never write a stale snapshot of other command bits. } + if (mode < 2) and device^.supportsNCQ then device^.port^.sata_active := uint32(1) shl slot; + device^.port^.cmd_issue := uint32(1) shl slot; + end else device^.pending[slot].inUse := false; + if (flags and $200) <> 0 then asm sti end; + submit_command := err; +end; function send_read_dma_async(device : PAHCI_Device; lba : uint64; count : uint32; buffer : puint32; completion : TIOCompletion; userdata : puint32) : TError; -var - fis : PHBA_FIS_REG_H2D; - cmd_header : PHBA_CMD_HEADER; - cmd_table : PHBA_CMD_TABLE; - i : uint32; - timeout : uint32; - sec_count : uint32; - prdt_count : uint32; - slot : uint32; -begin - send_read_dma_async := eIOError; - - prdt_count := (count div 4194304) + 1; - if prdt_count > 32 then exit; - - slot := find_cmd_slot(device); - if slot = $FFFFFFFF then begin - send_read_dma_async := eNoFreeSlot; - exit; - end; - - cmd_header := PHBA_CMD_HEADER(uint32(device^.command_list) + (slot * sizeof(THBA_CMD_HEADER))); - cmd_header^.cmd_fis_length := sizeof(THBA_FIS_REG_H2D) div sizeof(uint32); - cmd_header^.wrt := 0; - cmd_header^.prdtl := prdt_count; - cmd_header^.clear_busy := 1; - - cmd_table := PHBA_CMD_TABLE(uint32(device^.command_table) + (slot * sizeof(THBA_CMD_TABLE))); - - for i := 0 to prdt_count - 1 do begin - cmd_table^.prdt[i].dba := vtop(uint32(buffer) + (i * 4194304)); - if i = prdt_count - 1 then - cmd_table^.prdt[i].dbc := count - (i * 4194304) - 1 - else - cmd_table^.prdt[i].dbc := 4194304 - 1; - cmd_table^.prdt[i].int := 1; - end; - - fis := PHBA_FIS_REG_H2D(@cmd_table^.cmd_fis); - memset(uint32(fis), 0, sizeof(THBA_FIS_REG_H2D)); - fis^.fis_type := uint8(FIS_TYPE_REG_H2D); - fis^.c := $1; - fis^.device := $40; - - fis^.lba0 := lba and $FF; - fis^.lba1 := (lba shr 8) and $FF; - fis^.lba2 := (lba shr 16) and $FF; - fis^.lba3 := (lba shr 24) and $FF; - fis^.lba4 := (lba shr 32) and $FF; - fis^.lba5 := (lba shr 40) and $FF; - - sec_count := count div 512; - if sec_count = 0 then sec_count := 1; - if device^.supportsNCQ then begin - fis^.command := ATA_CMD_READ_FPDMA_QUEUED; - fis^.featurel := sec_count and $FF; - fis^.featureh := (sec_count shr 8) and $FF; - fis^.countl := (slot and $1F) shl 3; - fis^.counth := 0; - end else begin - fis^.command := ATA_CMD_READ_DMA_EXT; - fis^.countl := sec_count and $FF; - fis^.counth := (sec_count shr 8) and $FF; - end; - - { Wait for TFD BSY+DRQ to clear } - timeout := 0; - while (device^.port^.tfd and $88) <> 0 do begin - timeout := timeout + 1; - if timeout > 100000 then begin - io.syslog.writestringln('driver.storage.ctl.ahci: Timeout waiting for port ready (async read)'); - send_read_dma_async := eIOTimeout; - exit; - end; - end; - - { Record the pending operation before issuing (ISR may fire immediately) } - device^.pending[slot].inUse := true; - device^.pending[slot].completion := completion; - device^.pending[slot].userdata := userdata; - - if device^.supportsNCQ then - device^.port^.sata_active := device^.port^.sata_active or (1 shl slot); - - { Issue command \u2014 returns immediately } - device^.port^.cmd_issue := device^.port^.cmd_issue or (1 shl slot); - - send_read_dma_async := eNone; -end; - - +begin send_read_dma_async := submit_command(device,lba,count,buffer,0,completion,userdata); end; function send_write_dma_async(device : PAHCI_Device; lba : uint64; count : uint32; buffer : puint32; completion : TIOCompletion; userdata : puint32) : TError; -var - fis : PHBA_FIS_REG_H2D; - cmd_header : PHBA_CMD_HEADER; - cmd_table : PHBA_CMD_TABLE; - i : uint32; - timeout : uint32; - sec_count : uint32; - prdt_count : uint32; - slot : uint32; -begin - send_write_dma_async := eIOError; - - prdt_count := (count div 4194304) + 1; - if prdt_count > 32 then exit; - - slot := find_cmd_slot(device); - if slot = $FFFFFFFF then begin - send_write_dma_async := eNoFreeSlot; - exit; - end; - - cmd_header := PHBA_CMD_HEADER(uint32(device^.command_list) + (slot * sizeof(THBA_CMD_HEADER))); - cmd_header^.cmd_fis_length := sizeof(THBA_FIS_REG_H2D) div sizeof(uint32); - cmd_header^.wrt := 1; - cmd_header^.prdtl := prdt_count; - cmd_header^.clear_busy := 1; - - cmd_table := PHBA_CMD_TABLE(uint32(device^.command_table) + (slot * sizeof(THBA_CMD_TABLE))); - - for i := 0 to prdt_count - 1 do begin - cmd_table^.prdt[i].dba := vtop(uint32(buffer) + (i * 4194304)); - if i = prdt_count - 1 then - cmd_table^.prdt[i].dbc := count - (i * 4194304) - 1 - else - cmd_table^.prdt[i].dbc := 4194304 - 1; - cmd_table^.prdt[i].int := 1; - end; - - fis := PHBA_FIS_REG_H2D(@cmd_table^.cmd_fis); - memset(uint32(fis), 0, sizeof(THBA_FIS_REG_H2D)); - fis^.fis_type := uint8(FIS_TYPE_REG_H2D); - fis^.c := $1; - fis^.device := $40; - - fis^.lba0 := lba and $FF; - fis^.lba1 := (lba shr 8) and $FF; - fis^.lba2 := (lba shr 16) and $FF; - fis^.lba3 := (lba shr 24) and $FF; - fis^.lba4 := (lba shr 32) and $FF; - fis^.lba5 := (lba shr 40) and $FF; - - sec_count := count div 512; - if sec_count = 0 then sec_count := 1; - if device^.supportsNCQ then begin - fis^.command := ATA_CMD_WRITE_FPDMA_QUEUED; - fis^.featurel := sec_count and $FF; - fis^.featureh := (sec_count shr 8) and $FF; - fis^.countl := (slot and $1F) shl 3; - fis^.counth := 0; - end else begin - fis^.command := ATA_CMD_WRITE_DMA_EXT; - fis^.countl := sec_count and $FF; - fis^.counth := (sec_count shr 8) and $FF; - end; - - { Wait for TFD BSY+DRQ to clear } - timeout := 0; - while (device^.port^.tfd and $88) <> 0 do begin - timeout := timeout + 1; - if timeout > 100000 then begin - io.syslog.writestringln('driver.storage.ctl.ahci: Timeout waiting for port ready (async write)'); - send_write_dma_async := eIOTimeout; - exit; - end; - end; - - { Record the pending operation before issuing } - device^.pending[slot].inUse := true; - device^.pending[slot].completion := completion; - device^.pending[slot].userdata := userdata; - - if device^.supportsNCQ then - device^.port^.sata_active := device^.port^.sata_active or (1 shl slot); - - { Issue command \u2014 returns immediately } - device^.port^.cmd_issue := device^.port^.cmd_issue or (1 shl slot); - - send_write_dma_async := eNone; -end; - -//read atapi +begin send_write_dma_async := submit_command(device,lba,count,buffer,1,completion,userdata); end; function read_atapi_async(device : PAHCI_Device; lba : uint64; count : uint32; buffer : puint32; completion : TIOCompletion; userdata : puint32) : TError; -var - fis : PHBA_FIS_REG_H2D; - cmd_header : PHBA_CMD_HEADER; - cmd_table : PHBA_CMD_TABLE; - timeout : uint32; - sec_count : uint32; - prdt_count : uint32; - slot : uint32; -begin - read_atapi_async := eIOError; +begin read_atapi_async := submit_command(device,lba,count,buffer,2,completion,userdata); end; - prdt_count := (count div 4194304) + 1; - if prdt_count > 32 then exit; +type TCapacityWait = record Done : uint32; Error : TError; end; + PCapacityWait = ^TCapacityWait; +procedure capacity_done(error : TError; userdata : puint32); +var state : PCapacityWait; +begin state := PCapacityWait(userdata); state^.Error := error; puint32(@state^.Done)^ := 1; end; - slot := find_cmd_slot(device); - if slot = $FFFFFFFF then begin - read_atapi_async := eNoFreeSlot; - exit; - end; - - cmd_header := PHBA_CMD_HEADER(uint32(device^.command_list) + (slot * sizeof(THBA_CMD_HEADER))); - cmd_header^.cmd_fis_length := sizeof(THBA_FIS_REG_H2D) div sizeof(uint32); - cmd_header^.wrt := 0; - cmd_header^.prdtl := 1; - cmd_header^.atapi := 1; - cmd_header^.clear_busy := 1; - - cmd_table := PHBA_CMD_TABLE(uint32(device^.command_table) + (slot * sizeof(THBA_CMD_TABLE))); - cmd_table^.prdt[0].dba := vtop(uint32(buffer)); - cmd_table^.prdt[0].dbc := count - 1; - cmd_table^.prdt[0].int := 1; - - fis := PHBA_FIS_REG_H2D(@cmd_table^.cmd_fis); - memset(uint32(fis), 0, sizeof(THBA_FIS_REG_H2D)); - fis^.fis_type := uint8(FIS_TYPE_REG_H2D); - fis^.c := $1; - fis^.command := ATA_CMD_PACKET; - fis^.device := $A0; - fis^.featurel := fis^.featurel or 1; - fis^.featurel := fis^.featurel or (1 shl 2); - - { SCSI READ(12) CDB } - memset(uint32(@cmd_table^.acmd[0]), 0, 16); - cmd_table^.acmd[0] := $A8; - - cmd_table^.acmd[2] := uInt8((lba shr 24) and $FF); - cmd_table^.acmd[3] := uInt8((lba shr 16) and $FF); - cmd_table^.acmd[4] := uInt8((lba shr 8) and $FF); - cmd_table^.acmd[5] := uInt8(lba and $FF); - - sec_count := count div 2048; - if sec_count = 0 then sec_count := 1; - cmd_table^.acmd[6] := uInt8((sec_count shr 24) and $FF); - cmd_table^.acmd[7] := uInt8((sec_count shr 16) and $FF); - cmd_table^.acmd[8] := uInt8((sec_count shr 8) and $FF); - cmd_table^.acmd[9] := uInt8(sec_count and $FF); - - { Wait for TFD BSY+DRQ to clear } - timeout := 0; - while (device^.port^.tfd and $88) <> 0 do begin - timeout := timeout + 1; - if timeout > 100000 then begin - io.syslog.writestringln('driver.storage.ctl.ahci: Timeout waiting for port ready (async ATAPI)'); - read_atapi_async := eIOTimeout; - exit; - end; - end; - - { Record the pending operation before issuing } - device^.pending[slot].inUse := true; - device^.pending[slot].completion := completion; - device^.pending[slot].userdata := userdata; - - { Issue command \u2014 returns immediately } - device^.port^.cmd_issue := device^.port^.cmd_issue or (1 shl slot); - - read_atapi_async := eNone; -end; - -{ Send a SCSI READ CAPACITY (10) command to an ATAPI device via AHCI. - Returns the total sector count and block size (typically 2048 for CD-ROM). } function send_read_capacity(device : PAHCI_Device; sectorCount : puint32; blockSize : puint32) : boolean; -var - fis : PHBA_FIS_REG_H2D; - cmd_header : PHBA_CMD_HEADER; - cmd_table : PHBA_CMD_TABLE; - cmd : uint32; - timeout : uint32; - tfd : uint32; - buffer : puint32; - slot : uint32; - rawLBA : uint32; - rawBlkSz : uint32; +var buffer : puint32; state : TCapacityWait; rawLBA, rawBlkSz : uint32; err : TError; begin - send_read_capacity := false; - sectorCount^ := 0; - blockSize^ := 0; - - { Allocate an 8-byte buffer for the READ CAPACITY response } - buffer := kalloc(512); - memset(uint32(buffer), 0, 512); - - slot := find_cmd_slot(device); - if slot = $FFFFFFFF then begin - kfree(buffer); - exit; + send_read_capacity := false; sectorCount^ := 0; blockSize^ := 0; + if not INTE then exit; { init/task context only } + buffer := kalloc(512); if buffer = nil then exit; + memset(uint32(buffer),0,512); state.Done := 0; state.Error := eNone; + err := submit_command(device,0,8,buffer,3,@capacity_done,puint32(@state)); + if err = eNone then begin + { The watchdog ends ownership only after stopping DMA on error/timeout. } + while puint32(@state.Done)^ = 0 do asm hlt end; + err := state.Error; end; - cmd_header := PHBA_CMD_HEADER(uint32(device^.command_list) + (slot * sizeof(THBA_CMD_HEADER))); - cmd_header^.cmd_fis_length := sizeof(THBA_FIS_REG_H2D) div sizeof(uint32); - cmd_header^.wrt := 0; - cmd_header^.prdtl := 1; - cmd_header^.atapi := 1; - cmd_header^.clear_busy := 1; - - cmd_table := PHBA_CMD_TABLE(uint32(device^.command_table) + (slot * sizeof(THBA_CMD_TABLE))); - cmd_table^.prdt[0].dba := vtop(uint32(buffer)); - cmd_table^.prdt[0].dbc := 8 - 1; { 8 bytes response } - cmd_table^.prdt[0].int := 1; - - { Build the ATA PACKET command FIS } - fis := PHBA_FIS_REG_H2D(@cmd_table^.cmd_fis); - memset(uint32(fis), 0, sizeof(THBA_FIS_REG_H2D)); - fis^.fis_type := uint8(FIS_TYPE_REG_H2D); - fis^.c := $1; - fis^.command := ATA_CMD_PACKET; - fis^.device := $A0; - fis^.featurel := fis^.featurel or 1; - fis^.featurel := fis^.featurel or (1 shl 2); - - { Build the 12-byte SCSI READ CAPACITY(10) CDB } - memset(uint32(@cmd_table^.acmd[0]), 0, 16); - cmd_table^.acmd[0] := $25; { READ CAPACITY(10) opcode } - - { Wait for port ready } - timeout := 0; - while (device^.port^.tfd and $88) <> 0 do begin - timeout := timeout + 1; - if timeout > 100000 then begin - io.syslog.writestringln('driver.storage.ctl.ahci: Timeout waiting for port ready (READ CAPACITY)'); - kfree(buffer); - exit; + if err = eNone then begin + rawLBA := buffer[0]; rawBlkSz := buffer[1]; + rawLBA := ((rawLBA and $FF) shl 24) or ((rawLBA and $FF00) shl 8) or + ((rawLBA and $FF0000) shr 8) or ((rawLBA and $FF000000) shr 24); + rawBlkSz := ((rawBlkSz and $FF) shl 24) or ((rawBlkSz and $FF00) shl 8) or + ((rawBlkSz and $FF0000) shr 8) or ((rawBlkSz and $FF000000) shr 24); + if (rawLBA <> $FFFFFFFF) and (rawBlkSz = 2048) then begin + sectorCount^ := rawLBA+1; blockSize^ := rawBlkSz; send_read_capacity := true; end; end; - - { Issue the command } - device^.port^.cmd_issue := device^.port^.cmd_issue or (1 shl slot); - - { Wait for completion } - timeout := 0; - repeat - tfd := device^.port^.tfd; - timeout := timeout + 1; - if timeout > 100 then begin - io.syslog.writestringln('driver.storage.ctl.ahci: READ CAPACITY command timeout'); - kfree(buffer); - exit; - end; - if (tfd and $1) <> 0 then break; { ERR bit set } - psleep(1); - until ((device^.port^.cmd_issue and (1 shl slot)) = 0); - - if (device^.port^.cmd_issue and (1 shl slot)) <> 0 then begin - io.syslog.writestringln('driver.storage.ctl.ahci: Error sending READ CAPACITY command'); - io.syslog.writestring('driver.storage.ctl.ahci: Error register: '); - io.syslog.writehexln(device^.port^.sata_error); - kfree(buffer); - exit; - end; - - { Parse the 8-byte response: bytes 0-3 = last LBA (big-endian), bytes 4-7 = block size (big-endian) } - rawLBA := puint32(buffer)[0]; - rawBlkSz := puint32(buffer)[1]; - - { Byte-swap from big-endian to little-endian } - sectorCount^ := ((rawLBA and $FF) shl 24) or - ((rawLBA and $FF00) shl 8) or - ((rawLBA and $FF0000) shr 8) or - ((rawLBA and $FF000000) shr 24); - sectorCount^ := sectorCount^ + 1; { Last LBA + 1 = total sectors } - - blockSize^ := ((rawBlkSz and $FF) shl 24) or - ((rawBlkSz and $FF00) shl 8) or - ((rawBlkSz and $FF0000) shr 8) or - ((rawBlkSz and $FF000000) shr 24); - kfree(buffer); - send_read_capacity := true; end; { ---------- StorageManager async callback wrappers ---------- } @@ -1021,17 +841,12 @@ end; { ISR-safe completion — bridges from per-slot TIOCompletion callback to driver.storage.mgr.complete_io. userdata is the PIORequest pointer. } -procedure ahci_io_completion(success : boolean; userdata : puint32); -var - request : PIORequest; - err : TError; +procedure ahci_io_completion(error : TError; userdata : puint32); +var request : PIORequest; begin request := PIORequest(userdata); - if success then - err := eNone - else - err := eIOError; - driver.storage.mgr.complete_io(request, success, err); + if error = eNone then request^.ByteCount := request^.SectorCount*request^.Device^.sectorSize; + driver.storage.mgr.complete_io(request,error = eNone,error); end; { TDriverDispatch — SATA read via DMA } @@ -1040,6 +855,12 @@ var ahciDev : PAHCI_Device; err : TError; begin + if (device^.sectorSize = 0) or (request^.SectorCount = 0) or + (request^.SectorCount > $FFFFFFFF div device^.sectorSize) or + (request^.LBA >= device^.maxSectorCount) or + (request^.SectorCount > device^.maxSectorCount-request^.LBA) then begin + driver.storage.mgr.complete_io(request,false,eInvalidArgument); exit; + end; ahciDev := PAHCI_Device(device^.controllerId0); err := send_read_dma_async(ahciDev, request^.LBA, request^.SectorCount * device^.sectorSize, @@ -1055,6 +876,12 @@ var ahciDev : PAHCI_Device; err : TError; begin + if (device^.sectorSize = 0) or (request^.SectorCount = 0) or + (request^.SectorCount > $FFFFFFFF div device^.sectorSize) or + (request^.LBA >= device^.maxSectorCount) or + (request^.SectorCount > device^.maxSectorCount-request^.LBA) then begin + driver.storage.mgr.complete_io(request,false,eInvalidArgument); exit; + end; ahciDev := PAHCI_Device(device^.controllerId0); err := send_write_dma_async(ahciDev, request^.LBA, request^.SectorCount * device^.sectorSize, @@ -1070,6 +897,12 @@ var ahciDev : PAHCI_Device; err : TError; begin + if (device^.sectorSize = 0) or (request^.SectorCount = 0) or + (request^.SectorCount > $FFFFFFFF div device^.sectorSize) or + (request^.LBA >= device^.maxSectorCount) or + (request^.SectorCount > device^.maxSectorCount-request^.LBA) then begin + driver.storage.mgr.complete_io(request,false,eInvalidArgument); exit; + end; ahciDev := PAHCI_Device(device^.controllerId0); err := read_atapi_async(ahciDev, request^.LBA, request^.SectorCount * device^.sectorSize, @@ -1079,6 +912,116 @@ begin driver.storage.mgr.complete_io(request, false, err); end; + +{ Synthetic MMIO only: these tests never issue I/O to a real disk. } +type TTestCompletion = record Count : uint32; Error : TError; end; + PTestCompletion = ^TTestCompletion; +procedure test_done(error : TError; data : puint32); +begin + Inc(PTestCompletion(data)^.Count); PTestCompletion(data)^.Error := error; +end; + +procedure run_ahci_tests(stdout_buf : POutBuf); +var device : PAHCI_Device; port : PHBA_Port; table : PHBA_CMD_TABLE; + buffer : pointer; aligned, entries, first, second, passed, failed : uint32; + result : TError; completed : TTestCompletion; msg, number : pchar; + procedure Assert(ok : boolean; name : pchar); + begin + if ok then Inc(passed) else begin + Inc(failed); io.syslog.log('AHCITEST','FAIL: '); io.syslog.writestringln(name); + if stdout_buf <> nil then begin + io.stdio.bufWriteStr(stdout_buf,'FAIL: '); io.stdio.bufWriteStrLn(stdout_buf,name); + end; + end; + end; + procedure reset_fake; + begin + memset(uint32(port),0,sizeof(THBA_Port)); + memset(uint32(@device^.pending),0,sizeof(device^.pending)); + device^.offline := false; device^.recovering := false; + device^.supportsNCQ := true; completed.Count := 0; completed.Error := eNone; + end; +begin + passed := 0; failed := 0; + device := PAHCI_Device(kalloc(sizeof(TAHCI_Device))); + port := PHBA_Port(kalloc(sizeof(THBA_Port))); device^.port := port; + device^.command_list := PHBA_CMD_HEADER(kalloc(32*sizeof(THBA_CMD_HEADER))); + device^.command_table := PHBA_CMD_TABLE(kalloc(32*sizeof(THBA_CMD_TABLE))); + device^.slotCount := 32; table := device^.command_table; + buffer := kalloc($800000); aligned := (uint32(buffer)+$3FFFFF) and $FFC00000; + reset_fake; + Assert(build_prdt(table,aligned,$400000,entries)=eNone,'exact 4 MiB accepted'); + Assert((entries=1) and (table^.prdt[0].dbc=$3FFFFF),'exact 4 MiB has one nonempty PRD'); + Assert(build_prdt(table,aligned-512,1024,entries)=eNone,'cross-page transfer accepted'); + Assert((entries=2) and (table^.prdt[0].dbc=511) and (table^.prdt[1].dbc=511),'cross-page segments bounded'); + Assert((table^.prdt[0].dba=vtop(aligned-512)) and (table^.prdt[1].dba=vtop(aligned)),'each page translated independently'); + Assert((table^.prdt[0].int=0) and (table^.prdt[1].int=1),'only final PRD interrupts'); + Assert(build_prdt(table,aligned,0,entries)=eInvalidArgument,'zero length rejected'); + Assert(build_prdt(table,aligned+1,512,entries)=eInvalidArgument,'odd address rejected'); + Assert(build_prdt(table,aligned,513,entries)=eInvalidArgument,'odd length rejected'); + Assert(build_prdt(table,$FFFFFF00,512,entries)=eInvalidArgument,'address wrap rejected'); + first := find_cmd_slot(device); second := find_cmd_slot(device); + Assert((first=0) and (second=1),'reservation excludes concurrent builder'); + service_port(device,now_tick,AHCI_FATAL_MASK); + Assert(device^.pending[0].inUse and not device^.offline,'unissued reservation not completed or recovered'); + reset_fake; device^.supportsNCQ := false; + first := find_cmd_slot(device); second := find_cmd_slot(device); + Assert((first=0) and (second=$FFFFFFFF),'non-NCQ serializes commands'); + reset_fake; port^.cmd_issue := $20; port^.sata_active := $20; + result := send_read_dma_async(device,0,512,puint32(buffer),@test_done,puint32(@completed)); + Assert(result=eNone,'valid read accepted'); + Assert((port^.cmd_issue=1) and (port^.sata_active=1),'issue writes only newly set bit'); + Assert(device^.pending[0].issued,'ownership published before issuing'); + port^.cmd_issue := 0; port^.sata_active := 0; + service_port(device,now_tick,0); service_port(device,now_tick,0); + Assert((completed.Count=1) and (completed.Error=eNone),'successful completion exactly once'); + reset_fake; + Assert(send_read_dma_async(device,0,513,puint32(buffer),nil,nil)=eInvalidArgument,'partial sector rejected'); + Assert(send_read_dma_async(device,0,33554944,puint32(buffer),nil,nil)=eInvalidArgument,'ATA count overflow rejected'); + Assert(send_read_dma_async(device,$FFFFFFFFFFFF,1024,puint32(buffer),nil,nil)=eInvalidArgument,'ATA LBA wrap rejected'); + Assert(read_atapi_async(device,$FFFFFFFF,4096,puint32(buffer),nil,nil)=eInvalidArgument,'ATAPI LBA wrap rejected'); + Assert((port^.cmd_issue=0) and not device^.pending[0].inUse,'invalid requests do not acquire DMA ownership'); + port^.tfd := $80; + Assert(send_read_dma_async(device,0,512,puint32(buffer),nil,nil)=eDeviceNotReady,'busy device fails without spinning'); + Assert(not device^.pending[0].inUse,'rejected command releases reservation'); + reset_fake; + result := send_read_dma_async(device,0,512,puint32(buffer),@test_done,puint32(@completed)); + port^.cmd := $C011; + service_port(device,now_tick,$40000000); + Assert(device^.recovering and (completed.Count=0),'task-file error begins recovery without releasing buffer'); + service_port(device,device^.recoveryTick,0); + Assert((completed.Count=0) and device^.pending[0].inUse,'CR retains DMA ownership'); + port^.cmd := $4010; service_port(device,device^.recoveryTick,0); + Assert((completed.Count=0) and ((port^.cmd and $10)=0),'FR retains DMA ownership after FRE cleared'); + port^.cmd := 0; service_port(device,device^.recoveryTick,0); service_port(device,now_tick,0); + Assert((completed.Count=1) and (completed.Error=eIOError),'stopped error completes exactly once'); + Assert(send_read_dma_async(device,0,512,puint32(buffer),nil,nil)=eDeviceNotReady,'failed port rejects new commands'); + reset_fake; + result := submit_command(device,0,8,puint32(buffer),3,@test_done,puint32(@completed)); + Assert((result=eNone) and (table^.acmd[0]=$25),'READ CAPACITY uses tracked ownership'); + device^.pending[0].started := 0; port^.cmd := $C011; + service_port(device,AHCI_IO_TICKS,0); + Assert(device^.recovering and (completed.Count=0),'watchdog retains timed-out capacity buffer'); + port^.cmd := 0; service_port(device,device^.recoveryTick,0); + Assert((completed.Count=1) and (completed.Error=eIOTimeout),'timeout completes after DMA stop'); + port^.cmd := 1; + Assert(not wait_register(@port^.cmd,1,0,0),'stuck reset exits at bounded deadline'); + port^.cmd := 0; + Assert(wait_register(@port^.cmd,1,0,0),'cleared reset succeeds'); + kfree(buffer); kfree(void(device^.command_table)); kfree(void(device^.command_list)); kfree(void(port)); kfree(void(device)); + number := intToString(passed); msg := stringConcat(number,' passed, '); kfree(void(number)); + number := intToString(failed); buffer := msg; msg := stringConcat(msg,number); kfree(buffer); kfree(void(number)); + buffer := msg; msg := stringConcat(msg,' failed'); kfree(buffer); + io.syslog.logln('AHCITEST',msg); + if stdout_buf <> nil then io.stdio.bufWriteStrLn(stdout_buf,msg); + kfree(void(msg)); +end; + +procedure UnitTest; +begin run_ahci_tests(nil); end; +procedure cmd_ahcitest(params : PParamList; stdin_buf, stdout_buf, stderr_buf : POutBuf); +begin run_ahci_tests(stdout_buf); end; + Initialization boot.mgr.registerBoot('driver.storage.ctl.ahci', @init, 'AHCI Controller', 'driver.storage.fs.*'); diff --git a/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.types.pas b/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.types.pas index cc5d4645..54b5204d 100644 --- a/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.types.pas +++ b/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.types.pas @@ -21,6 +21,7 @@ unit driver.storage.ctl.ahci.types; interface uses io.syslog, + driver.storage.types, driver.mgr, driver.types, driver.storage.ctl.ide.types, @@ -251,11 +252,13 @@ type { Callback signature fired from IRQ context when an async I/O command completes. Must be very short — set a flag or post to a queue. Never call kalloc/kfree/VFS from within this callback. } - TIOCompletion = procedure(success : boolean; userdata : puint32); + TIOCompletion = procedure(error : TError; userdata : puint32); { Per-slot pending operation — one entry per HBA command slot } TPendingOp = record inUse : boolean; + issued : boolean; + started : uint32; completion : TIOCompletion; userdata : puint32; end; @@ -267,6 +270,11 @@ type } TAHCI_Device = record port : PHBA_Port; + owner : pointer; { stable PAHCI_Controller } + offline : boolean; + recovering : boolean; + recoveryTick : uint32; + recoveryError: TError; device_type : TDeviceType; ata_info : TIdentResponse; supportsNCQ : boolean; @@ -288,6 +296,7 @@ type } TAHCI_Controller = record pci_device : PPCI_Device; + offline : boolean; mio: PHBA_Memory; ata_info : TIdentResponse; devices : array[0..31] of TAHCI_Device; diff --git a/src/driver/storage/driver.storage.metatest.pas b/src/driver/storage/driver.storage.metatest.pas index 83219061..bd09ffe5 100644 --- a/src/driver/storage/driver.storage.metatest.pas +++ b/src/driver/storage/driver.storage.metatest.pas @@ -14,7 +14,7 @@ type PMock = ^TMock; TMock = record Data : puint8; - Writes, FailWrite, Callbacks : uint32; + Writes, FailWrite, Callbacks, Depth, MaxDepth : uint32; Error : TError; OutOfBounds, FailRead : boolean; end; @@ -27,7 +27,10 @@ begin (request^.SectorCount > device^.maxSectorCount-request^.LBA) then begin m^.OutOfBounds := true; complete_io(request, false, eInvalidArgument); exit; end; - if (request^.RequestType <> ioWrite) and m^.FailRead then begin complete_io(request, false, eIOError); exit; end; + if (request^.RequestType <> ioWrite) and m^.FailRead then begin + Inc(m^.Depth); if m^.Depth > m^.MaxDepth then m^.MaxDepth := m^.Depth; + complete_io(request,false,eIOError); Dec(m^.Depth); exit; + end; offset := request^.LBA*512; size := request^.SectorCount*512; if request^.RequestType = ioWrite then begin Inc(m^.Writes); @@ -81,7 +84,17 @@ begin device^.controllerId0 := uint32(mock); device^.sectorSize := 512; device^.maxSectorCount := 256; device^.writable := true; device^.maxActive := 1; device^.dispatchRead := @dispatch; device^.dispatchWrite := @dispatch; - device^.requestQueue := CFIFO_New(sizeof(Pointer),4); + device^.requestQueue := CFIFO_New(sizeof(Pointer),8); + device^.maxActive := 0; mock^.FailRead := true; + for i := 1 to 4 do + Assert(storage_read_async(device,0,1,puint32(mock^.Data),@completion,mock)=eNone,'queued fault request accepted'); + Assert(mock^.Callbacks=0,'queued requests wait for dispatch'); + device^.maxActive := 1; + Assert(storage_read_async(device,0,1,puint32(mock^.Data),@completion,mock)=eNone,'fault queue drain accepted'); + Assert((mock^.Callbacks=5) and (mock^.Error=eIOError),'every queued error completes once'); + Assert((mock^.MaxDepth=1) and (device^.activeCount=0) and (CFIFO_Size(device^.requestQueue)=0), + 'inline failures drain without recursive driver dispatch'); + mock^.FailRead := false; mock^.Callbacks := 0; device^.cachedMBR := kalloc(512); mbr := PMaster_Boot_Record(device^.cachedMBR); memset(uint32(mbr),0,512); mbr^.boot_sector := $AA55; mbr^.signature := $12345678; diff --git a/src/driver/storage/driver.storage.mgr.pas b/src/driver/storage/driver.storage.mgr.pas index 7c8ef025..4e1dcb4a 100644 --- a/src/driver/storage/driver.storage.mgr.pas +++ b/src/driver/storage/driver.storage.mgr.pas @@ -246,6 +246,7 @@ begin device^.removed := false; device^.requestQueue := CFIFO_New(SizeOf(PIORequest), 128); device^.activeCount := 0; + device^.dispatching := false; if device^.maxActive = 0 then device^.maxActive := 1; device^.cachedMBR := nil; device^.metadataBusy := false; @@ -416,6 +417,8 @@ procedure dispatch_next(device : PStorage_Device); var heapReq : PIORequest; begin + if device^.dispatching then exit; + device^.dispatching := true; { Dispatch queued requests up to maxActive hardware slots } while device^.activeCount < device^.maxActive do begin if not CFIFO_Dequeue(device^.requestQueue, @heapReq) then break; { queue empty } @@ -441,12 +444,14 @@ begin end; end; end; + device^.dispatching := false; end; function submit_io(request : PIORequest) : TError; var device : PStorage_Device; heapReq : PIORequest; + flags : uint32; begin { Pre-checks — fail fast before touching the queue } if request = nil then begin submit_io := eInvalidArgument; exit; end; @@ -477,38 +482,16 @@ begin { Enqueue and attempt dispatch — must be ISR-safe and bounded. Reject once the fixed queue is full instead of growing with CLI held. } - asm pushf; cli end; - if (device^.activeCount < device^.maxActive) and (CFIFO_Size(device^.requestQueue) = 0) then begin - device^.activeCount := device^.activeCount + 1; - heapReq^.State := iosDispatched; - asm popf end; - case heapReq^.RequestType of - ioRead: begin - if device^.dispatchRead <> nil then - device^.dispatchRead(device, heapReq) - else - complete_io(heapReq, false, eDeviceNotFound); - end; - ioWrite: begin - if device^.dispatchWrite <> nil then - device^.dispatchWrite(device, heapReq) - else - complete_io(heapReq, false, eDeviceNotFound); - end; - end; - submit_io := eNone; - exit; - end; - + asm pushfd; cli; pop flags end; if CFIFO_Size(device^.requestQueue) >= CFIFO_Capacity(device^.requestQueue) then begin - asm popf end; + if (flags and $200) <> 0 then asm sti end; ioreq_free(heapReq); submit_io := eQueueFull; exit; end; CFIFO_Enqueue(device^.requestQueue, @heapReq); dispatch_next(device); - asm popf end; + if (flags and $200) <> 0 then asm sti end; submit_io := eNone; end; diff --git a/src/driver/storage/driver.storage.types.pas b/src/driver/storage/driver.storage.types.pas index 5027fe2d..9bc82597 100644 --- a/src/driver/storage/driver.storage.types.pas +++ b/src/driver/storage/driver.storage.types.pas @@ -270,6 +270,7 @@ type dispatchRead : TDriverDispatch; { nil if driver not yet migrated } dispatchWrite : TDriverDispatch; { nil if driver not yet migrated } requestQueue : PCFIFOQueue; { per-device I/O request CFIFO } + dispatching : boolean; { prevents recursive synchronous-error queue draining } activeCount : uint32; { number of commands currently dispatched to HW } maxActive : uint32; { max concurrent commands (NCQ depth) } diff --git a/src/test/test.pas b/src/test/test.pas index 09b5d665..fece7d8b 100644 --- a/src/test/test.pas +++ b/src/test/test.pas @@ -34,6 +34,7 @@ uses driver.storage.vfs, driver.storage.test, driver.storage.metatest, + driver.storage.ctl.ahci, core.enc.fnv1a, core.enc.djb2, core.ds.bloom, @@ -70,6 +71,7 @@ begin driver.storage.vfs.UnitTest; driver.storage.test.UnitTest; driver.storage.metatest.UnitTest; + driver.storage.ctl.ahci.UnitTest; core.enc.fnv1a.UnitTest; core.enc.djb2.UnitTest; core.ds.bloom.UnitTest; -- 2.54.0 From 8e0ab3da8b32f4054a5ca4978db389b88b108b95 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 31 Oct 2026 17:32:59 +0000 Subject: [PATCH 12/21] Improve storage performance with read pipelining, run lookup, request merging, and counters --- doc/src/driver/storage/driver.storage.mgr.md | 34 ++++ src/app/app.diskcmd.pas | 11 + .../ctl/ahci/driver.storage.ctl.ahci.pas | 1 + .../storage/driver.storage.iorequest.pas | 10 +- src/driver/storage/driver.storage.mgr.pas | 111 +++++++--- .../storage/driver.storage.perftest.pas | 189 ++++++++++++++++++ src/driver/storage/driver.storage.types.pas | 13 ++ .../fs/fat32/driver.storage.fs.fat32.core.pas | 138 +++++++------ .../driver.storage.fs.fat32.transfer.pas | 139 +++++++++++-- .../fat32/driver.storage.fs.fat32.types.pas | 12 ++ src/test/test.pas | 2 + 11 files changed, 546 insertions(+), 114 deletions(-) create mode 100644 src/driver/storage/driver.storage.perftest.pas diff --git a/doc/src/driver/storage/driver.storage.mgr.md b/doc/src/driver/storage/driver.storage.mgr.md index 7b3c84cf..f92a72d6 100644 --- a/doc/src/driver/storage/driver.storage.mgr.md +++ b/doc/src/driver/storage/driver.storage.mgr.md @@ -219,3 +219,37 @@ Drop-in procedure replacements for callers using the old void-returning API. For - `dispatch_next` is an internal procedure called with interrupts disabled. It dequeues the head of the CFIFO, allocates a heap copy via `ioreq_copy` (for ISR safety), and invokes the appropriate `dispatchRead` or `dispatchWrite` on the device. If neither dispatch pointer is set, the request is failed with `eDeviceNotFound`. - The blocking path relies on pointer-dereferenced spin (`puint32(@wait.Done)^`) to prevent FPC from caching the `Done` flag in a register across the `hlt` instruction. - Devices without `dispatchRead`/`dispatchWrite` set (i.e., drivers not yet migrated to Phase 2) will have `submit_io` return `eDeviceNotFound`. Legacy drivers must be migrated before they can use the new I/O path. + + +## Bounded performance path + +The manager merges up to 32 consecutive queued requests when operation, device, +LBA range, and virtual buffer range are contiguous. It never searches past the +queue head or delays a lone request to create a merge. Drivers opt in through +`mergeMaxSectors`; SATA AHCI advertises 65,536 sectors, while other drivers default +to zero (disabled). A merged command delivers one completion to each original +request, in FIFO order. A command error fails every member conservatively. + +`storage_get_stats` takes an interrupt-protected snapshot of the device counters. +`DISK info ` displays accepted/completed/failed logical requests, hardware +commands, merged requests, queue-full rejections, queue and active high-water +marks, successful read/write sectors, request-size buckets, queue/service ticks, +and FAT metadata cache hits/misses. Queue ticks are accumulated per logical +request; service ticks are accumulated per hardware command. Tick values use +`Counters.c32`, not milliseconds. Counters are unsigned 32-bit values, start at +device registration, and wrap; use snapshot differences over bounded benchmark +intervals. Size buckets are <=8, 9..256, and >256 sectors. + +FAT32 blocking and async reads share a pooled transfer engine with batches of up +to four outstanding reads, capped by device queue depth. Destination slices are +disjoint. On any error, the engine drains every accepted request before releasing +its context and reports only the contiguous successful byte prefix. Unaligned +edges and odd DMA addresses use the context's private scratch sector. Writes +remain sequential. Run maps store cumulative file-cluster offsets; an open-file +cursor handles sequential locality and binary search handles random seeks. + +`STORPERF` runs memory-backed regression checks (also included in boot unit tests) +for reverse completion order, failures, unaligned reads, run lookup, queue +rejection, merging limits, and callback accounting. It does not access real disks +or measure hardware throughput. Compare counters alongside `STORBENCH` and file +benchmarks on contiguous and fragmented files for actual performance validation. diff --git a/src/app/app.diskcmd.pas b/src/app/app.diskcmd.pas index d21dc43a..ec73b5b4 100644 --- a/src/app/app.diskcmd.pas +++ b/src/app/app.diskcmd.pas @@ -150,6 +150,7 @@ var device : PStorage_Device; volCount : uint32; freeSec : uint32; + stats : TStorageStats; begin if paramCount(params) < 2 then begin println('Usage: DISK info '); @@ -191,6 +192,16 @@ begin else volCount := 0; print('Partitions: '); printint(volCount); println(''); + storage_get_stats(device,stats); + print('I/O submitted/completed/failed: '); printint(stats.Submitted); print(' / '); printint(stats.Completed); print(' / '); printint(stats.Failed); println(''); + print('Commands / merged requests: '); printint(stats.Commands); print(' / '); printint(stats.Merged); println(''); + print('Queue peak / active peak / queue full: '); printint(stats.QueuePeak); print(' / '); printint(stats.ActivePeak); print(' / '); printint(stats.QueueFull); println(''); + print('Read / written sectors: '); printint(stats.ReadSectors); print(' / '); printint(stats.WriteSectors); println(''); + print('Requests <=8 / <=256 / >256 sectors: '); printint(stats.SmallRequests); print(' / '); printint(stats.MediumRequests); print(' / '); printint(stats.LargeRequests); println(''); + print('Queue ticks total / max: '); printint(stats.QueueTicks); print(' / '); printint(stats.MaxQueueTicks); println(''); + print('Service ticks total / max: '); printint(stats.ServiceTicks); print(' / '); printint(stats.MaxServiceTicks); println(''); + print('FAT cache hits / misses: '); printint(stats.FatCacheHits); print(' / '); printint(stats.FatCacheMisses); println(''); + end; { ---------- DISK WIPE ---------- } diff --git a/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas b/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas index 0e6fe06c..fd1fff29 100644 --- a/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas +++ b/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas @@ -397,6 +397,7 @@ begin storageDev^.controllerId0 := uint32(device); { store PAHCI_Device for callback wrappers } storageDev^.sectorSize := storageDev_sectorSize; storageDev^.maxSectorCount := sec_count; + if not isATAPI then storageDev^.mergeMaxSectors := 65536; storageDev^.start := 0; storageDev^.volumes := DL_New(sizeof(Pointer)); diff --git a/src/driver/storage/driver.storage.iorequest.pas b/src/driver/storage/driver.storage.iorequest.pas index daf680f1..293b2e11 100644 --- a/src/driver/storage/driver.storage.iorequest.pas +++ b/src/driver/storage/driver.storage.iorequest.pas @@ -39,8 +39,8 @@ function ioreq_alloc(reqType : TIORequestType; procedure ioreq_free(req : PIORequest); { Copy an existing TIORequest into a new pooled allocation (deep copy of the - record, NOT the buffer it points to). Used by dispatch_next to create an - ISR-safe copy from the CFIFO value-element. } + record, NOT the buffer it points to). Used by submit_io before enqueueing + the owned pointer. Manager-only merge/timing fields are initialised here. } function ioreq_copy(src : PIORequest) : PIORequest; implementation @@ -132,6 +132,9 @@ begin ioreq_alloc := nil; exit; end; + req^.MergeNext := nil; + req^.OriginalSectors := sectors; + req^.QueuedTick := 0; req^.DispatchTick := 0; req^.RequestType := reqType; req^.State := iosPending; req^.Device := device; @@ -161,6 +164,9 @@ begin ioreq_copy := nil; exit; end; + dst^.MergeNext := nil; + dst^.OriginalSectors := src^.SectorCount; + dst^.QueuedTick := 0; dst^.DispatchTick := 0; dst^.RequestType := src^.RequestType; dst^.State := src^.State; dst^.Device := src^.Device; diff --git a/src/driver/storage/driver.storage.mgr.pas b/src/driver/storage/driver.storage.mgr.pas index 4e1dcb4a..588e7d8d 100644 --- a/src/driver/storage/driver.storage.mgr.pas +++ b/src/driver/storage/driver.storage.mgr.pas @@ -35,6 +35,7 @@ unit driver.storage.mgr; interface uses + arch.x86.bda, boot.mgr, core.ds.cfifo, core.ds.types, @@ -59,6 +60,7 @@ var hdCount : uint32; { non-ATAPI devices registered so far } atapiCount : uint32; { ATAPI devices registered so far } +procedure storage_get_stats(device : PStorage_Device; var stats : TStorageStats); procedure init(); procedure set_boot_drive_byte(driveByte : uint8); function get_boot_drive_byte() : uint8; @@ -212,6 +214,14 @@ begin kfree(void(ctx)); end; +procedure storage_get_stats(device : PStorage_Device; var stats : TStorageStats); +begin + asm pushf; cli end; + if device <> nil then stats := device^.Stats + else memset(uint32(@stats),0,sizeof(stats)); + asm popf end; +end; + procedure init(); begin push_trace('driver.storage.mgr.init'); @@ -245,6 +255,7 @@ begin { Initialise new Phase 2 fields } device^.removed := false; device^.requestQueue := CFIFO_New(SizeOf(PIORequest), 128); + memset(uint32(@device^.Stats),0,sizeof(TStorageStats)); device^.activeCount := 0; device^.dispatching := false; if device^.maxActive = 0 then device^.maxActive := 1; @@ -415,7 +426,9 @@ end; Called with interrupts disabled (CLI context) or from ISR context. } procedure dispatch_next(device : PStorage_Device); var - heapReq : PIORequest; + heapReq, nextReq, tail : PIORequest; + peek : pointer; + merged, bytes, ticks : uint32; begin if device^.dispatching then exit; device^.dispatching := true; @@ -425,8 +438,41 @@ begin if heapReq = nil then break; + { Merge only consecutive FIFO entries with consecutive DMA buffers. + Bound both command size and completion fan-out; never reorder writes. } + tail := heapReq; merged := 1; + while (merged < 32) and (device^.mergeMaxSectors > 0) and + (device^.sectorSize > 0) do begin + peek := CFIFO_Peek(device^.requestQueue); + if peek = nil then break; + nextReq := PIORequest(puint32(peek)^); + if nextReq = nil then break; + if (nextReq^.Device <> device) or (nextReq^.RequestType <> heapReq^.RequestType) then break; + if heapReq^.SectorCount > device^.mergeMaxSectors then break; + if nextReq^.SectorCount > device^.mergeMaxSectors-heapReq^.SectorCount then break; + if heapReq^.SectorCount > ($FFFFFFFF div device^.sectorSize) then break; + bytes := heapReq^.SectorCount*device^.sectorSize; + if (heapReq^.LBA > $FFFFFFFF-heapReq^.SectorCount) or + (uint32(heapReq^.Buffer) > $FFFFFFFF-bytes) then break; + if (nextReq^.LBA <> heapReq^.LBA+heapReq^.SectorCount) or + (uint32(nextReq^.Buffer) <> uint32(heapReq^.Buffer)+bytes) then break; + if not CFIFO_Dequeue(device^.requestQueue,@nextReq) then break; + tail^.MergeNext := nextReq; tail := nextReq; + Inc(heapReq^.SectorCount,nextReq^.SectorCount); Inc(merged); + Inc(device^.Stats.Merged); + end; + ticks := puint32(@Counters.c32)^; + tail := heapReq; + while tail <> nil do begin + tail^.DispatchTick := ticks; tail^.State := iosDispatched; + bytes := ticks-tail^.QueuedTick; + Inc(device^.Stats.QueueTicks,bytes); + if bytes > device^.Stats.MaxQueueTicks then device^.Stats.MaxQueueTicks := bytes; + tail := tail^.MergeNext; + end; + Inc(device^.Stats.Commands); device^.activeCount := device^.activeCount + 1; - heapReq^.State := iosDispatched; + if device^.activeCount > device^.Stats.ActivePeak then device^.Stats.ActivePeak := device^.activeCount; case heapReq^.RequestType of ioRead: begin @@ -484,12 +530,20 @@ begin Reject once the fixed queue is full instead of growing with CLI held. } asm pushfd; cli; pop flags end; if CFIFO_Size(device^.requestQueue) >= CFIFO_Capacity(device^.requestQueue) then begin + Inc(device^.Stats.QueueFull); if (flags and $200) <> 0 then asm sti end; ioreq_free(heapReq); submit_io := eQueueFull; exit; end; + heapReq^.QueuedTick := puint32(@Counters.c32)^; + Inc(device^.Stats.Submitted); + if request^.SectorCount <= 8 then Inc(device^.Stats.SmallRequests) + else if request^.SectorCount <= 256 then Inc(device^.Stats.MediumRequests) + else Inc(device^.Stats.LargeRequests); CFIFO_Enqueue(device^.requestQueue, @heapReq); + if CFIFO_Size(device^.requestQueue) > device^.Stats.QueuePeak then + device^.Stats.QueuePeak := CFIFO_Size(device^.requestQueue); dispatch_next(device); if (flags and $200) <> 0 then asm sti end; @@ -545,36 +599,39 @@ begin end; procedure complete_io(request : PIORequest; success : boolean; error : TError); -var - device : PStorage_Device; +var device : PStorage_Device; next : PIORequest; ticks, flags : uint32; + wasDispatching : boolean; begin if request = nil then exit; - device := request^.Device; - - if success then begin - request^.State := iosComplete; - request^.Error := eNone; - end else begin - request^.State := iosError; - request^.Error := error; + asm pushfd; cli; pop flags end; + wasDispatching := false; + if device <> nil then begin + wasDispatching := device^.dispatching; + device^.dispatching := true; { callbacks cannot dispatch inside merged fan-out } + if device^.activeCount > 0 then Dec(device^.activeCount); + ticks := puint32(@Counters.c32)^-request^.DispatchTick; + Inc(device^.Stats.ServiceTicks,ticks); + if ticks > device^.Stats.MaxServiceTicks then device^.Stats.MaxServiceTicks := ticks; end; - - { Decrement active count on the device } - if device <> nil then - if device^.activeCount > 0 then - device^.activeCount := device^.activeCount - 1; - - { Invoke completion callback (sync path uses sync_io_callback, async path uses caller's callback) } - if request^.Callback <> nil then - request^.Callback(request^.Error, request^.CallbackData); - - { Free the heap copy of the request } - ioreq_free(request); - - { Dispatch next queued request for this device } - if device <> nil then + while request <> nil do begin + next := request^.MergeNext; + if success then begin request^.State := iosComplete; request^.Error := eNone; end + else begin request^.State := iosError; request^.Error := error; end; + if device <> nil then begin + Inc(device^.Stats.Completed); + if not success then Inc(device^.Stats.Failed) + else if request^.RequestType = ioRead then Inc(device^.Stats.ReadSectors,request^.OriginalSectors) + else Inc(device^.Stats.WriteSectors,request^.OriginalSectors); + end; + if request^.Callback <> nil then request^.Callback(request^.Error,request^.CallbackData); + ioreq_free(request); request := next; + end; + if device <> nil then begin + device^.dispatching := wasDispatching; dispatch_next(device); + end; + if (flags and $200) <> 0 then asm sti end; end; { ========================================================================== } diff --git a/src/driver/storage/driver.storage.perftest.pas b/src/driver/storage/driver.storage.perftest.pas new file mode 100644 index 00000000..5acf8901 --- /dev/null +++ b/src/driver/storage/driver.storage.perftest.pas @@ -0,0 +1,189 @@ +unit driver.storage.perftest; +interface +procedure UnitTest; +procedure init; +implementation +uses boot.mgr, io.stdio, io.syslog, memory.heap, core.util, core.ds.cfifo, + driver.storage.types, driver.storage.mgr, driver.storage.fs.fat32.types, + driver.storage.fs.fat32.core, driver.storage.fs.fat32.transfer; +type + PMock = ^TMock; + TMock = record + Data : puint8; + Pending : array[0..7] of PIORequest; + Count, Calls, FailLBA, Callbacks, Bytes : uint32; + Deferred : boolean; + Error : TError; + end; +procedure finish(request : PIORequest); +var m : PMock; bytes : uint32; +begin + m := PMock(request^.Device^.controllerId0); + if request^.LBA = m^.FailLBA then begin complete_io(request,false,eIOError); exit; end; + if (request^.LBA >= 128) or (request^.SectorCount > 128-request^.LBA) then begin + complete_io(request,false,eInvalidArgument); exit; + end; + if (uint32(request^.Buffer) and 1) <> 0 then begin complete_io(request,false,eInvalidArgument); exit; end; + bytes := request^.SectorCount*512; + if request^.RequestType = ioRead then memcpy(uint32(m^.Data)+request^.LBA*512,uint32(request^.Buffer),bytes) + else memcpy(uint32(request^.Buffer),uint32(m^.Data)+request^.LBA*512,bytes); + complete_io(request,true,eNone); +end; +procedure dispatch(device : PStorage_Device; request : PIORequest); +var m : PMock; +begin + m := PMock(device^.controllerId0); Inc(m^.Calls); + if m^.Deferred then begin + if m^.Count = 8 then begin complete_io(request,false,eQueueFull); exit; end; + m^.Pending[m^.Count] := request; Inc(m^.Count); + end else finish(request); +end; +procedure done(error : TError; data : pointer); +var m : PMock; +begin m := PMock(data); Inc(m^.Callbacks); m^.Error := error; end; +procedure drain(m : PMock); +var request : PIORequest; n : uint32; +begin + n := 0; + while (m^.Count > 0) and (n < 64) do begin + Inc(n); Dec(m^.Count); request := m^.Pending[m^.Count]; finish(request); + end; +end; +procedure UnitTest; +var device : PStorage_Device; volume : PStorage_Volume; ofi : PFATOpenFile; + m : PMock; vc : PFATVolumeCtx; br : PBootRecord; buffer : puint8; + i, j, idx, base, cluster, off, available, passed, failed, before : uint32; + ok : boolean; + queued : PIORequest; + procedure Assert(value : boolean; name : pchar); + begin + if value then Inc(passed) else begin Inc(failed); io.syslog.logln('STORPERF FAIL',name); end; + end; +begin + passed := 0; failed := 0; + device := PStorage_Device(kalloc(sizeof(TStorage_Device))); + volume := PStorage_Volume(kalloc(sizeof(TStorage_Volume))); + ofi := PFATOpenFile(kalloc(sizeof(TFATOpenFile))); + m := PMock(kalloc(sizeof(TMock))); buffer := puint8(kalloc(4096)); + if (device=nil) or (volume=nil) or (ofi=nil) or (m=nil) or (buffer=nil) then begin + if device<>nil then kfree(void(device)); if volume<>nil then kfree(void(volume)); + if ofi<>nil then kfree(void(ofi)); if m<>nil then kfree(void(m)); if buffer<>nil then kfree(void(buffer)); + io.syslog.logln('STORPERF FAIL','allocation'); exit; + end; + memset(uint32(device),0,sizeof(TStorage_Device)); memset(uint32(volume),0,sizeof(TStorage_Volume)); + memset(uint32(ofi),0,sizeof(TFATOpenFile)); memset(uint32(m),0,sizeof(TMock)); + m^.Data := puint8(kalloc(128*512)); ofi^.RunMap.Runs := PFATRun(kalloc(4*sizeof(TFATRun))); + if (m^.Data=nil) or (ofi^.RunMap.Runs=nil) then begin + if m^.Data<>nil then kfree(void(m^.Data)); if ofi^.RunMap.Runs<>nil then kfree(void(ofi^.RunMap.Runs)); + kfree(void(device)); kfree(void(volume)); kfree(void(ofi)); kfree(void(m)); kfree(void(buffer)); + io.syslog.logln('STORPERF FAIL','data allocation'); exit; + end; + for i := 0 to 127 do memset(uint32(m^.Data)+i*512,i,512); + br := PBootRecord(m^.Data); memset(uint32(br),0,sizeof(TBootRecord)); + br^.sectorSize := 512; br^.spc := 1; br^.rsvSectors := 1; br^.numFats := 1; + br^.FATSize := 1; br^.manySectors := 128; br^.rootCluster := 2; + device^.controllerId0 := uint32(m); device^.sectorSize := 512; device^.maxSectorCount := 128; + device^.writable := true; device^.maxActive := 4; + device^.dispatchRead := @dispatch; device^.dispatchWrite := @dispatch; + device^.requestQueue := CFIFO_New(sizeof(pointer),16); m^.FailLBA := $FFFFFFFF; + volume^.device := device; volume^.sectorCount := 128; volume^.sectorSize := 512; + vc := FAT32GetVolumeCtx(volume); + Assert(vc<>nil,'synthetic volume context'); + if vc<>nil then begin + ofi^.Volume := volume; ofi^.VolumeInfo := @vc^.Info; ofi^.ByteSize := 4096; + ofi^.AllocClusters := 8; ofi^.RunMap.Count := 4; ofi^.RunMap.Capacity := 4; + ofi^.RunMap.TotalClusters := 8; ofi^.Exists := true; ofi^.Registered := true; ofi^.DirLoc.Valid := true; + ofi^.ScratchSector := puint32(kalloc(512)); ofi^.ScratchSize := 512; + for i := 0 to 3 do begin + ofi^.RunMap.Runs[i].StartCluster := 10+i*10; + ofi^.RunMap.Runs[i].ClusterCount := 2; + ofi^.RunMap.Runs[i].FileClusterStart := i*2; + end; + Assert(FAT32FindRunForOffset(ofi,3584,false,0,0,idx,base,cluster,off,available) and + (idx=3) and (cluster=41) and (available=512),'random run seek'); + Assert(FAT32FindRunForOffset(ofi,512,false,0,0,idx,base,cluster,off,available) and + (idx=0) and (cluster=11),'backward persistent cursor seek'); + Assert(not FAT32FindRunForOffset(ofi,4096,false,0,0,idx,base,cluster,off,available),'run EOF'); + m^.Deferred := true; m^.Callbacks := 0; + FAT32ReadFileAtOffsetAsync(volume,nil,nil,0,puint32(buffer),4096,ofi,@m^.Bytes,@done,m); + Assert((m^.Count=4) and (m^.Callbacks=0),'four reads outstanding'); + Dec(m^.Count); finish(m^.Pending[m^.Count]); + Assert(m^.Callbacks=0,'reverse completion retains context'); drain(m); + Assert((m^.Callbacks=1) and (m^.Bytes=4096) and (m^.Error=eNone),'batch completes once'); + ok := true; + for i := 0 to 4095 do if buffer[i] <> 10+(i div 1024)*10+(i mod 1024) div 512 then ok := false; + Assert(ok,'fragmented DMA contents and cluster offsets'); + m^.Callbacks := 0; m^.FailLBA := 20; + FAT32ReadFileAtOffsetAsync(volume,nil,nil,0,puint32(buffer),4096,ofi,@m^.Bytes,@done,m); drain(m); + Assert((m^.Callbacks=1) and (m^.Bytes=1024) and (m^.Error=eIOError) and (device^.activeCount=0),'failure drains DMA and reports prefix'); + m^.FailLBA := $FFFFFFFF; m^.Callbacks := 0; + FAT32ReadFileAtOffsetAsync(volume,nil,nil,513,puint32(buffer),3073,ofi,@m^.Bytes,@done,m); drain(m); + ok := (m^.Callbacks=1) and (m^.Bytes=3073) and (m^.Error=eNone); + for i := 0 to 3072 do begin j := i+513; + if buffer[i] <> 10+(j div 1024)*10+(j mod 1024) div 512 then ok := false; + end; + Assert(ok,'unaligned head and tail around pipeline'); + m^.Deferred := false; m^.Callbacks := 0; + FAT32ReadFileAtOffsetAsync(volume,nil,nil,0,puint32(buffer),4096,ofi,@m^.Bytes,@done,m); + Assert((m^.Callbacks=1) and (m^.Bytes=4096) and (m^.Error=eNone),'inline batch completion is iterative'); + m^.FailLBA := 10; m^.Callbacks := 0; + FAT32ReadFileAtOffsetAsync(volume,nil,nil,0,puint32(buffer),4096,ofi,@m^.Bytes,@done,m); + Assert((m^.Callbacks=1) and (m^.Bytes=0) and (m^.Error=eIOError),'inline first-read failure completes once'); + m^.FailLBA := $FFFFFFFF; + device^.maxActive := 0; m^.Callbacks := 0; + for i := 0 to 15 do Assert(storage_read_async(device,50,1,puint32(buffer),nil,nil)=eNone,'fill fixed queue'); + FAT32ReadFileAtOffsetAsync(volume,nil,nil,0,puint32(buffer),4096,ofi,@m^.Bytes,@done,m); + Assert((m^.Callbacks=1) and (m^.Bytes=0) and (m^.Error=eQueueFull),'rejected pipeline submission releases context'); + device^.maxActive := 4; + { Simulate dispatch of the first queued request, then let completion drain. } + if CFIFO_Dequeue(device^.requestQueue,@queued) then begin + Inc(device^.activeCount); finish(queued); + end; + Assert(CFIFO_Size(device^.requestQueue)=0,'full queue drained'); + Assert(FAT32ReadFileAtOffset(volume,nil,nil,512,puint32(buffer),512,ofi)=512,'blocking read uses pipeline'); + Assert(buffer[0]=11,'offset inside contiguous run is not counted twice'); + m^.FailLBA := 10; + Assert(FAT32WriteFileAtOffset(volume,nil,nil,0,puint32(buffer),512,ofi)=0,'failed write reports zero bytes'); + m^.FailLBA := $FFFFFFFF; + if ofi^.ScratchSector<>nil then kfree(void(ofi^.ScratchSector)); + FAT32ReleaseVolumeCtx(volume); + end; + { Force a backlog without issuing hardware I/O, then drain through a mock. } + device^.maxActive := 0; device^.mergeMaxSectors := 8; m^.Deferred := false; + m^.Callbacks := 0; before := m^.Calls; + for i := 0 to 3 do Assert(storage_read_async(device,10+i,1,puint32(uint32(buffer)+i*512),@done,m)=eNone,'merge enqueue'); + device^.maxActive := 1; + Assert(storage_read_async(device,50,1,puint32(buffer),@done,m)=eNone,'drain trigger'); + Assert((m^.Calls-before=2) and (m^.Callbacks=5),'four requests merge with five callbacks including trigger'); + Assert(device^.Stats.Merged=3,'merge counter'); + device^.maxActive := 0; m^.Callbacks := 0; m^.FailLBA := 10; + for i := 0 to 1 do Assert(storage_write_async(device,10+i,1,puint32(uint32(buffer)+i*512),@done,m)=eNone,'merged write enqueue'); + device^.maxActive := 1; before := device^.Stats.Failed; + Assert(storage_read_async(device,50,1,puint32(buffer),@done,m)=eNone,'failed merge drain trigger'); + Assert((device^.Stats.Failed-before=2) and (m^.Callbacks=3),'merged failure fans out once'); + m^.FailLBA := $FFFFFFFF; device^.maxActive := 0; device^.mergeMaxSectors := 2; + m^.Callbacks := 0; before := m^.Calls; + for i := 0 to 3 do Assert(storage_read_async(device,10+i,1,puint32(uint32(buffer)+i*512),@done,m)=eNone,'limited merge enqueue'); + device^.maxActive := 1; + Assert(storage_read_async(device,50,1,puint32(buffer),@done,m)=eNone,'limited merge drain trigger'); + Assert((m^.Calls-before=3) and (m^.Callbacks=5),'driver command limit splits merges'); + device^.maxActive := 0; device^.mergeMaxSectors := 8; before := m^.Calls; + Assert(storage_read_async(device,10,1,puint32(buffer),nil,nil)=eNone,'ordering read'); + Assert(storage_write_async(device,11,1,puint32(uint32(buffer)+512),nil,nil)=eNone,'ordering write'); + Assert(storage_write_async(device,12,1,puint32(buffer),nil,nil)=eNone,'noncontiguous buffer'); + device^.maxActive := 1; + Assert(storage_read_async(device,50,1,puint32(buffer),nil,nil)=eNone,'ordering drain trigger'); + Assert(m^.Calls-before=4,'operation and buffer boundaries prevent merging'); + Assert(device^.Stats.Completed=device^.Stats.Submitted,'all accepted requests accounted'); + CFIFO_Free(device^.requestQueue); kfree(void(ofi^.RunMap.Runs)); kfree(void(m^.Data)); + kfree(void(device)); kfree(void(volume)); kfree(void(ofi)); kfree(void(m)); kfree(void(buffer)); + io.syslog.log('STORPERF','Passed: '); io.syslog.writeint(passed); + io.syslog.writestring(' Failed: '); io.syslog.writeintln(failed); +end; +procedure command(params : PParamList; stdin_buf, stdout_buf, stderr_buf : POutBuf); +begin UnitTest; io.stdio.bufWriteStrLn(stdout_buf,'Storage performance regression results written to serial log.'); end; +procedure init; +begin io.stdio.registerCommand('STORPERF',@command,'Run memory-backed storage performance regression tests'); end; +initialization + boot.mgr.registerBoot('driver.storage.perftest',@init,'Storage Performance Tests',BOOT_MGR_BARRIER_MID); +end. diff --git a/src/driver/storage/driver.storage.types.pas b/src/driver/storage/driver.storage.types.pas index 9bc82597..936ea5d3 100644 --- a/src/driver/storage/driver.storage.types.pas +++ b/src/driver/storage/driver.storage.types.pas @@ -252,11 +252,22 @@ type UserData : pointer; { points back to caller's stack-local request (blocking path) } Callback : TIOCallback; { async completion callback (nil for blocking path) } CallbackData : pointer; { opaque data passed to Callback } + { Manager-owned fields; callers need not initialise these. } + MergeNext : PIORequest; + OriginalSectors, QueuedTick, DispatchTick : uint32; end; byteArray8 = array[0..7] of char; PByteArray8 = ^byteArray8; + TStorageStats = record + Submitted, Completed, Failed, Commands, Merged, QueueFull : uint32; + ReadSectors, WriteSectors, QueuePeak, ActivePeak : uint32; + QueueTicks, ServiceTicks, MaxQueueTicks, MaxServiceTicks : uint32; + SmallRequests, MediumRequests, LargeRequests : uint32; + FatCacheHits, FatCacheMisses : uint32; + end; + { Generic storage device } TStorage_Device = record id : uint8; @@ -272,6 +283,7 @@ type requestQueue : PCFIFOQueue; { per-device I/O request CFIFO } dispatching : boolean; { prevents recursive synchronous-error queue draining } activeCount : uint32; { number of commands currently dispatched to HW } + mergeMaxSectors : uint32; { zero disables merging; driver sets safe command limit } maxActive : uint32; { max concurrent commands (NCQ depth) } maxSectorCount : uint32; @@ -281,6 +293,7 @@ type mbrWriteBusy : boolean; { serializes staged sector-zero writes } cachedMBR : pointer; { cached mbr data, nil if not yet read } isBootDevice : boolean; { true if this is the device GRUB booted from } + Stats : TStorageStats; { cold tail: wrapping counters, timer ticks } end; { Filesystem driver descriptor } diff --git a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.core.pas b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.core.pas index 01ccea54..24742ac5 100644 --- a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.core.pas +++ b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.core.pas @@ -58,6 +58,25 @@ var sioc_allocVol : PStorage_Volume = nil; fatCtxListHead : PFATVolumeCtxNode = nil; +{ Bounded set operations; honour FAT_CACHE_WAYS rather than assuming two ways. } +procedure cacheTouch(cache : PFATCache; base, slot : uint32); +var w : uint32; +begin + for w := 0 to FAT_CACHE_WAYS-1 do + if cache^.Lines[base+w].LRU < FAT_CACHE_WAYS-1 then Inc(cache^.Lines[base+w].LRU); + cache^.Lines[slot].LRU := 0; +end; +function cacheVictim(cache : PFATCache; base : uint32) : uint32; +var w, victim : uint32; +begin + victim := base; + for w := 0 to FAT_CACHE_WAYS-1 do begin + if not cache^.Lines[base+w].Valid then begin cacheVictim := base+w; exit; end; + if cache^.Lines[base+w].LRU > cache^.Lines[victim].LRU then victim := base+w; + end; + cacheVictim := victim; +end; + function readFat(volume : PStorage_Volume; cluster : uint32; bootRecord : PBootRecord) : uint32; forward; procedure writeFat(volume : PStorage_Volume; cluster : uint32; value : uint32; bootRecord : PBootRecord); forward; procedure fatFreeClusterChain(volume : PStorage_Volume; info : PFATVolumeInfo; startCluster : uint32); forward; @@ -771,6 +790,7 @@ begin while true do begin asm pushf; cli end; if not cache^.Busy then begin + Inc(cache^.Device^.Stats.FatCacheMisses); cache^.Busy := true; asm popf end; break; @@ -846,10 +866,10 @@ begin asm pushf; cli end; for w := 0 to FAT_CACHE_WAYS - 1 do if cache^.Lines[baseSlot + w].Valid and (cache^.Lines[baseSlot + w].SectorIdx = fatSecIdx) then begin + Inc(cache^.Device^.Stats.FatCacheHits); dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); readFat := dataPtr[entryOff] and $0FFFFFFF; - cache^.Lines[baseSlot + w].LRU := 0; - cache^.Lines[baseSlot + (1 - w)].LRU := 1; + cacheTouch(cache,baseSlot,baseSlot+w); asm popf end; exit; end; @@ -860,23 +880,16 @@ begin if not cache^.Busy then begin for w := 0 to FAT_CACHE_WAYS - 1 do if cache^.Lines[baseSlot + w].Valid and (cache^.Lines[baseSlot + w].SectorIdx = fatSecIdx) then begin + Inc(cache^.Device^.Stats.FatCacheHits); dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); readFat := dataPtr[entryOff] and $0FFFFFFF; - cache^.Lines[baseSlot + w].LRU := 0; - cache^.Lines[baseSlot + (1 - w)].LRU := 1; + cacheTouch(cache,baseSlot,baseSlot+w); asm popf end; exit; end; + Inc(cache^.Device^.Stats.FatCacheMisses); cache^.Busy := true; - victim := baseSlot; - if not cache^.Lines[baseSlot].Valid then - victim := baseSlot - else if not cache^.Lines[baseSlot + 1].Valid then - victim := baseSlot + 1 - else if cache^.Lines[baseSlot + 1].LRU >= cache^.Lines[baseSlot].LRU then - victim := baseSlot + 1 - else - victim := baseSlot; + victim := cacheVictim(cache,baseSlot); dataPtr := puint32(uint32(cache^.Data) + (victim * 512)); doEvict := cache^.Lines[victim].Valid and cache^.Lines[victim].Dirty; if doEvict then begin @@ -900,11 +913,7 @@ begin cache^.Lines[victim].SectorIdx := fatSecIdx; cache^.Lines[victim].Valid := true; cache^.Lines[victim].Dirty := false; - cache^.Lines[victim].LRU := 0; - if victim = baseSlot then - cache^.Lines[baseSlot + 1].LRU := 1 - else - cache^.Lines[baseSlot].LRU := 1; + cacheTouch(cache,baseSlot,victim); readFat := dataPtr[entryOff] and $0FFFFFFF; cache^.Busy := false; asm popf end; @@ -936,14 +945,14 @@ begin asm pushf; cli end; for w := 0 to FAT_CACHE_WAYS - 1 do if cache^.Lines[baseSlot + w].Valid and (cache^.Lines[baseSlot + w].SectorIdx = fatSecIdx) then begin + Inc(cache^.Device^.Stats.FatCacheHits); dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); if value = 0 then dataPtr[entryOff] := 0 else dataPtr[entryOff] := (dataPtr[entryOff] and $F0000000) or (value and $0FFFFFFF); cache^.Lines[baseSlot + w].Dirty := true; - cache^.Lines[baseSlot + w].LRU := 0; - cache^.Lines[baseSlot + (1 - w)].LRU := 1; + cacheTouch(cache,baseSlot,baseSlot+w); asm popf end; exit; end; @@ -954,27 +963,20 @@ begin if not cache^.Busy then begin for w := 0 to FAT_CACHE_WAYS - 1 do if cache^.Lines[baseSlot + w].Valid and (cache^.Lines[baseSlot + w].SectorIdx = fatSecIdx) then begin + Inc(cache^.Device^.Stats.FatCacheHits); dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); if value = 0 then dataPtr[entryOff] := 0 else dataPtr[entryOff] := (dataPtr[entryOff] and $F0000000) or (value and $0FFFFFFF); cache^.Lines[baseSlot + w].Dirty := true; - cache^.Lines[baseSlot + w].LRU := 0; - cache^.Lines[baseSlot + (1 - w)].LRU := 1; + cacheTouch(cache,baseSlot,baseSlot+w); asm popf end; exit; end; + Inc(cache^.Device^.Stats.FatCacheMisses); cache^.Busy := true; - victim := baseSlot; - if not cache^.Lines[baseSlot].Valid then - victim := baseSlot - else if not cache^.Lines[baseSlot + 1].Valid then - victim := baseSlot + 1 - else if cache^.Lines[baseSlot + 1].LRU >= cache^.Lines[baseSlot].LRU then - victim := baseSlot + 1 - else - victim := baseSlot; + victim := cacheVictim(cache,baseSlot); dataPtr := puint32(uint32(cache^.Data) + (victim * 512)); doEvict := cache^.Lines[victim].Valid and cache^.Lines[victim].Dirty; if doEvict then begin @@ -1003,11 +1005,7 @@ begin else dataPtr[entryOff] := (dataPtr[entryOff] and $F0000000) or (value and $0FFFFFFF); cache^.Lines[victim].Dirty := true; - cache^.Lines[victim].LRU := 0; - if victim = baseSlot then - cache^.Lines[baseSlot + 1].LRU := 1 - else - cache^.Lines[baseSlot].LRU := 1; + cacheTouch(cache,baseSlot,victim); cache^.Busy := false; asm popf end; end; @@ -1064,6 +1062,7 @@ begin end; end; if not runMapEnsureCapacity(runMap, runMap.Count + 1) then exit; + runMap.Runs[runMap.Count].FileClusterStart := runMap.TotalClusters; runMap.Runs[runMap.Count].StartCluster := startCluster; runMap.Runs[runMap.Count].ClusterCount := clusterCount; runMap.Count := runMap.Count + 1; @@ -1609,44 +1608,43 @@ function FAT32FindRunForOffset(ofi : PFATOpenFile; fileOffset : uint32; var outRunIdx : uint32; var outRunFileCluster : uint32; var outStartCluster : uint32; var outClusterOffset : uint32; var outAvailableBytes : uint32) : boolean; -var - targetCluster : uint32; - clusterBase : uint32; - i : uint32; - bytesPerCluster : uint32; - byteOffsetInCl : uint32; +var target, i, lo, hi, mid, base, bpc, flags : uint32; begin FAT32FindRunForOffset := false; - if (ofi = nil) or (ofi^.VolumeInfo = nil) or (ofi^.RunMap.Count = 0) then - exit; - - bytesPerCluster := ofi^.VolumeInfo^.BytesPerCluster; - if bytesPerCluster = 0 then exit; - - targetCluster := fileOffset div bytesPerCluster; - byteOffsetInCl := fileOffset mod bytesPerCluster; - - if hintValid and (hintRunIdx < ofi^.RunMap.Count) and (hintFileCluster <= targetCluster) then begin - i := hintRunIdx; - clusterBase := hintFileCluster; - end else begin - i := 0; - clusterBase := 0; - end; - - while i < ofi^.RunMap.Count do begin - if targetCluster < (clusterBase + ofi^.RunMap.Runs[i].ClusterCount) then begin - outRunIdx := i; - outRunFileCluster := clusterBase; - outClusterOffset := targetCluster - clusterBase; - outStartCluster := ofi^.RunMap.Runs[i].StartCluster + outClusterOffset; - outAvailableBytes := ((ofi^.RunMap.Runs[i].ClusterCount - outClusterOffset) * bytesPerCluster) - byteOffsetInCl; - FAT32FindRunForOffset := true; - exit; + if (ofi = nil) or (ofi^.VolumeInfo = nil) or (ofi^.RunMap.Count = 0) then exit; + bpc := ofi^.VolumeInfo^.BytesPerCluster; + if bpc = 0 then exit; + target := fileOffset div bpc; + if target >= ofi^.RunMap.TotalClusters then exit; + { Only the index is shared: derive the base from the current run map. } + i := ofi^.Cursor.RunIdx; + if hintValid then i := hintRunIdx; + if i >= ofi^.RunMap.Count then i := 0; + base := ofi^.RunMap.Runs[i].FileClusterStart; + if (target < base) or (target-base >= ofi^.RunMap.Runs[i].ClusterCount) then begin + { Sequential boundary crossing stays O(1); random seeks are O(log runs). } + if (i+1 < ofi^.RunMap.Count) and + (target = ofi^.RunMap.Runs[i+1].FileClusterStart) then Inc(i) + else begin + lo := 0; hi := ofi^.RunMap.Count; + while lo < hi do begin + mid := lo + (hi-lo) div 2; + if ofi^.RunMap.Runs[mid].FileClusterStart <= target then lo := mid+1 + else hi := mid; + end; + if lo = 0 then exit; + i := lo-1; end; - clusterBase := clusterBase + ofi^.RunMap.Runs[i].ClusterCount; - i := i + 1; + base := ofi^.RunMap.Runs[i].FileClusterStart; end; + outRunIdx := i; outRunFileCluster := base; + outClusterOffset := target-base; + outStartCluster := ofi^.RunMap.Runs[i].StartCluster + outClusterOffset; + outAvailableBytes := (ofi^.RunMap.Runs[i].ClusterCount-outClusterOffset)*bpc - (fileOffset mod bpc); + asm pushfd; cli; pop flags end; + ofi^.Cursor.RunIdx := i; ofi^.Cursor.FileClusterStart := base; ofi^.Cursor.Valid := true; + if (flags and $200) <> 0 then asm sti end; + FAT32FindRunForOffset := true; end; function FAT32EnsureCapacityForWrite(ofi : PFATOpenFile; offset : uint32; byteCount : uint32) : TError; diff --git a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.transfer.pas b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.transfer.pas index a164c2fb..d82e6b88 100644 --- a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.transfer.pas +++ b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.transfer.pas @@ -19,6 +19,7 @@ procedure FAT32WriteFileAtOffsetAsync(volume : PStorage_Volume; directory : pcha implementation uses + proc.mgr, proc.types, driver.storage.mgr, core.util, arch.x86.util, memory.heap, @@ -57,6 +58,7 @@ var chunkBytes : uint32; chunkSectors : uint32; sectorLBA : uint32; + err : TError; begin transferSync := 0; if (ofi = nil) or (buffer = nil) or (byteCount = 0) then exit; @@ -79,21 +81,24 @@ begin hintRunIdx := runIdx; hintFileCluster := runFileCluster; - runByteOffset := currentOffset - (runFileCluster * bytesPerCluster); + runByteOffset := currentOffset mod bytesPerCluster; sectorOffset := runByteOffset mod sectorSize; sectorLBA := FAT32ClusterToLBA(ofi^.VolumeInfo, startCluster) + (runByteOffset div sectorSize); - if (sectorOffset <> 0) or (remaining < sectorSize) then begin - if scratch = nil then break; + if (sectorOffset <> 0) or (remaining < sectorSize) or + (((uint32(buffer)+bytesDone) and 1) <> 0) then begin + if (scratch = nil) or (scratchSize < sectorSize) then break; chunkBytes := sectorSize - sectorOffset; if chunkBytes > remaining then chunkBytes := remaining; - driver.storage.mgr.storage_read(ofi^.Volume^.device, sectorLBA, 1, scratch); + err := driver.storage.mgr.storage_read(ofi^.Volume^.device, sectorLBA, 1, scratch); + if err <> eNone then break; if mode = ftmRead then memcpy(uint32(scratch) + sectorOffset, uint32(buffer) + bytesDone, chunkBytes) else begin memcpy(uint32(buffer) + bytesDone, uint32(scratch) + sectorOffset, chunkBytes); - driver.storage.mgr.storage_write(ofi^.Volume^.device, sectorLBA, 1, scratch); + err := driver.storage.mgr.storage_write(ofi^.Volume^.device, sectorLBA, 1, scratch); + if err <> eNone then break; end; end else begin chunkBytes := remaining; @@ -106,13 +111,14 @@ begin chunkSectors := FAT_MAX_IO_SECTORS; chunkBytes := chunkSectors * sectorSize; if mode = ftmRead then - driver.storage.mgr.storage_read(ofi^.Volume^.device, sectorLBA, chunkSectors, + err := driver.storage.mgr.storage_read(ofi^.Volume^.device, sectorLBA, chunkSectors, puint32(uint32(buffer) + bytesDone)) else - driver.storage.mgr.storage_write(ofi^.Volume^.device, sectorLBA, chunkSectors, + err := driver.storage.mgr.storage_write(ofi^.Volume^.device, sectorLBA, chunkSectors, puint32(uint32(buffer) + bytesDone)); end; + if err <> eNone then break; bytesDone := bytesDone + chunkBytes; remaining := remaining - chunkBytes; if mode = ftmWrite then @@ -123,13 +129,18 @@ begin end; procedure asyncComplete(ctx : PFATTransferCtx; error : TError); +var callback : TIOCallback; data : pointer; begin if ctx = nil then exit; + if ctx^.Pumping then begin + ctx^.Finished := true; ctx^.LastError := error; exit; + end; if ctx^.BytesOut <> nil then ctx^.BytesOut^ := ctx^.BytesDone; - if ctx^.Callback <> nil then - ctx^.Callback(error, ctx^.CallbackData); + callback := ctx^.Callback; data := ctx^.CallbackData; + { Return the context before user code can close the volume or submit again. } FAT32TransferFree(ctx); + if callback <> nil then callback(error,data); end; procedure asyncContinue(ctx : PFATTransferCtx); forward; @@ -178,7 +189,64 @@ begin end; end; -procedure asyncContinue(ctx : PFATTransferCtx); +{ A batch owns four disjoint destination slices. On failure, drain every + accepted DMA before releasing context and report only the successful prefix. } +procedure readBatchFinish(ctx : PFATTransferCtx); +var i : uint32; err : TError; +begin + err := eNone; + if ctx^.ReadCount > 0 then for i := 0 to ctx^.ReadCount-1 do begin + if ctx^.ReadSlots[i].Error <> eNone then begin err := ctx^.ReadSlots[i].Error; break; end; + Inc(ctx^.BytesDone,ctx^.ReadSlots[i].Bytes); + end; + ctx^.ReadCount := 0; + if err <> eNone then asyncComplete(ctx,err) else asyncContinue(ctx); +end; + +procedure readSlotDone(error : TError; data : pointer); +var slot : PFATReadSlot; ctx : PFATTransferCtx; +begin + slot := PFATReadSlot(data); ctx := slot^.Owner; + slot^.Error := error; Dec(ctx^.ReadPending); + if (ctx^.ReadPending = 0) and not ctx^.ReadSubmitting then readBatchFinish(ctx); +end; + +procedure readBatch(ctx : PFATTransferCtx); +var pos, remaining, idx, base, cluster, off, available, sectors, bytes, lba, size, depth : uint32; + slot : PFATReadSlot; err : TError; +begin + ctx^.ReadSubmitting := true; ctx^.ReadCount := 0; ctx^.ReadPending := 0; + pos := ctx^.BytesDone; size := ctx^.OpenFile^.VolumeInfo^.BootRecord.sectorSize; + depth := ctx^.Volume^.device^.maxActive; + if depth > 4 then depth := 4; + if depth = 0 then depth := 1; + while (ctx^.ReadCount < depth) and (pos < ctx^.ByteCount) do begin + remaining := ctx^.ByteCount-pos; + if remaining < size then break; + slot := @ctx^.ReadSlots[ctx^.ReadCount]; + slot^.Owner := ctx; slot^.Bytes := 0; slot^.Error := eNone; + Inc(ctx^.ReadCount); + if not FAT32FindRunForOffset(ctx^.OpenFile,ctx^.Offset+pos,false,0,0, + idx,base,cluster,off,available) then begin slot^.Error := eCorruptFilesystem; break; end; + bytes := remaining; if bytes > available then bytes := available; + sectors := bytes div size; + if sectors > FAT_MAX_IO_SECTORS then sectors := FAT_MAX_IO_SECTORS; + if sectors = 0 then begin slot^.Error := eInvalidArgument; break; end; + bytes := sectors*size; slot^.Bytes := bytes; + lba := FAT32ClusterToLBA(ctx^.OpenFile^.VolumeInfo,cluster) + + ((ctx^.Offset+pos) mod ctx^.OpenFile^.VolumeInfo^.BytesPerCluster) div size; + Inc(ctx^.ReadPending); + err := driver.storage.mgr.storage_read_async(ctx^.Volume^.device,lba,sectors, + puint32(uint32(ctx^.Buffer)+pos),@readSlotDone,slot); + if err <> eNone then begin Dec(ctx^.ReadPending); slot^.Error := err; break; end; + if slot^.Error <> eNone then break; { synchronous completion failure } + Inc(pos,bytes); + end; + ctx^.ReadSubmitting := false; + if ctx^.ReadPending = 0 then readBatchFinish(ctx); +end; + +procedure asyncAdvance(ctx : PFATTransferCtx); var runIdx : uint32; runFileCluster : uint32; @@ -216,12 +284,13 @@ begin ctx^.RunHintIndex := runIdx; ctx^.RunHintFileCluster := runFileCluster; - runByteOffset := currentOffset - (runFileCluster * bytesPerCluster); + runByteOffset := currentOffset mod bytesPerCluster; sectorOffset := runByteOffset mod sectorSize; ctx^.PendingLBA := FAT32ClusterToLBA(ctx^.OpenFile^.VolumeInfo, startCluster) + (runByteOffset div sectorSize); ctx^.PendingBufferOffset := ctx^.BytesDone; - if (sectorOffset <> 0) or (remaining < sectorSize) then begin + if (sectorOffset <> 0) or (remaining < sectorSize) or + (((uint32(ctx^.Buffer)+ctx^.BytesDone) and 1) <> 0) then begin chunkBytes := sectorSize - sectorOffset; if chunkBytes > remaining then chunkBytes := remaining; @@ -235,6 +304,8 @@ begin exit; end; + if ctx^.Mode = ftmRead then begin readBatch(ctx); exit; end; + chunkBytes := remaining; if chunkBytes > availableBytes then chunkBytes := availableBytes; @@ -260,6 +331,36 @@ begin asyncComplete(ctx, err); end; +{ Serialize pump state against IRQ completion and turn inline completions + into iteration, so fake/RAM drivers cannot recursively exhaust the stack. } +procedure asyncContinue(ctx : PFATTransferCtx); +var flags : uint32; +begin + asm pushfd; cli; pop flags end; + if ctx^.Pumping then ctx^.Resume := true + else begin + ctx^.Pumping := true; + repeat + ctx^.Resume := false; + asyncAdvance(ctx); + until (not ctx^.Resume) or ctx^.Finished; + ctx^.Pumping := false; + if ctx^.Finished then asyncComplete(ctx,ctx^.LastError); + end; + if (flags and $200) <> 0 then asm sti end; +end; + +type + PReadWait = ^TReadWait; + TReadWait = record Done, Bytes : uint32; Process : PProcessContext; end; +procedure readWaitDone(error : TError; data : pointer); +var wait : PReadWait; +begin + wait := PReadWait(data); + if wait^.Process <> nil then wait^.Process^.State := psReady; + puint32(@wait^.Done)^ := 1; +end; + function FAT32ReadFileAtOffset(volume : PStorage_Volume; directory : pchar; fileName : pchar; offset : uint32; buffer : puint32; byteCount : uint32; ctx : pointer) : uint32; var @@ -267,6 +368,7 @@ var tempCtx : pointer; fileSize : uint32; readLimit : uint32; + wait : TReadWait; begin FAT32ReadFileAtOffset := 0; if (buffer = nil) or (byteCount = 0) then exit; @@ -280,9 +382,16 @@ begin if ofi = nil then exit; readLimit := FAT32GetReadLimit(ofi, offset, byteCount); - if readLimit > 0 then - FAT32ReadFileAtOffset := transferSync(ofi, buffer, offset, readLimit, ftmRead, - ofi^.ScratchSector, ofi^.ScratchSize); + if readLimit > 0 then begin + wait.Done := 0; wait.Bytes := 0; wait.Process := CurrentProcess; + FAT32ReadFileAtOffsetAsync(volume,directory,fileName,offset,buffer,readLimit,ofi, + @wait.Bytes,@readWaitDone,@wait); + asm pushf; cli end; + if (puint32(@wait.Done)^ = 0) and (CurrentProcess <> nil) then CurrentProcess^.State := psAwaiting; + asm popf end; + while puint32(@wait.Done)^ = 0 do asm hlt end; + FAT32ReadFileAtOffset := wait.Bytes; + end; if tempCtx <> nil then FAT32CloseFile(tempCtx); diff --git a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.types.pas b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.types.pas index e4eef98c..6096cffb 100644 --- a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.types.pas +++ b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.types.pas @@ -99,6 +99,7 @@ type TFATRun = record StartCluster : uint32; ClusterCount : uint32; + FileClusterStart : uint32; end; TFATAllocHint = record @@ -183,6 +184,13 @@ type PFATVolumeCtx = ^TFATVolumeCtx; PFATTransferCtx = ^TFATTransferCtx; + PFATReadSlot = ^TFATReadSlot; + TFATReadSlot = record + Owner : PFATTransferCtx; + Bytes : uint32; + Error : TError; + end; + TFATTransferCtx = record Next : PFATTransferCtx; VolumeCtx : PFATVolumeCtx; @@ -209,6 +217,10 @@ type RunHintIndex : uint32; RunHintFileCluster : uint32; LastError : TError; + Pumping, Resume, Finished : boolean; + ReadSubmitting : boolean; + ReadPending, ReadCount : uint32; + ReadSlots : array[0..3] of TFATReadSlot; end; TFATVolumeCtx = record diff --git a/src/test/test.pas b/src/test/test.pas index fece7d8b..b9ddd739 100644 --- a/src/test/test.pas +++ b/src/test/test.pas @@ -34,6 +34,7 @@ uses driver.storage.vfs, driver.storage.test, driver.storage.metatest, + driver.storage.perftest, driver.storage.ctl.ahci, core.enc.fnv1a, core.enc.djb2, @@ -71,6 +72,7 @@ begin driver.storage.vfs.UnitTest; driver.storage.test.UnitTest; driver.storage.metatest.UnitTest; + driver.storage.perftest.UnitTest; driver.storage.ctl.ahci.UnitTest; core.enc.fnv1a.UnitTest; core.enc.djb2.UnitTest; -- 2.54.0 From 720ba466309ea5ee083acfb5bc5f2e22abebdcd8 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 31 Oct 2026 19:06:46 +0000 Subject: [PATCH 13/21] Improve disk benchmark suites and timing, and fix command launch race --- doc/src/driver/storage/driver.storage.test.md | 108 +++ src/arch/x86/isr/arch.x86.isr.tmr0.pas | 6 +- .../storage/driver.storage.bench.clock.pas | 284 ++++++++ src/driver/storage/driver.storage.bench.pas | 634 ++++++++++++++++++ src/driver/storage/driver.storage.test.pas | 173 ----- src/io/io.stdio.pas | 4 + src/proc/proc.mgr.pas | 27 +- src/test/test.pas | 2 + 8 files changed, 1057 insertions(+), 181 deletions(-) create mode 100644 src/driver/storage/driver.storage.bench.clock.pas create mode 100644 src/driver/storage/driver.storage.bench.pas diff --git a/doc/src/driver/storage/driver.storage.test.md b/doc/src/driver/storage/driver.storage.test.md index dff0b55c..70b2652a 100644 --- a/doc/src/driver/storage/driver.storage.test.md +++ b/doc/src/driver/storage/driver.storage.test.md @@ -58,3 +58,111 @@ Registers the `STORTEST` and `DISKTEST` shell commands with `io.stdio`. Must be - `run_tests` exercises `makeAbsolutePathFrom`, `resolvePathFrom`, `changeDirectoryFrom`, `GetDirectoryListingFrom`, `newVirtualDirectory`, and `PathValid` including `.` and `..` traversal. - `run_disk_tests` is destructive and will irreversibly wipe the partition table of disk 0. It is never called automatically. - The `Assert` helper used in both procedures increments `passed` or `failed` counters and logs the test name on failure; passing tests produce no output to avoid noise. + + +## Raw read benchmark + +`STORBENCH` is implemented and registered by `driver.storage.bench`. It performs +read-only raw-device I/O; it does not invoke the destructive `DISKTEST` path. + +```text +STORBENCH [disk] [MiB] +STORBENCH list +STORBENCH run [disk] [MiB] [KiB/request] [depth] [seq|random] [start_lba] +STORBENCH compare [disk] [MiB] +STORBENCH help +``` + +Manual (`run`) defaults are disk 0, 128 MiB, 1024 KiB/request, sequential reads, and a depth of +`min(4, hardware depth)` (at least one). Examples: + +```text +STORBENCH 0 128 64 4 +STORBENCH 0 128 4 16 random +STORBENCH 0 256 1024 1 seq 2048 +STORBENCH compare 0 128 +``` + +The comparison runs sequential reads at 4, 64, and 1024 KiB/request, each at depths +1, 4, 16, and 32. The MiB argument applies to each of the twelve cases. Queue depth +may exceed hardware depth to expose software queuing and request merging. + +Limits are 1..16384 MiB, 1..1024 KiB/request, and depth 1..32. Request size must be +an exact number of device sectors. Buffers are allocated once per case, with a +separate slice for every outstanding request and a maximum total size of 32 MiB. +The transfer range is clamped to the device. Without an explicit LBA, reads start +at sector 2048 when the full requested range fits there; otherwise they start at +zero. Random mode uses a fixed xorshift seed and samples that range with replacement, +so total bytes read are bounded but unique coverage is not guaranteed. + +Reports include successful bytes, elapsed microseconds, MiB/s, IOPS, accepted and +completed request counts, peak outstanding requests, average/p95/max completion +latency, and per-run storage-counter deltas. Latencies are measured from submission +to callback. The p95 is an upper bound from logarithmic microsecond buckets, not an exact +microsecond percentile. Runs shorter than 10 ms are excluded from suite speed comparisons. +No console output occurs inside the measured loop. Any failure stops new work; +already accepted DMA is drained before buffers are freed, and failed requests +contribute no bytes or successful IOPS. + +Timing prefers the Hyper-V-compatible VM reference counter (100 ns ticks), detected +through its Hv#1 interface and reference-counter privilege. It needs no calibration. +Other platforms use an invariant TSC calibrated against two complete RTC seconds, +with up to three attempts to tolerate transient RTC updates or scheduling delays. +Calibration requires agreement within 1%; failures include the specific reason. +Unlike delivered timer interrupts, this clock advances while interrupts are masked. +Queue submission restores interrupts between requests. Filesystem caches are +bypassed, but drive and virtual-machine/host caches may still affect results. +Virtual or sparse disks can report throughput above physical-media speed. +Device counter deltas may include concurrent I/O from other processes; +benchmark-owned byte, request, and latency counters do not. + +`driver.storage.bench.UnitTest` runs memory-only parser, arithmetic, range, +queue-depth, completion-order, and error-accounting tests at boot. It never reads +or writes a real disk. The clock tests also verify that an advertised VM reference +counter advances with interrupts masked; they never calibrate the RTC at boot. + + +### Automatic suite and table output + +`STORBENCH` without arguments, or `STORBENCH auto [disk] [MiB]`, runs six +read-only profiles. Auto defaults to disk 0 and **32 MiB per profile** (up to +192 MiB total before device-size clamping). One or two numeric arguments select the auto suite: `STORBENCH 2 512` tests +disk 2 with 512 MiB per profile. `STORBENCH list` lists disk indices, capacities, +controllers, sector sizes, and hardware queue depths. Three or more numeric +arguments retain the legacy manual syntax; `run` selects manual mode explicitly. + +| Profile | Request size | Queue depth | +| --- | --- | --- | +| Seq large | 1024 KiB | 1 | +| Seq queued | 1024 KiB | queued | +| Seq medium | 64 KiB | queued | +| Random small | 4 KiB | 1 | +| Random queued | 4 KiB | queued | +| Random medium | 64 KiB | queued | + +Queued depth is the device hardware depth clamped to 4..16, allowing software +queue testing even on serial devices. A request smaller than a device sector is +increased to one sector. Each profile samples the same byte-span size, with the +existing fixed random seed and range clamping rules. + +Auto and compare modes print an aligned ASCII table with request KiB, queue +depth, MiB/s, IOPS, average latency, p95 latency upper bound, and status. Rows are +printed outside the measured loop. Successful runs shorter than 10 ms show `-` metrics and +`SHORT`, and are excluded from best-result and queue-gain summaries. Errors show +`FAILED`, stop the suite after accepted DMA has drained, and leave remaining +profiles unrun. The summary includes successful profiles, bytes actually read, +measured time, best sequential throughput, peak random IOPS, and (in auto mode) +queued/QD1 throughput ratios. These ratios describe the measured cases, including +any controller-cache effects; they are not a hardware rating. + +```text +STORBENCH +STORBENCH list +STORBENCH 2 512 +STORBENCH auto 0 64 +STORBENCH compare 0 32 +``` + +Profiles reuse the same raw device range. Host/drive caches are not flushed, so +later profiles may read warm data. These results are observed raw read throughput, +not a claim of uncached physical-media performance. diff --git a/src/arch/x86/isr/arch.x86.isr.tmr0.pas b/src/arch/x86/isr/arch.x86.isr.tmr0.pas index 0408693c..9dd86a7e 100644 --- a/src/arch/x86/isr/arch.x86.isr.tmr0.pas +++ b/src/arch/x86/isr/arch.x86.isr.tmr0.pas @@ -27,6 +27,10 @@ uses arch.x86.isr.mgr, arch.x86.idt; +const + PIT_INPUT_HZ = 1193182; + PIT_DIVISOR = 149; + procedure register(); procedure hook(hook_method : uint32); procedure unhook(hook_method : uint32); @@ -54,7 +58,7 @@ procedure register(); begin if not registered then begin asm - mov ax, 149 + mov ax, PIT_DIVISOR out $40, al mov al, ah out $40, al diff --git a/src/driver/storage/driver.storage.bench.clock.pas b/src/driver/storage/driver.storage.bench.clock.pas new file mode 100644 index 00000000..e631301c --- /dev/null +++ b/src/driver/storage/driver.storage.bench.clock.pas @@ -0,0 +1,284 @@ +{ Benchmark time must advance while IRQ delivery is masked. + Prefer the fixed-rate Hyper-V reference counter; RTC-calibrated TSC is a fallback. } +unit driver.storage.bench.clock; +interface +function prepare : boolean; +function ready : boolean; +function nowUS : uint64; +function sourceName : pchar; +function errorReason : pchar; +procedure UnitTest; +implementation +uses arch.x86.util, io.syslog; +var clockReady, clockBusy, referenceClock : boolean; + failure : pchar; + frequency, epoch : uint64; + +{ Hv#1 is an interface signature, not a vendor string: VirtualBox also + implements it. Never probe a synthetic MSR without its advertised privilege. + https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/tlfs/timers } +function referenceAvailable(cpuECX, cpuEDX, maxLeaf, signature, privileges : uint32) : boolean; +begin + referenceAvailable := ((cpuECX and $80000000)<>0) and + ((cpuEDX and $20)<>0) and (maxLeaf >= $40000003) and + (signature=$31237648) and ((privileges and 2)<>0); +end; +function referenceSupported : boolean; +var cpuECX, cpuEDX, maxLeaf, signature, privileges : uint32; +begin + referenceSupported:=false; + asm + push ebx + mov eax,1 + cpuid + mov cpuECX,ecx + mov cpuEDX,edx + pop ebx + end; + if ((cpuECX and $80000000)=0) or ((cpuEDX and $20)=0) then exit; + asm + push ebx + mov eax,$40000000 + cpuid + mov maxLeaf,eax + mov eax,$40000001 + cpuid + mov signature,eax + pop ebx + end; + if (maxLeaf<$40000003) or (signature<>$31237648) then exit; + asm + push ebx + mov eax,$40000003 + cpuid + mov privileges,eax + pop ebx + end; + referenceSupported:=referenceAvailable(cpuECX,cpuEDX,maxLeaf,signature,privileges); +end; +function readReference : uint64; +var hi, lo : uint32; +begin + asm + mov ecx,$40000020 + rdmsr + mov lo,eax + mov hi,edx + end; + readReference:=(uint64(hi) shl 32) or lo; +end; +function referenceUS(value : uint64) : uint64; +begin referenceUS:=value div 10; end; { reference ticks are 100 ns } +function sourceName : pchar; +begin + if not clockReady then sourceName:='unavailable' + else if referenceClock then sourceName:='VM reference clock (no calibration)' + else sourceName:='RTC-calibrated invariant TSC'; +end; +function errorReason : pchar; +begin + if failure=nil then errorReason:='Clock initialization is already running; retry shortly.' + else errorReason:=failure; +end; + +function readCycles : uint64; +var hi, lo : uint32; +begin + { prepare checks SSE2 and invariant TSC before this is ever used. } + asm + lfence + rdtsc + mov lo,eax + mov hi,edx + lfence + end; + readCycles := (uint64(hi) shl 32) or lo; +end; +function supported : boolean; +var basic, extended, features, invariant : uint32; +begin + asm + push ebx + xor eax,eax + cpuid + mov basic,eax + mov eax,1 + cpuid + mov features,edx + mov eax,$80000000 + cpuid + mov extended,eax + pop ebx + end; + supported := false; + if (basic<1) or ((features and $04000010)<>$04000010) or (extended<$80000007) then exit; + asm + push ebx + mov eax,$80000007 + cpuid + mov invariant,edx + pop ebx + end; + supported := (invariant and $100)<>0; +end; +function toUS(cycles, hz : uint64) : uint64; +begin + toUS := 0; + if hz=0 then exit; + { Split before multiplying: raw TSC values can exceed 32 bits every second. } + toUS := (cycles div hz)*1000000 + ((cycles mod hz)*1000000) div hz; +end; +function acceptSamples(first, second : uint64; var hz : uint64) : boolean; +var delta : uint64; +begin + acceptSamples := false; hz := 0; + if (first<1000000) or (first>20000000000) or (second<1000000) or (second>20000000000) then exit; + if first>second then delta:=first-second else delta:=second-first; + if delta>first div 100 then exit; { reject >1% calibration disagreement } + hz := (first+second) div 2; acceptSamples := true; +end; +function rtcSecond(var seconds : uint32) : boolean; +var flags : uint32; before, after, raw, again, mode : uint8; +begin + { Keep each CMOS index/data transaction together, but never wait under CLI. } + asm pushfd; cli; pop flags end; + outb($70,$0A); before:=inb($71); + outb($70,$00); raw:=inb($71); + outb($70,$00); again:=inb($71); + outb($70,$0B); mode:=inb($71); + outb($70,$0A); after:=inb($71); + if (flags and $200)<>0 then asm sti end; + rtcSecond := false; + if (((before or after) and $80)<>0) or (raw<>again) then exit; + if (mode and 4)<>0 then seconds:=raw + else begin + if ((raw and $0F)>9) or ((raw shr 4)>5) then exit; + seconds:=(raw shr 4)*10+(raw and $0F); + end; + rtcSecond := seconds<60; +end; +function nextSecond(var previous : uint32; var stamp : uint64) : boolean; +var i, current : uint32; start, now : uint64; +begin + nextSecond:=false; start:=readCycles; + for i:=1 to 2000000 do begin + if rtcSecond(current) then if current<>previous then begin + if current<>(previous+1) mod 60 then exit; + previous:=current; stamp:=readCycles; nextSecond:=true; exit; + end; + now:=readCycles; + if (now40000000000) then exit; + end; +end; +function prepare : boolean; +var flags, second, attempt, i : uint32; + first, middle, last, hz : uint64; ok : boolean; +begin + prepare:=false; + asm pushfd; cli; pop flags end; + if clockReady then begin + if (flags and $200)<>0 then asm sti end; + prepare:=true; exit; + end; + if clockBusy then begin + if (flags and $200)<>0 then asm sti end; + exit; + end; + clockBusy:=true; failure:=nil; + if (flags and $200)<>0 then asm sti end; + ok:=false; + if referenceSupported then begin + first:=readReference; + for i:=1 to 10000 do begin + last:=readReference; + if last=10 then begin ok:=true; break; end; + end; + if ok then begin + asm cli end; + referenceClock:=true; epoch:=first; clockReady:=true; clockBusy:=false; + if (flags and $200)<>0 then asm sti end; + io.syslog.logln('BENCHCLOCK','Using VM reference clock; no calibration.'); + prepare:=true; exit; + end; + failure:='VM reference clock did not advance.'; + end; + if (flags and $200)=0 then failure:='TSC fallback needs interrupts enabled.' + else if not supported then failure:='No VM reference clock or invariant TSC available.' + else begin + { A transient RTC update or a delayed VM timeslice must not permanently + reject an otherwise usable clock. Retry bounded full-second samples. } + for attempt:=1 to 3 do begin + ok:=false; + for i:=1 to 10000 do if rtcSecond(second) then begin ok:=true; break; end; + if ok then ok:=nextSecond(second,first); + if ok then ok:=nextSecond(second,middle); + if ok then ok:=nextSecond(second,last); + if ok then begin + ok:=(middle>first) and (last>middle); + if ok then ok:=acceptSamples(middle-first,last-middle,hz); + end; + if ok then break; + end; + if not ok then failure:='RTC/TSC fallback samples were unstable; retry when the system is idle.'; + end; + asm cli end; + if ok then begin frequency:=hz; epoch:=last; referenceClock:=false; clockReady:=true; end; + clockBusy:=false; + if (flags and $200)<>0 then asm sti end; + prepare:=ok; +end; +function ready : boolean; +begin ready:=clockReady; end; +function nowUS : uint64; +var value : uint64; +begin + nowUS:=0; + if not clockReady then exit; + if referenceClock then begin + value:=readReference; + if value>=epoch then nowUS:=referenceUS(value-epoch); + end else begin + value:=readCycles; + if value>=epoch then nowUS:=toUS(value-epoch,frequency); + end; +end; +procedure UnitTest; +var hz, first, last : uint64; passed, failed, flags, i : uint32; + procedure Assert(ok : boolean; name : pchar); + begin if ok then Inc(passed) else begin Inc(failed); io.syslog.logln('BENCHCLOCK FAIL',name); end; end; +begin + passed:=0;failed:=0; + Assert(referenceAvailable($80000000,$20,$40000006,$31237648,$AF2),'VBox Hv#1 reference clock'); + Assert(not referenceAvailable(0,$20,$40000006,$31237648,$AF2),'no hypervisor no MSR'); + Assert(not referenceAvailable($80000000,0,$40000006,$31237648,$AF2),'no RDMSR support'); + Assert(not referenceAvailable($80000000,$20,$40000002,$31237648,$AF2),'missing feature leaf'); + Assert(not referenceAvailable($80000000,$20,$40000006,0,$AF2),'different hypervisor interface'); + Assert(not referenceAvailable($80000000,$20,$40000006,$31237648,$AF0),'missing reference privilege'); + Assert(referenceUS(10000000)=1000000,'100 ns ticks to microseconds'); + Assert(referenceUS(50000000000)=5000000000,'reference conversion beyond uint32'); + Assert(referenceUS(9)=0,'reference sub-microsecond truncation'); + { Safe hardware smoke test only when CPUID explicitly grants MSR access. + No disk I/O and no RTC calibration occurs during boot tests. } + if referenceSupported then begin + Assert(prepare,'VM reference clock initialization'); + if ready then begin + asm pushfd; cli; pop flags end; + first:=nowUS; last:=first; + for i:=1 to 10000 do begin last:=nowUS; if last>first then break; end; + if (flags and $200)<>0 then asm sti end; + Assert(last>first,'VM clock advances with interrupts masked'); + end; + end; + Assert(acceptSamples(4291845105,4291845105,hz) and (hz=4291845105),'near uint32 frequency boundary'); + Assert(acceptSamples(5000000000,5000000000,hz) and (hz=5000000000),'frequency above uint32'); + Assert(not acceptSamples(5000000000,5100000000,hz),'reject unstable calibration'); + Assert(not acceptSamples(0,0,hz),'reject missing clock'); + Assert(toUS(10000000000,5000000000)=2000000,'64-bit elapsed conversion'); + Assert(toUS(5000002500,5000000000)=1000000,'fractional microsecond truncation'); + Assert(toUS(18000000000000,5000000000)=3600000000,'one hour does not overflow'); + Assert(toUS(123,0)=0,'invalid divisor'); + io.syslog.log('BENCHCLOCK','Passed: '); io.syslog.writeint(passed); + io.syslog.writestring(' Failed: '); io.syslog.writeintln(failed); +end; +end. diff --git a/src/driver/storage/driver.storage.bench.pas b/src/driver/storage/driver.storage.bench.pas new file mode 100644 index 00000000..6406cc05 --- /dev/null +++ b/src/driver/storage/driver.storage.bench.pas @@ -0,0 +1,634 @@ +{ Read-only raw storage benchmark. No disk I/O is performed by UnitTest. } +unit driver.storage.bench; +interface +procedure init; +procedure UnitTest; +implementation +uses boot.mgr, io.stdio, io.syslog, core.strings, core.util, arch.x86.util, + arch.x86.bda, arch.x86.isr.tmr0, memory.heap, proc.mgr, proc.types, + driver.storage.types, driver.storage.mgr, core.ds.cfifo, driver.storage.bench.clock; +const + MAX_DEPTH = 32; + DEFAULT_MIB = 128; + AUTO_MIB = 32; + MIN_SAMPLE_US = 10000; { avoid presenting millisecond cache hits as drive ratings } +type + PCaseResult = ^TCaseResult; + TCaseResult = record + Name : pchar; + KiB, Depth : uint32; + RandomRead, Measured : boolean; + Error : TError; + Rate10, IOPS, AvgUS, P95US, Bytes : uint64; + Elapsed : uint64; + end; + PBench = ^TBench; + PSlot = ^TSlot; + TSlot = record + Owner : PBench; + Buffer : puint32; + State, LBA, Sectors : uint32; { State: 0 free, 1 DMA, 2 completed } + Started : uint64; + end; + TBench = record + Device : PStorage_Device; + Process : PProcessContext; + Buffer : pointer; + Depth, RequestBytes, ChunkSectors, StartLBA, Span, Remaining, Seed : uint32; + RandomRead : boolean; + Active, Peak, WakeSequence, Accepted, Completed, Successful : uint32; + Error : TError; + ErrorLBA : uint32; + StartTick, Elapsed, LatencyMax : uint64; + Bytes, LatencyTotal : uint64; + Histogram : array[0..31] of uint32; + BeforeStats, AfterStats : TStorageStats; + Slots : array[0..MAX_DEPTH-1] of TSlot; + end; + +function nowTick : uint64; +begin nowTick := driver.storage.bench.clock.nowUS; end; + +{ Reject overflow before multiplication; CLI parsing must never wrap. } +function parseNumber(text : pchar; limit : uint32; var value : uint32) : boolean; +var i, digit : uint32; +begin + parseNumber := false; value := 0; + if (text=nil) or (text[0]=#0) then exit; + i := 0; + while text[i]<>#0 do begin + if (text[i]<'0') or (text[i]>'9') then exit; + digit := ord(text[i])-ord('0'); + if (value>limit div 10) or ((value=limit div 10) and (digit>limit mod 10)) then exit; + value := value*10+digit; Inc(i); + end; + parseNumber := true; +end; + +function ticksToUS(ticks : uint64) : uint64; +begin ticksToUS := ticks; end; { samples now use independent hardware microseconds } +function perSecond(value, ticks : uint64) : uint64; +begin + perSecond := 0; + if ticks<>0 then perSecond := (value*1000000) div ticks; +end; +function latencyBucket(ticks : uint64) : uint32; +var bucket : uint32; +begin + bucket := 0; + while (ticks<>0) and (bucket<31) do begin Inc(bucket); ticks := ticks shr 1; end; + latencyBucket := bucket; +end; +function p95Ticks(run : PBench) : uint64; +var target, count, i : uint32; +begin + p95Ticks := 0; + if run^.Completed=0 then exit; + target := run^.Completed - run^.Completed div 20; count := 0; + for i := 0 to 31 do begin + Inc(count,run^.Histogram[i]); + if count>=target then begin + if i=31 then p95Ticks := run^.LatencyMax + else p95Ticks := (uint32(1) shl i)-1; + exit; + end; + end; +end; + +procedure writeNumber(output : POutBuf; value : uint64); +var digits : array[0..20] of char; n : uint32; +begin + n := 0; + repeat digits[n] := char(ord('0')+value mod 10); value := value div 10; Inc(n); until value=0; + while n>0 do begin Dec(n); bufWriteChar(output,digits[n]); end; +end; +procedure writeValue(output : POutBuf; name : pchar; value : uint64); +begin bufWriteStr(output,name); writeNumber(output,value); end; + +{ All geometry arithmetic is bounded before buffer allocation or I/O. } +function configure(run : PBench; device : PStorage_Device; mib, kib, depth, start : uint32; + explicitStart, randomRead : boolean) : boolean; +var sectors : uint64; +begin + configure := false; + if device=nil then exit; + if device^.removed or (device^.dispatchRead=nil) or (device^.maxSectorCount=0) then exit; + if (device^.sectorSize=0) or (device^.sectorSize>1048576) then exit; + if (mib=0) or (mib>16384) or (kib=0) or (kib>1024) or (depth=0) or (depth>MAX_DEPTH) then exit; + if ((kib*1024) mod device^.sectorSize)<>0 then exit; + sectors := (uint64(mib) shl 20) div device^.sectorSize; + if not explicitStart then begin + start := 0; + if device^.maxSectorCount>2048 then + if sectors<=device^.maxSectorCount-2048 then start := 2048; + end; + if start>=device^.maxSectorCount then exit; + if sectors>device^.maxSectorCount-start then sectors := device^.maxSectorCount-start; + if sectors=0 then exit; + run^.Device := device; run^.Depth := depth; run^.RequestBytes := kib*1024; + run^.ChunkSectors := run^.RequestBytes div device^.sectorSize; + run^.StartLBA := start; run^.Span := uint32(sectors); run^.Remaining := run^.Span; + run^.Seed := $6D2B79F5; run^.RandomRead := randomRead; + configure := true; +end; + +procedure planRead(run : PBench; slot : PSlot); +var span : uint32; +begin + slot^.Sectors := run^.ChunkSectors; + if slot^.Sectors>run^.Remaining then slot^.Sectors := run^.Remaining; + if run^.RandomRead then begin + run^.Seed := run^.Seed xor (run^.Seed shl 13); + run^.Seed := run^.Seed xor (run^.Seed shr 17); + run^.Seed := run^.Seed xor (run^.Seed shl 5); + span := run^.Span-slot^.Sectors+1; + slot^.LBA := run^.StartLBA + run^.Seed mod span; + end else slot^.LBA := run^.StartLBA + (run^.Span-run^.Remaining); + Dec(run^.Remaining,slot^.Sectors); +end; + +procedure readDone(error : TError; data : pointer); +var slot : PSlot; run : PBench; ticks : uint64; +begin + slot := PSlot(data); run := slot^.Owner; + ticks := nowTick-slot^.Started; + Inc(run^.Completed); Inc(run^.LatencyTotal,ticks); + if ticks>run^.LatencyMax then run^.LatencyMax := ticks; + Inc(run^.Histogram[latencyBucket(ticks)]); + if error=eNone then begin + Inc(run^.Successful); Inc(run^.Bytes,uint64(slot^.Sectors)*run^.Device^.sectorSize); + end else if run^.Error=eNone then begin run^.Error := error; run^.ErrorLBA := slot^.LBA; end; + Dec(run^.Active); slot^.State := 2; + if run^.Process<>nil then run^.Process^.State := psReady; + Inc(run^.WakeSequence); +end; + +{ Task context only. Reserve before submit to support immediate callbacks. + Stop submitting on the first error, but drain accepted DMA before freeing. } +procedure execute(run : PBench); +var i, flags, wake, attemptedActive : uint32; slot : PSlot; err : TError; finished, ready : boolean; +begin + run^.Process := CurrentProcess; + for i := 0 to run^.Depth-1 do begin + run^.Slots[i].Owner := run; + run^.Slots[i].Buffer := puint32(uint32(run^.Buffer)+i*run^.RequestBytes); + end; + storage_get_stats(run^.Device,run^.BeforeStats); run^.StartTick := nowTick; + repeat + for i := 0 to run^.Depth-1 do begin + asm pushfd; cli; pop flags end; + slot := @run^.Slots[i]; + if slot^.State=2 then slot^.State := 0; + if (slot^.State=0) and (run^.Remaining>0) and (run^.Error=eNone) then begin + planRead(run,slot); slot^.State := 1; slot^.Started := nowTick; + Inc(run^.Active); Inc(run^.Accepted); attemptedActive := run^.Active; + err := storage_read_async(run^.Device,slot^.LBA,slot^.Sectors,slot^.Buffer,@readDone,slot); + if err<>eNone then begin + Dec(run^.Active); Dec(run^.Accepted); slot^.State := 0; + if run^.Error=eNone then begin run^.Error := err; run^.ErrorLBA := slot^.LBA; end; + end; + if (err=eNone) and (attemptedActive>run^.Peak) then run^.Peak := attemptedActive; + end; + if (flags and $200)<>0 then asm sti end; + end; + asm pushfd; cli; pop flags end; + finished := (run^.Active=0) and ((run^.Remaining=0) or (run^.Error<>eNone)); + ready := false; + for i := 0 to run^.Depth-1 do if run^.Slots[i].State=2 then ready := true; + wake := run^.WakeSequence; + if not finished and not ready and (run^.Process<>nil) then run^.Process^.State := psAwaiting; + if (flags and $200)<>0 then asm sti end; + if not finished and not ready then + while puint32(@run^.WakeSequence)^=wake do asm hlt end; + until finished; + run^.Elapsed := nowTick-run^.StartTick; + storage_get_stats(run^.Device,run^.AfterStats); +end; + +procedure report(run : PBench; output, errors : POutBuf); +var rate, ticks, commands : uint64; +begin + if run^.Error<>eNone then begin + writeValue(errors,'Read failed at LBA ',run^.ErrorLBA); + writeValue(errors,', error=',ord(run^.Error)); + bufWriteStrLn(errors,'. All accepted reads drained; results below are partial.'); + end; + writeValue(output,' Read ',run^.Bytes div 1024); bufWriteStr(output,' KiB'); + writeValue(output,' in ',ticksToUS(run^.Elapsed)); bufWriteStr(output,' us; '); + if run^.Elapsed0 then begin + writeValue(output,' Latency avg: ',ticksToUS(run^.LatencyTotal) div run^.Completed); + ticks := p95Ticks(run); + if ticks=0 then writeValue(output,' us; p95 < ',ticksToUS(1)+1) + else writeValue(output,' us; p95 < ',ticksToUS(ticks+1)+1); + writeValue(output,' us; max < ',ticksToUS(uint64(run^.LatencyMax)+1)+1); + bufWriteStrLn(output,' us (hardware clock; p95 uses log buckets)'); + end; + commands := uint32(run^.AfterStats.Commands-run^.BeforeStats.Commands); + writeValue(output,' Device deltas: commands=',commands); + writeValue(output,', merged=',uint32(run^.AfterStats.Merged-run^.BeforeStats.Merged)); + writeValue(output,', failed=',uint32(run^.AfterStats.Failed-run^.BeforeStats.Failed)); + writeValue(output,', queue-full=',uint32(run^.AfterStats.QueueFull-run^.BeforeStats.QueueFull)); + bufWriteNewLine(output); + +end; + +function runCase(device : PStorage_Device; mib, kib, depth, start : uint32; + explicitStart, randomRead : boolean; output, errors : POutBuf; + summary : PCaseResult) : boolean; +var run : PBench; +begin + runCase := false; + run := PBench(kalloc(sizeof(TBench))); + if summary<>nil then summary^.Error := eOutOfMemory; + if run=nil then begin bufWriteStrLn(errors,'Out of memory allocating benchmark state.'); exit; end; + memset(uint32(run),0,sizeof(TBench)); + if not configure(run,device,mib,kib,depth,start,explicitStart,randomRead) then begin + if summary<>nil then summary^.Error := eInvalidArgument; + bufWriteStrLn(errors,'Invalid/unreadable device, range, or request size (must be a whole number of device sectors).'); + kfree(void(run)); exit; + end; + run^.Buffer := kalloc(run^.Depth*run^.RequestBytes); + if run^.Buffer=nil then begin bufWriteStrLn(errors,'Out of memory allocating benchmark buffers.'); kfree(void(run)); exit; end; + memset(uint32(run^.Buffer),0,run^.Depth*run^.RequestBytes); + if summary=nil then begin + if randomRead then bufWriteStr(output,'Random') else bufWriteStr(output,'Sequential'); + writeValue(output,' read: ',kib); writeValue(output,' KiB/request, depth=',depth); + writeValue(output,', LBA=',run^.StartLBA); + writeValue(output,', span=',uint64(run^.Span)*device^.sectorSize div 1024); bufWriteStrLn(output,' KiB'); + end; + execute(run); runCase := run^.Error=eNone; + if summary=nil then report(run,output,errors) + else begin + summary^.Error := run^.Error; summary^.Bytes := run^.Bytes; + summary^.Elapsed := run^.Elapsed; summary^.Measured := run^.Elapsed>=MIN_SAMPLE_US; + if summary^.Measured then begin + summary^.Rate10 := perSecond(run^.Bytes*10,run^.Elapsed) div 1048576; + summary^.IOPS := perSecond(run^.Successful,run^.Elapsed); + end; + if run^.Completed>0 then begin + summary^.AvgUS := ticksToUS(run^.LatencyTotal) div run^.Completed; + summary^.P95US := ticksToUS(uint64(p95Ticks(run))+1)+1; + end; + if run^.Error<>eNone then begin + writeValue(errors,'Read failed at LBA ',run^.ErrorLBA); + writeValue(errors,', error=',ord(run^.Error)); bufWriteStrLn(errors,'. Accepted reads drained.'); + end; + end; + kfree(run^.Buffer); kfree(void(run)); +end; + +{ Fixed-width ASCII output stays readable in terminals and redirected logs. } +procedure textCell(output : POutBuf; text : pchar; width : uint32); +var i : uint32; +begin + i := 0; + while (i#0) do begin bufWriteChar(output,text[i]); Inc(i); end; + while ieNone then bufWriteStrLn(output,' FAILED') else bufWriteStrLn(output,' SHORT'); + end; +end; +function suiteProfile(index : uint32; automatic : boolean; hardwareDepth : uint32; + summary : PCaseResult) : boolean; +var queued : uint32; +begin + suiteProfile := false; + memset(uint32(summary),0,sizeof(TCaseResult)); + queued := hardwareDepth; if queued<4 then queued:=4; if queued>16 then queued:=16; + if automatic then begin + if index>=6 then exit; + case index of + 0:begin summary^.Name:='Seq large'; summary^.KiB:=1024; summary^.Depth:=1; end; + 1:begin summary^.Name:='Seq queued'; summary^.KiB:=1024; summary^.Depth:=queued; end; + 2:begin summary^.Name:='Seq medium'; summary^.KiB:=64; summary^.Depth:=queued; end; + 3:begin summary^.Name:='Random small'; summary^.KiB:=4; summary^.Depth:=1; end; + 4:begin summary^.Name:='Random queued'; summary^.KiB:=4; summary^.Depth:=queued; end; + 5:begin summary^.Name:='Random medium'; summary^.KiB:=64; summary^.Depth:=queued; end; + end; + summary^.RandomRead := index>=3; + end else begin + if index>=12 then exit; + summary^.Name := 'Sequential'; + case index div 4 of 0:summary^.KiB:=4; 1:summary^.KiB:=64; 2:summary^.KiB:=1024; end; + case index mod 4 of 0:summary^.Depth:=1; 1:summary^.Depth:=4; 2:summary^.Depth:=16; 3:summary^.Depth:=32; end; + end; + suiteProfile := true; +end; +procedure queueGain(output : POutBuf; name : pchar; baseline, queued : PCaseResult); +begin + if (baseline^.Error<>eNone) or (queued^.Error<>eNone) or + not baseline^.Measured or not queued^.Measured or (baseline^.Rate10=0) then exit; + bufWriteStr(output,name); numberCell(output,(queued^.Rate10*10) div baseline^.Rate10,0,true); + bufWriteStrLn(output,'x QD1 throughput'); +end; +procedure runSuite(device : PStorage_Device; disk, mib : uint32; automatic : boolean; + output, errors : POutBuf); +var summaries, row : PCaseResult; count, i, completed, bestSeq, bestRandom : uint32; + totalBytes, totalTicks : uint64; ok : boolean; +begin + if automatic then count:=6 else count:=12; + summaries := PCaseResult(kalloc(count*sizeof(TCaseResult))); + if summaries=nil then begin bufWriteStrLn(errors,'Out of memory allocating results.'); exit; end; + memset(uint32(summaries),0,count*sizeof(TCaseResult)); + bufWriteNewLine(output); tableLine(output); + if automatic then bufWriteStrLn(output,' STORAGE BENCHMARK / AUTO') else bufWriteStrLn(output,' STORAGE BENCHMARK / COMPARE'); + writeValue(output,' Disk ',disk); writeValue(output,' | ',mib); writeValue(output,' MiB/profile | ',count); + writeValue(output,' profiles | hardware QD ',device^.maxActive); bufWriteNewLine(output); + bufWriteStrLn(output,' Raw read throughput (includes host/drive cache effects).'); + bufWriteStrLn(output,' Asuro filesystem cache bypassed. Host/drive caches are not flushed.'); + bufWriteStrLn(output,' Profiles reuse the same range; later reads may be warm-cache hits.'); + writeValue(output,' Device capacity: ',(uint64(device^.maxSectorCount)*device^.sectorSize) div 1048576); bufWriteStrLn(output,' MiB'); + if (uint64(mib) shl 20)>uint64(device^.maxSectorCount)*device^.sectorSize then + bufWriteStrLn(output,' Requested size exceeds this disk; each profile is clamped to capacity.'); + tableHeader(output); + completed:=0; bestSeq:=$FFFFFFFF; bestRandom:=$FFFFFFFF; totalBytes:=0; totalTicks:=0; + for i:=0 to count-1 do begin + row:=@summaries[i]; + if not suiteProfile(i,automatic,device^.maxActive,row) then break; + { Keep the small-request profiles usable on larger-sector devices. } + if (row^.KiB*1024summaries[bestSeq].Rate10 then bestSeq:=i; + end else begin + if bestRandom=$FFFFFFFF then bestRandom:=i + else if row^.IOPS>summaries[bestRandom].IOPS then bestRandom:=i; + end; + end; + end; + tableLine(output); + if bestSeq<>$FFFFFFFF then begin + bufWriteStr(output,' Best sequential: '); numberCell(output,summaries[bestSeq].Rate10,0,true); + writeValue(output,' MiB/s (',summaries[bestSeq].KiB); writeValue(output,' KiB, QD ',summaries[bestSeq].Depth); bufWriteStrLn(output,')'); + end; + if bestRandom<>$FFFFFFFF then begin + writeValue(output,' Peak random: ',summaries[bestRandom].IOPS); bufWriteStr(output,' IOPS ('); + writeNumber(output,summaries[bestRandom].KiB); writeValue(output,' KiB, QD ',summaries[bestRandom].Depth); bufWriteStrLn(output,')'); + end; + if automatic then begin + if completed>=2 then queueGain(output,' Sequential queue gain: ',@summaries[0],@summaries[1]); + if completed>=5 then queueGain(output,' Small random queue gain: ',@summaries[3],@summaries[4]); + end; + writeValue(output,' Finished ',completed); writeValue(output,'/',count); + writeValue(output,' profiles | read ',totalBytes div 1024); + writeValue(output,' KiB | measured ',ticksToUS(totalTicks) div 1000); bufWriteStrLn(output,' ms'); + if completed2 then commandMode:=bmManual; { retain detailed numeric syntax } +end; +procedure usage(output : POutBuf); +begin + bufWriteStrLn(output,'STORBENCH [disk] [MiB] Default six-profile suite'); + bufWriteStrLn(output,'STORBENCH auto [disk] [MiB] Same suite (default 32 MiB/profile)'); + bufWriteStrLn(output,'STORBENCH list List disks and capacities'); + bufWriteStrLn(output,'STORBENCH compare [disk] [MiB] Twelve sequential cases'); + bufWriteStrLn(output,'STORBENCH run [disk] [MiB] [KiB/request] [depth] [seq|random] [start_lba]'); + bufWriteStrLn(output,'Examples: STORBENCH 2 512 STORBENCH run 2 512 64 4'); + bufWriteStrLn(output,'Manual defaults: disk 0, 128 MiB, 1024 KiB/request, depth up to 4, sequential.'); + bufWriteStrLn(output,'Limits: 1..16384 MiB, 1..1024 KiB/request, depth 1..32; sizes clamp to disk.'); + bufWriteStrLn(output,'Read-only. Cached/sparse VM reads do not measure physical media speed.'); +end; +procedure listDisks(output : POutBuf); +var count, i : uint32; device : PStorage_Device; +begin + count:=get_device_count; + if count=0 then begin bufWriteStrLn(output,'No storage devices.'); exit; end; + bufWriteStrLn(output,'Disk Controller MiB Sector bytes HW QD'); + tableLine(output); + for i:=0 to count-1 do begin + device:=get_device(i); + if device<>nil then begin + numberCell(output,i,4,false); bufWriteStr(output,' '); + textCell(output,controller_type_2_string(device^.controller),16); + numberCell(output,(uint64(device^.maxSectorCount)*device^.sectorSize) div 1048576,9,false); + numberCell(output,device^.sectorSize,17,false); numberCell(output,device^.maxActive,7,false); + if device^.removed then bufWriteStr(output,' removed'); + bufWriteNewLine(output); + end; + end; + bufWriteStrLn(output,'Select a disk and MiB per profile: STORBENCH 2 512'); +end; +procedure command(params : PParamList; stdin_buf, stdout_buf, stderr_buf : POutBuf); +var count, disk, mib, kib, depth, start, base : uint32; + mode : TBenchMode; randomRead, explicitStart : boolean; device : PStorage_Device; first : pchar; +begin + count:=paramCount(params); first:=nil; + if count>0 then first:=getParam(0,params); + mode:=commandMode(first,count,base); + if mode=bmHelp then begin usage(stdout_buf); exit; end; + if mode=bmList then begin + if count<>1 then usage(stderr_buf) else listDisks(stdout_buf); + exit; + end; + if ((mode<>bmManual) and (count>base+2)) or ((mode=bmManual) and (count>base+6)) then begin usage(stderr_buf); exit; end; + disk:=0; mib:=DEFAULT_MIB; kib:=1024; start:=0; + if mode=bmAuto then mib:=AUTO_MIB; + randomRead:=false; explicitStart:=false; + if count>base then if not parseNumber(getParam(base,params),$FFFFFFFF,disk) then begin usage(stderr_buf); exit; end; + if count>base+1 then if not parseNumber(getParam(base+1,params),16384,mib) or (mib=0) then begin usage(stderr_buf); exit; end; + device:=get_device(disk); + if device=nil then begin bufWriteStrLn(stderr_buf,'Storage device not found. Use STORBENCH list.'); exit; end; + depth:=device^.maxActive; if depth>4 then depth:=4; if depth=0 then depth:=1; + if mode=bmManual then begin + if count>base+2 then if not parseNumber(getParam(base+2,params),1024,kib) or (kib=0) then begin usage(stderr_buf); exit; end; + if count>base+3 then if not parseNumber(getParam(base+3,params),MAX_DEPTH,depth) or (depth=0) then begin usage(stderr_buf); exit; end; + if count>base+4 then begin + randomRead:=stringEqualsCI(getParam(base+4,params),'random'); + if not randomRead and not stringEqualsCI(getParam(base+4,params),'seq') then begin usage(stderr_buf); exit; end; + end; + if count>base+5 then begin + explicitStart:=true; + if not parseNumber(getParam(base+5,params),$FFFFFFFF,start) then begin usage(stderr_buf); exit; end; + end; + end; + if not driver.storage.bench.clock.prepare then begin + bufWriteStr(stderr_buf,'Benchmark clock unavailable: '); + bufWriteStrLn(stderr_buf,driver.storage.bench.clock.errorReason); exit; + end; + bufWriteStr(stdout_buf,'Clock: '); bufWriteStrLn(stdout_buf,driver.storage.bench.clock.sourceName); + if mode<>bmManual then begin runSuite(device,disk,mib,mode=bmAuto,stdout_buf,stderr_buf); exit; end; + writeValue(stdout_buf,'Disk ',disk); writeValue(stdout_buf,', sector bytes=',device^.sectorSize); + writeValue(stdout_buf,', hardware depth=',device^.maxActive); bufWriteNewLine(stdout_buf); + bufWriteStrLn(stdout_buf,'Virtual/cached/sparse reads can exceed physical media speed. Device deltas include background I/O.'); + runCase(device,mib,kib,depth,start,explicitStart,randomRead,stdout_buf,stderr_buf,nil); +end; + +type + PBenchMock = ^TBenchMock; + TBenchMock = record + Pending : array[0..3] of PIORequest; + Count, Batch, Calls, FailLBA : uint32; + InvalidRange : boolean; + end; +procedure testDispatch(device : PStorage_Device; request : PIORequest); +var mock : PBenchMock; pending : PIORequest; +begin + mock := PBenchMock(device^.controllerId0); Inc(mock^.Calls); + if (request^.RequestType<>ioRead) or (request^.LBA>=device^.maxSectorCount) or + (request^.SectorCount>device^.maxSectorCount-request^.LBA) then mock^.InvalidRange := true; + mock^.Pending[mock^.Count] := request; Inc(mock^.Count); + if mock^.Count=mock^.Batch then while mock^.Count>0 do begin + Dec(mock^.Count); pending := mock^.Pending[mock^.Count]; + if pending^.LBA=mock^.FailLBA then complete_io(pending,false,eIOError) + else complete_io(pending,true,eNone); + end; +end; + +procedure UnitTest; +var run : PBench; device : PStorage_Device; slot : PSlot; value, i, passed, failed : uint32; + mock : PBenchMock; buffer : pointer; summary : PCaseResult; + procedure Assert(ok : boolean; name : pchar); + begin if ok then Inc(passed) else begin Inc(failed); io.syslog.logln('STORBENCH FAIL',name); end; end; +begin + passed := 0; failed := 0; + Assert(parseNumber('4294967295',$FFFFFFFF,value) and (value=$FFFFFFFF),'parse uint32 max'); + Assert(not parseNumber('4294967296',$FFFFFFFF,value),'reject numeric overflow'); + Assert(not parseNumber('-1',16384,value),'reject negative'); + Assert(not parseNumber('',16384,value),'reject empty'); + Assert(not parseNumber('1x',16384,value),'reject trailing junk'); + Assert((commandMode(nil,0,value)=bmAuto) and (value=0),'no args select suite'); + Assert((commandMode('2',2,value)=bmAuto) and (value=0),'disk and size select suite'); + Assert((commandMode('auto',3,value)=bmAuto) and (value=1),'explicit auto arguments'); + Assert((commandMode('run',7,value)=bmManual) and (value=1),'explicit manual arguments'); + Assert((commandMode('2',4,value)=bmManual) and (value=0),'legacy detailed arguments'); + Assert(commandMode('list',1,value)=bmList,'disk list mode'); + driver.storage.bench.clock.UnitTest; + Assert(ticksToUS(1234567)=1234567,'hardware microseconds'); + Assert(perSecond(1,1000000)=1,'hardware rate conversion'); + Assert(perSecond(uint64(4096) shl 20,2000000)=(uint64(4096) shl 20) div 2,'rate beyond 4 GiB'); + Assert(perSecond(100,0)=0,'zero ticks'); + Assert((latencyBucket(0)=0) and (latencyBucket(1)=1) and (latencyBucket(3)=2) and (latencyBucket($FFFFFFFF)=31),'latency buckets'); + summary := PCaseResult(kalloc(sizeof(TCaseResult))); + if summary<>nil then begin + Assert(suiteProfile(0,true,32,summary) and (summary^.KiB=1024) and (summary^.Depth=1) and not summary^.RandomRead,'auto sequential baseline'); + Assert(suiteProfile(1,true,32,summary) and (summary^.Depth=16),'auto caps queued depth'); + Assert(suiteProfile(1,true,1,summary) and (summary^.Depth=4),'auto tests software queue on serial devices'); + Assert(suiteProfile(3,true,8,summary) and (summary^.KiB=4) and (summary^.Depth=1) and summary^.RandomRead,'auto random baseline'); + Assert(suiteProfile(4,true,8,summary) and (summary^.KiB=4) and (summary^.Depth=8) and summary^.RandomRead,'auto random queued'); + Assert(suiteProfile(5,true,8,summary) and (summary^.KiB=64) and summary^.RandomRead,'auto medium random'); + Assert(not suiteProfile(6,true,8,summary),'auto has exactly six cases'); + Assert(suiteProfile(11,false,1,summary) and (summary^.KiB=1024) and (summary^.Depth=32),'compare retains full matrix'); + Assert(not suiteProfile(12,false,1,summary),'compare has exactly twelve cases'); + kfree(void(summary)); + end else Assert(false,'suite test allocation'); + run := PBench(kalloc(sizeof(TBench))); device := PStorage_Device(kalloc(sizeof(TStorage_Device))); + if (run=nil) or (device=nil) then begin + if run<>nil then kfree(void(run)); if device<>nil then kfree(void(device)); + io.syslog.logln('STORBENCH FAIL','test allocation'); exit; + end; + memset(uint32(run),0,sizeof(TBench)); memset(uint32(device),0,sizeof(TStorage_Device)); + device^.dispatchRead := @testDispatch; device^.sectorSize := 512; device^.maxSectorCount := $FFFFFFFF; + Assert(configure(run,device,4096,1024,32,0,true,false) and (run^.Span=8388608),'4 GiB transfer does not wrap'); + Assert(not configure(run,device,1,1024,33,0,true,false),'depth memory cap'); + Assert(not configure(run,device,1,0,1,0,true,false),'zero request rejected'); + Assert(not configure(run,device,1,4,1,$FFFFFFFF,true,false),'start beyond device rejected'); + Assert(configure(run,device,1,4,1,$FFFFFFFE,true,false) and (run^.Span=1),'range clamp near uint32 end'); + slot := @run^.Slots[0]; planRead(run,slot); + Assert((slot^.LBA=$FFFFFFFE) and (slot^.Sectors=1) and (run^.Remaining=0),'short final request'); + device^.maxSectorCount := 4096; + Assert(configure(run,device,1,4,4,0,false,true) and (run^.StartLBA=2048),'default starting LBA'); + for i := 1 to 256 do begin + planRead(run,slot); + Assert((slot^.LBA>=2048) and (slot^.LBA<=4096-slot^.Sectors),'random request stays in range'); + end; + Assert(run^.Remaining=0,'random workload has exact sector count'); + device^.sectorSize := 2048; + Assert(not configure(run,device,1,1,1,0,true,false),'request sector alignment'); + run^.Completed := 20; run^.Histogram[0] := 18; run^.Histogram[2] := 1; run^.Histogram[10] := 1; + Assert(p95Ticks(run)=3,'95th percentile excludes slowest five percent'); + { Synthetic driver only: callbacks arrive inline in reverse groups of four. } + mock := PBenchMock(kalloc(sizeof(TBenchMock))); buffer := kalloc(4*65536); + device^.requestQueue := CFIFO_New(sizeof(pointer),8); + if (mock<>nil) and (buffer<>nil) and (device^.requestQueue<>nil) then begin + memset(uint32(mock),0,sizeof(TBenchMock)); mock^.Batch := 4; mock^.FailLBA := $FFFFFFFF; + device^.controllerId0 := uint32(mock); device^.sectorSize := 512; device^.maxActive := 4; + memset(uint32(run),0,sizeof(TBench)); run^.Buffer := buffer; + Assert(configure(run,device,1,64,4,0,true,false),'configure queued mock'); + execute(run); + Assert((run^.Accepted=16) and (run^.Completed=16) and (run^.Bytes=1048576),'queued reads complete exact byte count'); + Assert((run^.Peak=4) and (mock^.Count=0) and (run^.Active=0),'reverse groups drain at configured depth'); + Assert(not mock^.InvalidRange,'benchmark issues only in-range reads'); + memset(uint32(run),0,sizeof(TBench)); run^.Buffer := buffer; mock^.FailLBA := 128; + Assert(configure(run,device,1,64,4,0,true,false),'configure failing mock'); + execute(run); + Assert((run^.Error=eIOError) and (run^.Accepted=4) and (run^.Completed=4),'failure stops new submissions and drains accepted requests'); + Assert((run^.Bytes=3*65536) and (run^.Active=0),'failed reads excluded from throughput'); + memset(uint32(run),0,sizeof(TBench)); run^.Buffer := buffer; mock^.FailLBA := $FFFFFFFF; mock^.Batch := 1; + Assert(configure(run,device,1,64,1,0,true,false),'configure immediate mock'); + execute(run); + Assert((run^.Peak=1) and (run^.Bytes=1048576),'inline completions preserve peak and byte accounting'); + memset(uint32(run),0,sizeof(TBench)); run^.Buffer := buffer; + Assert(configure(run,device,1,64,4,0,true,false),'configure removed device mock'); + device^.removed := true; execute(run); + Assert((run^.Error=eDeviceRemoved) and (run^.Accepted=0) and (run^.Active=0),'submission rejection has no completion ownership'); + end else Assert(false,'mock allocation'); + if mock<>nil then kfree(void(mock)); if buffer<>nil then kfree(buffer); + if device^.requestQueue<>nil then CFIFO_Free(device^.requestQueue); + kfree(void(device)); kfree(void(run)); + io.syslog.log('STORBENCH','Passed: '); io.syslog.writeint(passed); + io.syslog.writestring(' Failed: '); io.syslog.writeintln(failed); +end; +procedure init; +begin registerCommand('STORBENCH',@command,'Read-only disk benchmark. STORBENCH help for sizes, queue depths and comparison mode.'); end; +initialization + boot.mgr.registerBoot('driver.storage.bench',@init,'Storage Benchmark',BOOT_MGR_BARRIER_LATE); +end. diff --git a/src/driver/storage/driver.storage.test.pas b/src/driver/storage/driver.storage.test.pas index 8400c885..b99a8a7f 100644 --- a/src/driver/storage/driver.storage.test.pas +++ b/src/driver/storage/driver.storage.test.pas @@ -50,27 +50,6 @@ uses driver.storage.vfs, driver.storage.vol.mgr; -const - STORBENCH_DEFAULT_MB = 32; - STORBENCH_BUFFER_BYTES = 1024 * 1024; - STORBENCH_TICK_HZ = 1024; - -function isNumericStr(s : pchar) : boolean; -var - i : uint32; -begin - isNumericStr := false; - if (s = nil) or (s[0] = char(0)) then - exit; - i := 0; - while s[i] <> char(0) do begin - if (s[i] < '0') or (s[i] > '9') then - exit; - i := i + 1; - end; - isNumericStr := true; -end; - { ============================================================ run_tests — shared test body called by both UnitTest (boot) and the STORTEST shell command (runtime). @@ -496,156 +475,6 @@ begin kfree(void(fStr)); end; -procedure cmd_storbench(params : PParamList; stdin_buf, stdout_buf, stderr_buf : POutBuf); -var - disk : PStorage_Device; - devIdx : uint32; - mbToRead : uint32; - totalBytes : uint32; - buf : puint32; - chunkSectors: uint32; - sectorsLeft : uint32; - sectorsNow : uint32; - startLBA : uint32; - lba : uint32; - t0, t1 : uint32; - tLast : uint32; - elapsed : uint32; - bytesInSec : uint32; - doneBytes : uint32; - kbps : uint32; - err : TError; - - procedure writeThroughput(kbpsValue : uint32); - begin - if kbpsValue >= 1024 then begin - io.stdio.bufWriteInt(stdout_buf, kbpsValue div 1024); - io.stdio.bufWriteStr(stdout_buf, '.'); - io.stdio.bufWriteInt(stdout_buf, ((kbpsValue mod 1024) * 10) div 1024); - io.stdio.bufWriteStr(stdout_buf, ' MB/s'); - end else begin - io.stdio.bufWriteInt(stdout_buf, kbpsValue); - io.stdio.bufWriteStr(stdout_buf, ' KB/s'); - end; - end; -begin - devIdx := 0; - mbToRead := STORBENCH_DEFAULT_MB; - - if paramCount(params) >= 1 then begin - if not isNumericStr(getParam(0, params)) then begin - io.stdio.bufWriteStrLn(stderr_buf, 'Usage: STORBENCH [device_index] [mb]'); - exit; - end; - devIdx := stringToInt(getParam(0, params)); - end; - - if paramCount(params) >= 2 then begin - if not isNumericStr(getParam(1, params)) then begin - io.stdio.bufWriteStrLn(stderr_buf, 'Usage: STORBENCH [device_index] [mb]'); - exit; - end; - mbToRead := stringToInt(getParam(1, params)); - if mbToRead = 0 then - mbToRead := STORBENCH_DEFAULT_MB; - end; - - disk := driver.storage.mgr.get_device(devIdx); - if disk = nil then begin - io.stdio.bufWriteStr(stderr_buf, 'No storage device at index '); - io.stdio.bufWriteIntLn(stderr_buf, devIdx); - exit; - end; - - if disk^.sectorSize = 0 then begin - io.stdio.bufWriteStrLn(stderr_buf, 'Device has invalid sector size.'); - exit; - end; - - totalBytes := mbToRead * 1024 * 1024; - chunkSectors := STORBENCH_BUFFER_BYTES div disk^.sectorSize; - if chunkSectors = 0 then - chunkSectors := 1; - - buf := puint32(kalloc(chunkSectors * disk^.sectorSize)); - if buf = nil then begin - io.stdio.bufWriteStrLn(stderr_buf, 'Out of memory allocating benchmark buffer.'); - exit; - end; - - sectorsLeft := totalBytes div disk^.sectorSize; - if sectorsLeft = 0 then - sectorsLeft := 1; - if sectorsLeft > disk^.maxSectorCount then - sectorsLeft := disk^.maxSectorCount; - - if disk^.maxSectorCount > (sectorsLeft + 2048) then - startLBA := 2048 - else - startLBA := 0; - lba := startLBA; - - io.stdio.bufWriteStr(stdout_buf, 'Raw read benchmark: disk '); - io.stdio.bufWriteInt(stdout_buf, devIdx); - io.stdio.bufWriteStr(stdout_buf, ', sectorSize='); - io.stdio.bufWriteInt(stdout_buf, disk^.sectorSize); - io.stdio.bufWriteStr(stdout_buf, ', maxActive='); - io.stdio.bufWriteInt(stdout_buf, disk^.maxActive); - io.stdio.bufWriteNewLine(stdout_buf); - - t0 := arch.x86.bda.Counters.c32; - tLast := t0; - bytesInSec := 0; - doneBytes := 0; - - while sectorsLeft > 0 do begin - sectorsNow := chunkSectors; - if sectorsNow > sectorsLeft then - sectorsNow := sectorsLeft; - - err := driver.storage.mgr.storage_read(disk, lba, sectorsNow, buf); - if err <> eNone then begin - io.stdio.bufWriteStr(stderr_buf, 'Raw read failed at LBA '); - io.stdio.bufWriteInt(stderr_buf, lba); - io.stdio.bufWriteStr(stderr_buf, ' error='); - io.stdio.bufWriteIntLn(stderr_buf, ord(err)); - kfree(void(buf)); - exit; - end; - - lba := lba + sectorsNow; - sectorsLeft := sectorsLeft - sectorsNow; - doneBytes := doneBytes + (sectorsNow * disk^.sectorSize); - bytesInSec := bytesInSec + (sectorsNow * disk^.sectorSize); - - t1 := arch.x86.bda.Counters.c32; - if (t1 - tLast) >= STORBENCH_TICK_HZ then begin - kbps := (bytesInSec div 1024) * STORBENCH_TICK_HZ div (t1 - tLast); - io.stdio.bufWriteStr(stdout_buf, ' RAW '); - io.stdio.bufWriteInt(stdout_buf, doneBytes div 1024); - io.stdio.bufWriteStr(stdout_buf, ' KB '); - writeThroughput(kbps); - io.stdio.bufWriteNewLine(stdout_buf); - tLast := t1; - bytesInSec := 0; - end; - end; - - t1 := arch.x86.bda.Counters.c32; - elapsed := t1 - t0; - io.stdio.bufWriteStr(stdout_buf, 'Raw read complete: '); - if elapsed > 0 then begin - kbps := ((doneBytes div 1024) * STORBENCH_TICK_HZ) div elapsed; - writeThroughput(kbps); - io.stdio.bufWriteStr(stdout_buf, ' ('); - io.stdio.bufWriteInt(stdout_buf, (elapsed * 1000) div STORBENCH_TICK_HZ); - io.stdio.bufWriteStrLn(stdout_buf, ' ms)'); - end else - io.stdio.bufWriteStrLn(stdout_buf, '< 1 tick'); - - kfree(void(buf)); -end; - { ============================================================ init — register the storage terminal commands ============================================================ } @@ -656,8 +485,6 @@ begin 'Run storage subsystem unit tests.'); io.stdio.registerCommand('DISKTEST', @cmd_disktest, 'DESTRUCTIVE: wipe disk 0, format FAT32 4MB, write+read file.'); - io.stdio.registerCommand('STORBENCH', @cmd_storbench, - 'Read-only raw storage benchmark: STORBENCH [device_index] [mb].'); debug.tracer.pop_trace; end; diff --git a/src/io/io.stdio.pas b/src/io/io.stdio.pas index fb287cd5..e90cd5b5 100644 --- a/src/io/io.stdio.pas +++ b/src/io/io.stdio.pas @@ -486,6 +486,10 @@ var begin search := params; for i := 0 to index do begin + if search = nil then begin + getParam := nil; + exit; + end; if search^.next = nil then begin getParam := nil; exit; diff --git a/src/proc/proc.mgr.pas b/src/proc/proc.mgr.pas index 83dfb486..68d78563 100644 --- a/src/proc/proc.mgr.pas +++ b/src/proc/proc.mgr.pas @@ -107,10 +107,10 @@ procedure process_trampoline; forward; { ---- Process creation ---- } -function create(name : pchar; +function createInternal(name : pchar; entry : TProcessEntryPoint; localData : void; - priority : uint8) : PProcessContext; + priority : uint8; runnable : boolean) : PProcessContext; var ctx : PProcessContext; slotPtr : ^uint32; @@ -119,7 +119,7 @@ var prio : uint8; begin push_trace('proc.mgr.create'); - create := nil; + createInternal := nil; if priority = 0 then prio := 1 else prio := priority; @@ -140,7 +140,7 @@ begin ctx^.ParentID := CurrentProcess^.ProcessID; { State } - ctx^.State := psCreated; + ctx^.State := psSuspended; ctx^.ExitCode := 0; { Entry point } @@ -269,14 +269,22 @@ begin slotPtr^ := uint32(ctx); { Mark as ready for scheduling } - ctx^.State := psReady; + if runnable then ctx^.State := psReady; asm popf end; io.syslog.log('PROCMGR', 'Created process: '); io.syslog.writestringln(name); pop_trace; - create := ctx; + createInternal := ctx; +end; + +{ Public creation retains its ready-on-return behavior. Commands need their + arguments and streams installed before the scheduler may enter them. } +function create(name : pchar; entry : TProcessEntryPoint; + localData : void; priority : uint8) : PProcessContext; +begin + create := createInternal(name, entry, localData, priority, true); end; { ---- Process trampoline ---- } @@ -543,12 +551,17 @@ function runCommand(name : pchar; method : TCommandMethod; params : PParamList; var ctx : PProcessContext; begin - ctx := create(name, @legacy_entry, void(method), 1); + ctx := createInternal(name, @legacy_entry, void(method), 1, false); if ctx <> nil then begin ctx^.MsgData := void(params); ctx^.StdIn := stdin_buf; ctx^.StdOut := stdout_buf; ctx^.StdErr := stderr_buf; + { Publish only after all fields read by legacy_entry are initialized. + psCreated is also runnable, so creation must use psSuspended. } + asm pushf; cli end; + ctx^.State := psReady; + asm popf end; end; runCommand := ctx; end; diff --git a/src/test/test.pas b/src/test/test.pas index b9ddd739..ea33ad07 100644 --- a/src/test/test.pas +++ b/src/test/test.pas @@ -33,6 +33,7 @@ uses core.ds.prio, driver.storage.vfs, driver.storage.test, + driver.storage.bench, driver.storage.metatest, driver.storage.perftest, driver.storage.ctl.ahci, @@ -71,6 +72,7 @@ begin core.ds.prio.UnitTest; driver.storage.vfs.UnitTest; driver.storage.test.UnitTest; + driver.storage.bench.UnitTest; driver.storage.metatest.UnitTest; driver.storage.perftest.UnitTest; driver.storage.ctl.ahci.UnitTest; -- 2.54.0 From c0c56a9c84f4adb7ce96249ebc0c0aa3dc347da7 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 31 Oct 2026 19:49:36 +0000 Subject: [PATCH 14/21] Add initial ext2 read/write and formatting support Register ext2 at boot, integrate mode-aware VFS opens and volume cleanup, and queue asynchronous transfers through a persistent worker. Validation: Pascal compiler check and git diff --check passed. Full build blocked by Docker engine HTTP 500; runtime and Linux interoperability remain unverified. --- src/boot/boot.mgr.pas | 3 +- src/driver/storage/driver.storage.types.pas | 3 + src/driver/storage/driver.storage.vfs.pas | 19 +- .../fs/ext2/driver.storage.fs.ext2.core.pas | 904 ++++++++++++++++++ .../fs/ext2/driver.storage.fs.ext2.format.pas | 203 ++++ .../fs/ext2/driver.storage.fs.ext2.pas | 209 ++++ .../fs/ext2/driver.storage.fs.ext2.types.pas | 62 ++ 7 files changed, 1398 insertions(+), 5 deletions(-) create mode 100644 src/driver/storage/fs/ext2/driver.storage.fs.ext2.core.pas create mode 100644 src/driver/storage/fs/ext2/driver.storage.fs.ext2.format.pas create mode 100644 src/driver/storage/fs/ext2/driver.storage.fs.ext2.pas create mode 100644 src/driver/storage/fs/ext2/driver.storage.fs.ext2.types.pas diff --git a/src/boot/boot.mgr.pas b/src/boot/boot.mgr.pas index 64ab9179..72a7a49b 100644 --- a/src/boot/boot.mgr.pas +++ b/src/boot/boot.mgr.pas @@ -96,6 +96,7 @@ uses driver.net.dev.e1000, driver.bus.usb.ehci, driver.storage.fs.fat32, + driver.storage.fs.ext2, arch.x86.fault, core.ds.fifo, driver.storage.fs.mgr, @@ -562,4 +563,4 @@ initialization EntryCount would silently discard their registrations. } registerBoot('boot.mgr.debug', @printBootEntriesTree, 'Debug: Print Boot Entries Tree', 'io.syslog'); -end. \ No newline at end of file +end. diff --git a/src/driver/storage/driver.storage.types.pas b/src/driver/storage/driver.storage.types.pas index 936ea5d3..134c9345 100644 --- a/src/driver/storage/driver.storage.types.pas +++ b/src/driver/storage/driver.storage.types.pas @@ -222,6 +222,8 @@ type to read/write hooks. Also returns the current file size through the var parameter. Returning nil is legal (disables per-call fast-path). } PPOpenFileHook = function(volume : PStorage_Volume; directory : pchar; fileName : pchar; var fileSize : uint32) : pointer; + { Mode-aware open avoids creating a missing file during a read-only open. } + PPOpenFileModeHook = function(volume : PStorage_Volume; directory : pchar; fileName : pchar; allowCreate : boolean; var fileSize : uint32) : pointer; { Per-file close hook: frees the context allocated by openFileCallback. } PPCloseFileHook = procedure(ctx : pointer); @@ -324,6 +326,7 @@ type renameFileCallback : PPRenameFileHook; { Per-file open/close — nil if FS does not cache per-file metadata } openFileCallback : PPOpenFileHook; + openFileModeCallback : PPOpenFileModeHook; closeFileCallback : PPCloseFileHook; formatParamFlags : uint32; validateLabelCallback : PPValidateLabelHook; diff --git a/src/driver/storage/driver.storage.vfs.pas b/src/driver/storage/driver.storage.vfs.pas index c8ae5e45..0c1a5d33 100644 --- a/src/driver/storage/driver.storage.vfs.pas +++ b/src/driver/storage/driver.storage.vfs.pas @@ -152,6 +152,7 @@ implementation uses driver.storage.fs.mgr, driver.storage.fs.fat32, + driver.storage.fs.ext2, proc.mgr, proc.types, io.stdio, @@ -618,6 +619,8 @@ begin if (vol^.filesystem <> nil) and (stringCompare(vol^.filesystem^.sName, 'FAT32') = 0) then FAT32ReleaseVolumeState(vol); + if (vol^.filesystem <> nil) and (stringCompare(vol^.filesystem^.sName, 'EXT2') = 0) then + driver.storage.fs.ext2.ReleaseVolume(vol); InvalidateVolume := true; debug.tracer.push_trace('driver.storage.vfs.InvalidateVolume.exit'); @@ -1427,8 +1430,12 @@ begin fd^.FSCloseHook := nil; { Cache per-file metadata and populate DataSize via FS open hook } - if (vol^.filesystem <> nil) and (vol^.filesystem^.openFileCallback <> nil) then begin - fd^.FSPrivate := vol^.filesystem^.openFileCallback(vol, dir, fname, fd^.DataSize); + if (vol^.filesystem <> nil) and ((vol^.filesystem^.openFileCallback <> nil) or + (vol^.filesystem^.openFileModeCallback <> nil)) then begin + if vol^.filesystem^.openFileModeCallback <> nil then + fd^.FSPrivate := vol^.filesystem^.openFileModeCallback(vol, dir, fname, + (OpenMode = omCreate) or (OpenMode = omWrite), fd^.DataSize) + else fd^.FSPrivate := vol^.filesystem^.openFileCallback(vol, dir, fname, fd^.DataSize); if fd^.FSPrivate = nil then begin if Error <> nil then begin if (OpenMode = omCreate) or (OpenMode = omWrite) then @@ -1724,8 +1731,12 @@ begin fd^.FSCloseHook := nil; OutHandle := slot; - if (vol^.filesystem <> nil) and (vol^.filesystem^.openFileCallback <> nil) then begin - fd^.FSPrivate := vol^.filesystem^.openFileCallback(vol, dir, fname, fd^.DataSize); + if (vol^.filesystem <> nil) and ((vol^.filesystem^.openFileCallback <> nil) or + (vol^.filesystem^.openFileModeCallback <> nil)) then begin + if vol^.filesystem^.openFileModeCallback <> nil then + fd^.FSPrivate := vol^.filesystem^.openFileModeCallback(vol, dir, fname, + (OpenMode = omCreate) or (OpenMode = omWrite), fd^.DataSize) + else fd^.FSPrivate := vol^.filesystem^.openFileCallback(vol, dir, fname, fd^.DataSize); if fd^.FSPrivate = nil then begin if Error <> nil then begin if (OpenMode = omCreate) or (OpenMode = omWrite) then diff --git a/src/driver/storage/fs/ext2/driver.storage.fs.ext2.core.pas b/src/driver/storage/fs/ext2/driver.storage.fs.ext2.core.pas new file mode 100644 index 00000000..f3d88d89 --- /dev/null +++ b/src/driver/storage/fs/ext2/driver.storage.fs.ext2.core.pas @@ -0,0 +1,904 @@ +unit driver.storage.fs.ext2.core; + +interface +uses driver.storage.types, driver.storage.fs.ext2.types, core.ds.lists; + +function Get16(p : pointer; offset : uint32) : uint16; +function Get32(p : pointer; offset : uint32) : uint32; +procedure Put16(p : pointer; offset : uint32; value : uint16); +procedure Put32(p : pointer; offset, value : uint32); +function HasSuper(group, features : uint32) : boolean; +function Identify(volume : PStorage_Volume) : boolean; +procedure ReleaseVolume(volume : PStorage_Volume); +function BeginOp(volume : PStorage_Volume) : PExt2Op; +procedure EndOp(op : PExt2Op); +function BeginMutation(op : PExt2Op) : boolean; +function FinishMutation(op : PExt2Op) : boolean; +function ReadBlock(op : PExt2Op; block : uint32; dest : pointer) : boolean; +function WriteBlock(op : PExt2Op; block : uint32; src : pointer) : boolean; +function ReadInode(op : PExt2Op; number : uint32; var inode : TExt2Inode) : boolean; +function WriteInode(op : PExt2Op; number : uint32; var inode : TExt2Inode) : boolean; +function MapBlock(op : PExt2Op; number : uint32; var inode : TExt2Inode; + logical : uint32; createBlocks : boolean; var block : uint32) : boolean; +function Resolve(op : PExt2Op; path : pchar; var number : uint32; var inode : TExt2Inode) : boolean; +function Lookup(op : PExt2Op; var dir : TExt2Inode; name : pchar; length : uint32; + var number, block, offset : uint32) : boolean; +function CreateNode(op : PExt2Op; parent : uint32; name : pchar; isDir : boolean; + var number : uint32) : boolean; +function RemoveNode(op : PExt2Op; path : pchar; isDir : boolean) : boolean; +function RenameNode(op : PExt2Op; path, newName : pchar) : boolean; +function ReadDirectory(volume : PStorage_Volume; directory : pchar; status : puint32) : PLinkedListBase; +function Transfer(volume : PStorage_Volume; fileCtx : PExt2File; offset : uint32; + buffer : puint32; count : uint32; writing : boolean; var error : TError) : uint32; +function OpenFile(volume : PStorage_Volume; directory, name : pchar; + allowCreate : boolean; var size : uint32) : pointer; +procedure CloseFile(ctx : pointer); +procedure FreeEntries(entries : PLinkedListBase); + +implementation +uses memory.heap, core.util, core.strings, driver.storage.mgr; + +function Get16(p : pointer; offset : uint32) : uint16; +begin Get16 := puint16(uint32(p)+offset)^; end; +function Get32(p : pointer; offset : uint32) : uint32; +begin Get32 := puint32(uint32(p)+offset)^; end; +procedure Put16(p : pointer; offset : uint32; value : uint16); +begin puint16(uint32(p)+offset)^ := value; end; +procedure Put32(p : pointer; offset, value : uint32); +begin puint32(uint32(p)+offset)^ := value; end; + +function PowerOf(value, base : uint32) : boolean; +begin + while (value > 1) and (value mod base = 0) do value := value div base; + PowerOf := value = 1; +end; + +function HasSuper(group, features : uint32) : boolean; +begin + HasSuper := ((features and 1) = 0) or (group <= 1) or + PowerOf(group,3) or PowerOf(group,5) or PowerOf(group,7); +end; + +function GroupDesc(v : PExt2Volume; group : uint32) : PExt2Group; +begin GroupDesc := PExt2Group(uint32(v^.Groups)+group*32); end; + +function GroupBlocks(v : PExt2Volume; group : uint32) : uint32; +var first, count : uint32; +begin + first := v^.FirstBlock+group*v^.BlocksPerGroup; + count := v^.BlockCount-first; + if count > v^.BlocksPerGroup then count := v^.BlocksPerGroup; + GroupBlocks := count; +end; + +function DataBlock(v : PExt2Volume; block : uint32) : boolean; +var g, first, tables, reserve : uint32; d : PExt2Group; +begin + DataBlock := false; + if (block <= v^.FirstBlock) or (block >= v^.BlockCount) then exit; + g := (block-v^.FirstBlock) div v^.BlocksPerGroup; + d := GroupDesc(v,g); + if (block = d^.BlockBitmap) or (block = d^.InodeBitmap) then exit; + tables := v^.InodesPerGroup div (v^.BlockSize div v^.InodeSize); + if (block >= d^.InodeTable) and (block-d^.InodeTable < tables) then exit; + first := v^.FirstBlock+g*v^.BlocksPerGroup; + reserve := 1+v^.GDTBlocks+Get16(v^.Super,206); + if HasSuper(g,Get32(v^.Super,100)) and (block-first < reserve) then exit; + DataBlock := true; +end; + +function ReadBlock(op : PExt2Op; block : uint32; dest : pointer) : boolean; +begin + ReadBlock := false; + if op^.Error <> eNone then exit; + if block >= op^.V^.BlockCount then begin op^.Error := eCorruptFilesystem; exit; end; + op^.Error := storage_read(op^.V^.Volume^.device, + op^.V^.Volume^.sectorStart+block*op^.V^.SectorsPerBlock, + op^.V^.SectorsPerBlock,puint32(dest)); + ReadBlock := op^.Error = eNone; +end; + +function WriteBlock(op : PExt2Op; block : uint32; src : pointer) : boolean; +begin + WriteBlock := false; + if op^.Error <> eNone then exit; + if block >= op^.V^.BlockCount then begin op^.Error := eCorruptFilesystem; exit; end; + op^.Error := storage_write(op^.V^.Volume^.device, + op^.V^.Volume^.sectorStart+block*op^.V^.SectorsPerBlock, + op^.V^.SectorsPerBlock,puint32(src)); + WriteBlock := op^.Error = eNone; +end; + +function SaveSuper(op : PExt2Op) : boolean; +begin + op^.Error := storage_write(op^.V^.Volume^.device,op^.V^.Volume^.sectorStart+2, + 2,puint32(op^.V^.Super)); + SaveSuper := op^.Error = eNone; +end; + +function BeginOp(volume : PStorage_Volume) : PExt2Op; +var op : PExt2Op; v : PExt2Volume; +begin + BeginOp := nil; + if volume = nil then exit; + v := PExt2Volume(volume^.fsPrivate); + if v = nil then exit; + op := PExt2Op(kalloc(sizeof(TExt2Op))); + if op = nil then exit; + memset(uint32(op),0,sizeof(TExt2Op)); + op^.V := v; + op^.Scratch := puint8(kalloc(v^.BlockSize*2)); + if op^.Scratch = nil then begin kfree(void(op)); exit; end; + op^.Data := puint8(uint32(op^.Scratch)+v^.BlockSize); + while true do begin + asm pushf; cli end; + if not v^.Busy then begin + v^.Busy := true; + asm popf end; + break; + end; + asm popf end; + asm hlt end; + end; + BeginOp := op; +end; + +procedure EndOp(op : PExt2Op); +begin + if op = nil then exit; + { An interrupted metadata transaction remains dirty and cannot be written + again until repaired offline. Never publish a clean superblock on error. } + if op^.Mutating then op^.V^.Poisoned := true; + op^.V^.Busy := false; + kfree(void(op^.Scratch)); kfree(void(op)); +end; + +function BeginMutation(op : PExt2Op) : boolean; +begin + BeginMutation := false; + if op^.Error <> eNone then exit; + if not op^.V^.Writable or op^.V^.Poisoned then begin op^.Error := eReadOnly; exit; end; + if op^.Mutating then begin BeginMutation := true; exit; end; + op^.Mutating := true; + Put16(op^.V^.Super,58,0); + BeginMutation := SaveSuper(op); +end; + +function FinishMutation(op : PExt2Op) : boolean; +begin + FinishMutation := false; + if op^.Error <> eNone then exit; + if op^.Mutating then begin + Put16(op^.V^.Super,58,1); + if not SaveSuper(op) then begin Put16(op^.V^.Super,58,0); exit; end; + op^.Mutating := false; + op^.V^.Volume^.freeSectors := Get32(op^.V^.Super,12)*op^.V^.SectorsPerBlock; + end; + FinishMutation := true; +end; + +function InodeLocation(op : PExt2Op; number : uint32; var block, offset : uint32) : boolean; +var group, index : uint32; +begin + InodeLocation := false; + if (number = 0) or (number > op^.V^.InodeCount) then begin op^.Error := eCorruptFilesystem; exit; end; + group := (number-1) div op^.V^.InodesPerGroup; + index := (number-1) mod op^.V^.InodesPerGroup; + block := GroupDesc(op^.V,group)^.InodeTable+index div (op^.V^.BlockSize div op^.V^.InodeSize); + offset := (index mod (op^.V^.BlockSize div op^.V^.InodeSize))*op^.V^.InodeSize; + InodeLocation := true; +end; + +function ReadInode(op : PExt2Op; number : uint32; var inode : TExt2Inode) : boolean; +var block, offset : uint32; +begin + ReadInode := false; + if not InodeLocation(op,number,block,offset) then exit; + if not ReadBlock(op,block,op^.Scratch) then exit; + memcpy(uint32(op^.Scratch)+offset,uint32(@inode),128); + if (inode.Links = 0) or (inode.DTime <> 0) then begin op^.Error := eCorruptFilesystem; exit; end; + { Compression, extents and inline data are not ext2 block maps. } + if (inode.Flags and ($4 or $200 or $800 or $80000 or $10000000)) <> 0 then begin op^.Error := eNotSupported; exit; end; + if ((inode.Mode and $F000) = EXT2_FILE) and (inode.SizeHigh <> 0) then begin op^.Error := eNotSupported; exit; end; + ReadInode := true; +end; + +function WriteInode(op : PExt2Op; number : uint32; var inode : TExt2Inode) : boolean; +var block, offset : uint32; +begin + WriteInode := false; + if not InodeLocation(op,number,block,offset) then exit; + if not ReadBlock(op,block,op^.Scratch) then exit; + memcpy(uint32(@inode),uint32(op^.Scratch)+offset,128); + WriteInode := WriteBlock(op,block,op^.Scratch); +end; + +procedure FreeVolume(v : PExt2Volume); +begin + if v^.Groups <> nil then kfree(void(v^.Groups)); + if v^.Super <> nil then kfree(void(v^.Super)); + kfree(void(v)); +end; + +function LoadVolume(v : PExt2Volume) : boolean; +var s : puint8; g, first, count, tables, reserve, i : uint32; d : PExt2Group; + op : TExt2Op; inode : TExt2Inode; +begin + LoadVolume := false; s := v^.Super; + if (Get16(s,56) <> EXT2_MAGIC) or (Get32(s,24) > 2) or (Get32(s,76) > 1) then exit; + if Get32(s,28) <> Get32(s,24) then exit; { fragments unsupported } + { Accept classic ext2 only; never claim ext3/ext4 volumes. } + if (Get32(s,76) <> 0) and + (((Get32(s,92) and not uint32($38)) <> 0) or + ((Get32(s,96) and not uint32(2)) <> 0) or + ((Get32(s,100) and not uint32(3)) <> 0)) then exit; + v^.BlockSize := 1024 shl Get32(s,24); v^.SectorsPerBlock := v^.BlockSize div 512; + v^.BlockCount := Get32(s,4); v^.InodeCount := Get32(s,0); + v^.FirstBlock := Get32(s,20); v^.BlocksPerGroup := Get32(s,32); v^.InodesPerGroup := Get32(s,40); + v^.InodeSize := 128; + if Get32(s,76) <> 0 then v^.InodeSize := Get16(s,88); + if (v^.BlockCount <= v^.FirstBlock) or (v^.InodeCount < 11) or + (v^.BlockCount > v^.Volume^.sectorCount div v^.SectorsPerBlock) or + (v^.FirstBlock <> uint32(ord(v^.BlockSize=1024))) or + (v^.BlocksPerGroup = 0) or (v^.BlocksPerGroup > v^.BlockSize*8) or + (v^.InodesPerGroup = 0) or (v^.InodesPerGroup > v^.BlockSize*8) or + (v^.InodeSize < 128) or (v^.InodeSize > v^.BlockSize) then exit; + if (v^.InodeSize and (v^.InodeSize-1)) <> 0 then exit; + if v^.InodesPerGroup mod (v^.BlockSize div v^.InodeSize) <> 0 then exit; + v^.GroupCount := (v^.BlockCount-v^.FirstBlock-1) div v^.BlocksPerGroup+1; + if (v^.InodeCount-1) div v^.InodesPerGroup+1 <> v^.GroupCount then exit; + if (Get32(s,12) > v^.BlockCount) or (Get32(s,16) > v^.InodeCount) then exit; + v^.GDTBlocks := (v^.GroupCount-1) div (v^.BlockSize div 32)+1; + if v^.GDTBlocks >= v^.BlockCount-v^.FirstBlock then exit; + v^.Groups := puint8(kalloc(v^.GDTBlocks*v^.BlockSize)); + if v^.Groups = nil then exit; + memset(uint32(@op),0,sizeof(op)); op.V := v; + for i := 0 to v^.GDTBlocks-1 do + if not ReadBlock(@op,v^.FirstBlock+1+i,pointer(uint32(v^.Groups)+i*v^.BlockSize)) then exit; + tables := v^.InodesPerGroup div (v^.BlockSize div v^.InodeSize); + for g := 0 to v^.GroupCount-1 do begin + d := GroupDesc(v,g); first := v^.FirstBlock+g*v^.BlocksPerGroup; count := GroupBlocks(v,g); + reserve := 0; + if HasSuper(g,Get32(s,100)) then reserve := 1+v^.GDTBlocks+Get16(s,206); + if (reserve >= count) or (tables >= count-reserve) then exit; + if (d^.BlockBitmap < first+reserve) or (d^.BlockBitmap >= first+count) or + (d^.InodeBitmap < first+reserve) or (d^.InodeBitmap >= first+count) or + (d^.BlockBitmap = d^.InodeBitmap) or + (d^.InodeTable < first+reserve) or (d^.InodeTable > first+count-tables) or + (d^.FreeBlocks > count) or (d^.FreeInodes > v^.InodesPerGroup) then exit; + if ((d^.BlockBitmap >= d^.InodeTable) and (d^.BlockBitmap-d^.InodeTable < tables)) or + ((d^.InodeBitmap >= d^.InodeTable) and (d^.InodeBitmap-d^.InodeTable < tables)) then exit; + end; + op.Scratch := puint8(kalloc(v^.BlockSize)); + if op.Scratch = nil then exit; + LoadVolume := ReadInode(@op,2,inode); + if LoadVolume then LoadVolume := ((inode.Mode and $F000) = EXT2_DIR) and (inode.Size > 0); + kfree(void(op.Scratch)); + v^.FileTypes := (Get32(s,76) <> 0) and ((Get32(s,96) and 2) <> 0); + v^.Writable := v^.Volume^.device^.writable and (Get16(s,58)=1) and (Get32(s,232)=0); +end; + +function Identify(volume : PStorage_Volume) : boolean; +var v : PExt2Volume; i : uint32; +begin + Identify := false; + if volume = nil then exit; + if volume^.device = nil then exit; + if (volume^.device^.sectorSize <> 512) or (volume^.sectorCount < 4) or + (volume^.sectorStart >= volume^.device^.maxSectorCount) then exit; + if volume^.sectorCount > volume^.device^.maxSectorCount-volume^.sectorStart then exit; + if volume^.fsPrivate <> nil then exit; + v := PExt2Volume(kalloc(sizeof(TExt2Volume))); + if v = nil then exit; + memset(uint32(v),0,sizeof(TExt2Volume)); v^.Volume := volume; + v^.Super := puint8(kalloc(1024)); + if v^.Super = nil then begin FreeVolume(v); exit; end; + if storage_read(volume^.device,volume^.sectorStart+2,2,puint32(v^.Super)) <> eNone then begin FreeVolume(v); exit; end; + if not LoadVolume(v) then begin FreeVolume(v); exit; end; + volume^.fsPrivate := v; volume^.sectorSize := 512; + volume^.freeSectors := Get32(v^.Super,12)*v^.SectorsPerBlock; + memset(uint32(@volume^.labelText),0,sizeof(volume^.labelText)); + for i := 0 to 15 do volume^.labelText[i] := char(v^.Super[120+i]); + Identify := true; +end; + +procedure ReleaseVolume(volume : PStorage_Volume); +var v : PExt2Volume; +begin + if volume = nil then exit; + v := PExt2Volume(volume^.fsPrivate); + if v = nil then exit; + volume^.fsPrivate := nil; FreeVolume(v); +end; + +function SaveGroup(op : PExt2Op; group : uint32) : boolean; +var block : uint32; +begin + block := group div (op^.V^.BlockSize div 32); + SaveGroup := WriteBlock(op,op^.V^.FirstBlock+1+block, + pointer(uint32(op^.V^.Groups)+block*op^.V^.BlockSize)); +end; + +function Allocate(op : PExt2Op; inodeBit : boolean; var number : uint32) : boolean; +var v : PExt2Volume; d : PExt2Group; pass, g, limit, bit, b, mask, absolute : uint32; +begin + Allocate := false; number := 0; v := op^.V; + for pass := 0 to v^.GroupCount-1 do begin + g := (v^.AllocGroup+pass) mod v^.GroupCount; d := GroupDesc(v,g); + if inodeBit then begin + if d^.FreeInodes = 0 then continue; + b := d^.InodeBitmap; limit := v^.InodesPerGroup; + if limit > v^.InodeCount-g*v^.InodesPerGroup then limit := v^.InodeCount-g*v^.InodesPerGroup; + end else begin + if d^.FreeBlocks = 0 then continue; + b := d^.BlockBitmap; limit := GroupBlocks(v,g); + end; + if not ReadBlock(op,b,op^.Scratch) then exit; + bit := 0; + while bit < limit do begin + { Skip whole allocated bitmap words on the allocation hot path. } + if (bit mod 32 = 0) and (limit-bit >= 32) and (Get32(op^.Scratch,bit div 8)=$FFFFFFFF) then begin Inc(bit,32); continue; end; + mask := 1 shl (bit mod 8); + if (op^.Scratch[bit div 8] and mask) = 0 then begin + if inodeBit then absolute := g*v^.InodesPerGroup+bit+1 + else absolute := v^.FirstBlock+g*v^.BlocksPerGroup+bit; + if inodeBit then begin + if absolute < 11 then begin Inc(bit); continue; end; + end else if not DataBlock(v,absolute) then begin op^.Error := eCorruptFilesystem; exit; end; + op^.Scratch[bit div 8] := op^.Scratch[bit div 8] or mask; + if not WriteBlock(op,b,op^.Scratch) then exit; + if inodeBit then begin + if Get32(v^.Super,16)=0 then begin op^.Error:=eCorruptFilesystem; exit; end; + Dec(d^.FreeInodes); Put32(v^.Super,16,Get32(v^.Super,16)-1); + end else begin + if Get32(v^.Super,12)=0 then begin op^.Error:=eCorruptFilesystem; exit; end; + Dec(d^.FreeBlocks); Put32(v^.Super,12,Get32(v^.Super,12)-1); + end; + if not SaveGroup(op,g) then exit; + v^.AllocGroup := g; number := absolute; + if not inodeBit then begin + memset(uint32(op^.Scratch),0,v^.BlockSize); + if not WriteBlock(op,number,op^.Scratch) then exit; + end; + Allocate := true; exit; + end; + Inc(bit); + end; + end; + op^.Error := eDiskFull; +end; + +function FreeNumber(op : PExt2Op; number : uint32; inodeBit : boolean) : boolean; +var v : PExt2Volume; d : PExt2Group; g, bit, b, mask : uint32; +begin + FreeNumber := false; v := op^.V; + if inodeBit then begin + if (number < 11) or (number > v^.InodeCount) then begin op^.Error := eCorruptFilesystem; exit; end; + g := (number-1) div v^.InodesPerGroup; bit := (number-1) mod v^.InodesPerGroup; + end else begin + if not DataBlock(v,number) then begin op^.Error := eCorruptFilesystem; exit; end; + g := (number-v^.FirstBlock) div v^.BlocksPerGroup; bit := (number-v^.FirstBlock) mod v^.BlocksPerGroup; + end; + d := GroupDesc(v,g); + if inodeBit then b := d^.InodeBitmap else b := d^.BlockBitmap; + if not ReadBlock(op,b,op^.Scratch) then exit; + mask := 1 shl (bit mod 8); + if (op^.Scratch[bit div 8] and mask)=0 then begin op^.Error := eCorruptFilesystem; exit; end; + op^.Scratch[bit div 8] := op^.Scratch[bit div 8] and not mask; + if not WriteBlock(op,b,op^.Scratch) then exit; + if inodeBit then begin Inc(d^.FreeInodes); Put32(v^.Super,16,Get32(v^.Super,16)+1); end + else begin Inc(d^.FreeBlocks); Put32(v^.Super,12,Get32(v^.Super,12)+1); end; + FreeNumber := SaveGroup(op,g); +end; + +function MapBlock(op : PExt2Op; number : uint32; var inode : TExt2Inode; + logical : uint32; createBlocks : boolean; var block : uint32) : boolean; +var n, level, slot, span, index, parent, child, remaining : uint32; +begin + MapBlock := false; block := 0; n := op^.V^.BlockSize div 4; + level := 0; slot := logical; remaining := logical; + if logical >= 12 then begin + remaining := logical-12; slot := 12; level := 1; span := n; + while (remaining >= span) and (level < 3) do begin + Dec(remaining,span); Inc(level); Inc(slot); span := span*n; + end; + if remaining >= span then begin op^.Error := eNotSupported; exit; end; + end; + block := inode.Blocks[slot]; + if (block=0) and createBlocks then begin + if not Allocate(op,false,block) then exit; + inode.Blocks[slot] := block; Inc(inode.Sectors,op^.V^.SectorsPerBlock); + if not WriteInode(op,number,inode) then exit; + end; + while (level > 0) and (block <> 0) do begin + if not DataBlock(op^.V,block) then begin op^.Error:=eCorruptFilesystem; exit; end; + if not ReadBlock(op,block,op^.Scratch) then exit; + span := 1; + if level=2 then span:=n else if level=3 then span:=n*n; + index := remaining div span; remaining := remaining mod span; + parent := block; child := Get32(op^.Scratch,index*4); + if (child=0) and createBlocks then begin + if not Allocate(op,false,child) then exit; + if not ReadBlock(op,parent,op^.Scratch) then exit; + Put32(op^.Scratch,index*4,child); + if not WriteBlock(op,parent,op^.Scratch) then exit; + Inc(inode.Sectors,op^.V^.SectorsPerBlock); + if not WriteInode(op,number,inode) then exit; + end; + block := child; Dec(level); + end; + if (block <> 0) and not DataBlock(op^.V,block) then begin op^.Error:=eCorruptFilesystem; exit; end; + MapBlock := true; +end; + +function DirRecord(op : PExt2Op; offset, limit : uint32; var d : PExt2Dirent) : boolean; +var i, length : uint32; +begin + DirRecord := false; + if limit-offset < 8 then begin op^.Error:=eCorruptFilesystem; exit; end; + d := PExt2Dirent(uint32(op^.Data)+offset); + length := d^.NameLength; + if not op^.V^.FileTypes and (d^.FileType <> 0) then begin op^.Error:=eCorruptFilesystem; exit; end; + if (d^.Length < 8) or (d^.Length mod 4 <> 0) or (d^.Length > limit-offset) or + (length > d^.Length-8) or (d^.Inode > op^.V^.InodeCount) then begin op^.Error:=eCorruptFilesystem; exit; end; + if d^.Inode <> 0 then begin + if length=0 then begin op^.Error:=eCorruptFilesystem; exit; end; + for i:=0 to length-1 do + if (op^.Data[offset+8+i]=0) or (op^.Data[offset+8+i]=ord('/')) then begin op^.Error:=eCorruptFilesystem; exit; end; + end; + DirRecord := true; +end; + +function SameName(raw : pchar; name : pchar; length : uint32) : boolean; +var i : uint32; +begin + SameName := false; + if length=0 then exit; + for i:=0 to length-1 do if raw[i]<>name[i] then exit; + SameName := true; +end; + +function Lookup(op : PExt2Op; var dir : TExt2Inode; name : pchar; length : uint32; + var number, block, offset : uint32) : boolean; +var logical, count : uint32; d : PExt2Dirent; +begin + Lookup := false; number:=0; + if (dir.Mode and $F000)<>EXT2_DIR then begin op^.Error:=eNotADirectory; exit; end; + if (dir.Size=0) or (dir.Size mod op^.V^.BlockSize<>0) then begin op^.Error:=eCorruptFilesystem; exit; end; + count:=dir.Size div op^.V^.BlockSize; + if count>op^.V^.BlockCount then begin op^.Error:=eCorruptFilesystem; exit; end; + for logical:=0 to count-1 do begin + if not MapBlock(op,0,dir,logical,false,block) then exit; + if block=0 then begin op^.Error:=eCorruptFilesystem; exit; end; + if not ReadBlock(op,block,op^.Data) then exit; + offset:=0; + while offset0) and (d^.NameLength=length) then + if SameName(pchar(uint32(d)+8),name,length) then begin number:=d^.Inode; Lookup:=true; exit; end; + Inc(offset,d^.Length); + end; + end; + op^.Error:=eFileDoesNotExist; +end; + +function Resolve(op : PExt2Op; path : pchar; var number : uint32; var inode : TExt2Inode) : boolean; +var pos, start, block, offset, next : uint32; +begin + Resolve:=false; number:=2; + if not ReadInode(op,number,inode) then exit; + if path=nil then begin op^.Error:=eInvalidPath; exit; end; + pos:=0; + while path[pos]<>char(0) do begin + if pos>=4096 then begin op^.Error:=eInvalidPath; exit; end; + if path[pos]='/' then begin Inc(pos); continue; end; + start:=pos; + while (path[pos]<>char(0)) and (path[pos]<>'/') and (pos-start<=255) do Inc(pos); + if pos-start>255 then begin op^.Error:=eFilenameTooLong; exit; end; + if not Lookup(op,inode,pchar(uint32(path)+start),pos-start,next,block,offset) then exit; + number:=next; + if not ReadInode(op,number,inode) then exit; + end; + Resolve:=true; +end; + +function ValidName(op : PExt2Op; name : pchar; var length : uint32) : boolean; +begin + ValidName:=false; length:=0; + if name=nil then begin op^.Error:=eInvalidFileName; exit; end; + while (length<=255) and (name[length]<>char(0)) do begin + if name[length]='/' then begin op^.Error:=eInvalidFileName; exit; end; + Inc(length); + end; + if length>255 then begin op^.Error:=eFilenameTooLong; exit; end; + if (length=0) or ((length=1) and (name[0]='.')) or + ((length=2) and (name[0]='.') and (name[1]='.')) then begin op^.Error:=eInvalidFileName; exit; end; + ValidName:=true; +end; + +function CanModify(op : PExt2Op; var inode : TExt2Inode) : boolean; +begin + CanModify:=false; + if (inode.Flags and ($10 or $20))<>0 then begin op^.Error:=ePermissionDenied; exit; end; + CanModify:=true; +end; + +procedure SetDirent(op : PExt2Op; offset, length, number : uint32; name : pchar; nameLength : uint32; isDir : boolean); +var d : PExt2Dirent; +begin + d:=PExt2Dirent(uint32(op^.Data)+offset); + memset(uint32(d),0,length); + d^.Inode:=number; d^.Length:=length; d^.NameLength:=nameLength; + if op^.V^.FileTypes then begin if isDir then d^.FileType:=2 else d^.FileType:=1; end; + memcpy(uint32(name),uint32(d)+8,nameLength); +end; + +function AddEntry(op : PExt2Op; parent : uint32; var dir : TExt2Inode; name : pchar; number : uint32; isDir : boolean) : boolean; +var logical, count, block, offset, used, need, length, space : uint32; d : PExt2Dirent; +begin + AddEntry:=false; + if not ValidName(op,name,length) then exit; + need:=(8+length+3) and not uint32(3); count:=dir.Size div op^.V^.BlockSize; + { Indexed directories can be read linearly. Drop INDEX before modifying the + linear representation; old htree records remain unused directory space. } + if (dir.Flags and $1000)<>0 then begin dir.Flags:=dir.Flags and not uint32($1000); if not WriteInode(op,parent,dir) then exit; end; + logical:=0; + while logical=need then begin + if used<>0 then d^.Length:=used; + SetDirent(op,offset+used,space,number,name,length,isDir); + AddEntry:=WriteBlock(op,block,op^.Data); exit; + end; + Inc(offset,d^.Length); + end; + Inc(logical); + end; + if dir.Size>$FFFFFFFF-op^.V^.BlockSize then begin op^.Error:=eDiskFull; exit; end; + if not MapBlock(op,parent,dir,count,true,block) then exit; + SetDirent(op,0,op^.V^.BlockSize,number,name,length,isDir); + if not WriteBlock(op,block,op^.Data) then exit; + Inc(dir.Size,op^.V^.BlockSize); + AddEntry:=WriteInode(op,parent,dir); +end; + +function CreateNode(op : PExt2Op; parent : uint32; name : pchar; isDir : boolean; var number : uint32) : boolean; +var dir, inode : TExt2Inode; length, existing, block, offset, g : uint32; d : PExt2Group; +begin + CreateNode:=false; number:=0; + if not ValidName(op,name,length) then exit; + if not ReadInode(op,parent,dir) then exit; + if not CanModify(op,dir) then exit; + if Lookup(op,dir,name,length,existing,block,offset) then begin op^.Error:=eAlreadyExists; exit; end; + if op^.Error<>eFileDoesNotExist then exit; + op^.Error:=eNone; + if isDir and (dir.Links=$FFFF) then begin op^.Error:=eDirectoryFull; exit; end; + if not BeginMutation(op) then exit; + if not Allocate(op,true,number) then exit; + { Clear the full inode slot, including any extended inode fields. } + if not InodeLocation(op,number,block,offset) then exit; + if not ReadBlock(op,block,op^.Scratch) then exit; + memset(uint32(op^.Scratch)+offset,0,op^.V^.InodeSize); + if not WriteBlock(op,block,op^.Scratch) then exit; + memset(uint32(@inode),0,sizeof(inode)); + inode.Mode:=EXT2_FILE or $1A4; inode.Links:=1; + if isDir then begin inode.Mode:=EXT2_DIR or $1ED; inode.Links:=2; end; + if not WriteInode(op,number,inode) then exit; + if isDir then begin + if not MapBlock(op,number,inode,0,true,block) then exit; + SetDirent(op,0,12,number,'.',1,true); + SetDirent(op,12,op^.V^.BlockSize-12,parent,'..',2,true); + if not WriteBlock(op,block,op^.Data) then exit; + inode.Size:=op^.V^.BlockSize; + if not WriteInode(op,number,inode) then exit; + g:=(number-1) div op^.V^.InodesPerGroup; d:=GroupDesc(op^.V,g); Inc(d^.Directories); + if not SaveGroup(op,g) then exit; + Inc(dir.Links); + if not WriteInode(op,parent,dir) then exit; + end; + if not AddEntry(op,parent,dir,name,number,isDir) then exit; + CreateNode:=true; +end; + +function ParentPath(op : PExt2Op; path : pchar; var parent : uint32; var name : pchar; var inode : TExt2Inode) : boolean; +var length, split, i : uint32; copyPath : pchar; +begin + ParentPath:=false; name:=nil; + if path=nil then begin op^.Error:=eInvalidPath; exit; end; + length:=0; + while (length<4096) and (path[length]<>char(0)) do Inc(length); + if (length=0) or (length=4096) then begin op^.Error:=eInvalidPath; exit; end; + split:=0; + for i:=0 to length-1 do if path[i]='/' then split:=i+1; + name:=pchar(uint32(path)+split); + if not ValidName(op,name,i) then exit; + copyPath:=pchar(kalloc(split+1)); + if copyPath=nil then begin op^.Error:=eOutOfMemory; exit; end; + memcpy(uint32(path),uint32(copyPath),split); copyPath[split]:=char(0); + ParentPath:=Resolve(op,copyPath,parent,inode); + kfree(void(copyPath)); +end; + +function FreeTree(op : PExt2Op; block, level : uint32; var budget : uint32) : boolean; +var data : puint32; i, child : uint32; +begin + FreeTree:=false; + if block=0 then begin FreeTree:=true; exit; end; + if (budget=0) or not DataBlock(op^.V,block) then begin op^.Error:=eCorruptFilesystem; exit; end; + Dec(budget); + if level>0 then begin + data:=puint32(kalloc(op^.V^.BlockSize)); + if data=nil then begin op^.Error:=eOutOfMemory; exit; end; + if not ReadBlock(op,block,data) then begin kfree(data); exit; end; + for i:=0 to op^.V^.BlockSize div 4-1 do begin + child:=data[i]; + if not FreeTree(op,child,level-1,budget) then begin kfree(data); exit; end; + end; + kfree(data); + end; + FreeTree:=FreeNumber(op,block,false); +end; + +function EmptyDirectory(op : PExt2Op; var inode : TExt2Inode) : boolean; +var logical, block, offset : uint32; d : PExt2Dirent; name : pchar; +begin + EmptyDirectory:=false; + if (inode.Size=0) or (inode.Size mod op^.V^.BlockSize<>0) or + (inode.Size div op^.V^.BlockSize>op^.V^.BlockCount) then begin op^.Error:=eCorruptFilesystem; exit; end; + for logical:=0 to inode.Size div op^.V^.BlockSize-1 do begin + if not MapBlock(op,0,inode,logical,false,block) then exit; + if block=0 then begin op^.Error:=eCorruptFilesystem; exit; end; + if not ReadBlock(op,block,op^.Data) then exit; + offset:=0; + while offset0 then + if not (((d^.NameLength=1) and (name[0]='.')) or + ((d^.NameLength=2) and (name[0]='.') and (name[1]='.'))) then begin op^.Error:=eDirectoryNotEmpty; exit; end; + Inc(offset,d^.Length); + end; + end; + EmptyDirectory:=true; +end; + +function RemoveNode(op : PExt2Op; path : pchar; isDir : boolean) : boolean; +var parent, number, block, offset, i, budget, g : uint32; name : pchar; + dir, inode : TExt2Inode; f : PExt2File; d : PExt2Group; +begin + RemoveNode:=false; + if not ParentPath(op,path,parent,name,dir) then exit; + if not Lookup(op,dir,name,stringSize(name),number,block,offset) then exit; + if number<11 then begin op^.Error:=ePermissionDenied; exit; end; + if not ReadInode(op,number,inode) then exit; + if isDir then begin + if (inode.Mode and $F000)<>EXT2_DIR then begin op^.Error:=eNotADirectory; exit; end; + if not EmptyDirectory(op,inode) then exit; + end else if (inode.Mode and $F000)<>EXT2_FILE then begin op^.Error:=eNotSupported; exit; end; + if (inode.ACL<>0) then begin op^.Error:=eNotSupported; exit; end; + if not CanModify(op,dir) or not CanModify(op,inode) then exit; + f:=op^.V^.Files; + while f<>nil do begin + if f^.Inode=number then begin op^.Error:=eFileInUse; exit; end; + f:=f^.Next; + end; + if not BeginMutation(op) then exit; + dir.Flags:=dir.Flags and not uint32($1000); + if isDir then begin + if dir.Links<2 then begin op^.Error:=eCorruptFilesystem; exit; end; + Dec(dir.Links); + end; + if not WriteInode(op,parent,dir) then exit; + if not ReadBlock(op,block,op^.Data) then exit; + Put32(op^.Data,offset,0); + if not WriteBlock(op,block,op^.Data) then exit; + if isDir then inode.Links:=0 else Dec(inode.Links); + if inode.Links=0 then begin + budget:=op^.V^.BlockCount; + for i:=0 to 14 do begin + g:=0; if i>=12 then g:=i-11; + if not FreeTree(op,inode.Blocks[i],g,budget) then exit; + inode.Blocks[i]:=0; + end; + inode.Size:=0; inode.Sectors:=0; inode.DTime:=1; + if not WriteInode(op,number,inode) then exit; + if not FreeNumber(op,number,true) then exit; + if isDir then begin + g:=(number-1) div op^.V^.InodesPerGroup; d:=GroupDesc(op^.V,g); + if d^.Directories=0 then begin op^.Error:=eCorruptFilesystem; exit; end; + Dec(d^.Directories); if not SaveGroup(op,g) then exit; + end; + end else if not WriteInode(op,number,inode) then exit; + RemoveNode:=FinishMutation(op); +end; + +function RenameNode(op : PExt2Op; path, newName : pchar) : boolean; +var parent, number, oldBlock, oldOffset, other, block, offset, length : uint32; + name : pchar; dir, inode : TExt2Inode; +begin + RenameNode:=false; + if not ValidName(op,newName,length) then exit; + if not ParentPath(op,path,parent,name,dir) then exit; + if not Lookup(op,dir,name,stringSize(name),number,oldBlock,oldOffset) then exit; + if stringEquals(name,newName) then begin RenameNode:=true; exit; end; + if Lookup(op,dir,newName,length,other,block,offset) then begin op^.Error:=eAlreadyExists; exit; end; + if op^.Error<>eFileDoesNotExist then exit; + op^.Error:=eNone; + if not ReadInode(op,number,inode) then exit; + if not CanModify(op,dir) or not CanModify(op,inode) then exit; + if ((inode.Mode and $F000)<>EXT2_FILE) and ((inode.Mode and $F000)<>EXT2_DIR) then begin op^.Error:=eNotSupported; exit; end; + if not BeginMutation(op) then exit; + if not AddEntry(op,parent,dir,newName,number,(inode.Mode and $F000)=EXT2_DIR) then exit; + if not ReadBlock(op,oldBlock,op^.Data) then exit; + Put32(op^.Data,oldOffset,0); + if not WriteBlock(op,oldBlock,op^.Data) then exit; + RenameNode:=FinishMutation(op); +end; + +procedure FreeEntries(entries : PLinkedListBase); +var node : PLinkedList; e : PDirectory_Entry; +begin + if entries=nil then exit; + node:=entries^.Head; + while node<>nil do begin + e:=PDirectory_Entry(node^.Data); + if e^.fileName<>nil then kfree(void(e^.fileName)); + node:=node^.Next; + end; + LL_Free(entries); +end; + +function ReadDirectory(volume : PStorage_Volume; directory : pchar; status : puint32) : PLinkedListBase; +var op : PExt2Op; list : PLinkedListBase; tail, node : PLinkedList; + dir, inode : TExt2Inode; number, logical, block, offset, length : uint32; + d : PExt2Dirent; e : PDirectory_Entry; +begin + ReadDirectory:=nil; if status<>nil then status^:=ord(eOutOfMemory); + op:=BeginOp(volume); if op=nil then exit; + list:=nil; tail:=nil; + if Resolve(op,directory,number,dir) then begin + if (dir.Mode and $F000)<>EXT2_DIR then op^.Error:=eNotADirectory + else if (dir.Size=0) or (dir.Size mod op^.V^.BlockSize<>0) or + (dir.Size div op^.V^.BlockSize>op^.V^.BlockCount) then op^.Error:=eCorruptFilesystem + else begin + list:=PLinkedListBase(kalloc(sizeof(TLinkedListBase))); + if list=nil then op^.Error:=eOutOfMemory else begin + memset(uint32(list),0,sizeof(TLinkedListBase)); list^.ElementSize:=sizeof(TDirectory_Entry); + for logical:=0 to dir.Size div op^.V^.BlockSize-1 do begin + if not MapBlock(op,number,dir,logical,false,block) then break; + if block=0 then begin op^.Error:=eCorruptFilesystem; break; end; + if not ReadBlock(op,block,op^.Data) then break; + offset:=0; + while offset0 then begin + if not ReadInode(op,d^.Inode,inode) then break; + if ((inode.Mode and $F000)=EXT2_FILE) or ((inode.Mode and $F000)=EXT2_DIR) then begin + node:=PLinkedList(kalloc(sizeof(TLinkedList))); + e:=PDirectory_Entry(kalloc(sizeof(TDirectory_Entry))); + if (node=nil) or (e=nil) then begin + if node<>nil then kfree(void(node)); if e<>nil then kfree(void(e)); + op^.Error:=eOutOfMemory; break; + end; + memset(uint32(e),0,sizeof(TDirectory_Entry)); + e^.fileName:=pchar(kalloc(uint32(d^.NameLength)+1)); + if e^.fileName=nil then begin kfree(void(node)); kfree(void(e)); op^.Error:=eOutOfMemory; break; end; + memcpy(uint32(d)+8,uint32(e^.fileName),d^.NameLength); e^.fileName[d^.NameLength]:=char(0); + e^.fileSize:=inode.Size; + if (inode.Mode and $F000)=EXT2_DIR then e^.entryType:=directoryEntry else e^.entryType:=fileEntry; + node^.Data:=void(e); node^.Next:=nil; node^.Previous:=tail; + if tail=nil then list^.Head:=node else tail^.Next:=node; + tail:=node; Inc(list^.Count); + end; + end; + Inc(offset,length); + end; + if op^.Error<>eNone then break; + end; + end; + end; + end; + if status<>nil then status^:=ord(op^.Error); + if op^.Error=eNone then ReadDirectory:=list else FreeEntries(list); + EndOp(op); +end; + +function OpenFile(volume : PStorage_Volume; directory, name : pchar; + allowCreate : boolean; var size : uint32) : pointer; +var op : PExt2Op; inode : TExt2Inode; parent, number, block, offset, length : uint32; f : PExt2File; +begin + OpenFile:=nil; size:=0; op:=BeginOp(volume); if op=nil then exit; + if ValidName(op,name,length) and Resolve(op,directory,parent,inode) then begin + if not Lookup(op,inode,name,length,number,block,offset) then begin + if allowCreate and (op^.Error=eFileDoesNotExist) then begin + op^.Error:=eNone; + if CreateNode(op,parent,name,false,number) then FinishMutation(op); + end; + end; + if op^.Error=eNone then + if ReadInode(op,number,inode) then + if (inode.Mode and $F000)=EXT2_FILE then begin + f:=PExt2File(kalloc(sizeof(TExt2File))); + if f<>nil then begin + f^.Volume:=op^.V; f^.Inode:=number; f^.Next:=op^.V^.Files; op^.V^.Files:=f; + size:=inode.Size; OpenFile:=f; + end; + end; + end; + EndOp(op); +end; + +procedure CloseFile(ctx : pointer); +var f, cur, prev : PExt2File; op : PExt2Op; +begin + f:=PExt2File(ctx); if f=nil then exit; + op:=BeginOp(f^.Volume^.Volume); if op=nil then exit; + prev:=nil; cur:=op^.V^.Files; + while cur<>nil do begin + if cur=f then begin + if prev=nil then op^.V^.Files:=cur^.Next else prev^.Next:=cur^.Next; + kfree(void(cur)); break; + end; + prev:=cur; cur:=cur^.Next; + end; + EndOp(op); +end; + +function Transfer(volume : PStorage_Volume; fileCtx : PExt2File; offset : uint32; + buffer : puint32; count : uint32; writing : boolean; var error : TError) : uint32; +var op : PExt2Op; inode : TExt2Inode; done, block, within, amount, limit, oldSize : uint32; +begin + Transfer:=0; error:=eInvalidArgument; + if (fileCtx=nil) or ((buffer=nil) and (count<>0)) then exit; + if fileCtx^.Volume^.Volume<>volume then exit; + if count>$FFFFFFFF-offset then exit; + op:=BeginOp(volume); if op=nil then begin error:=eOutOfMemory; exit; end; + done:=0; + if ReadInode(op,fileCtx^.Inode,inode) then begin + limit:=count; oldSize:=inode.Size; + if writing then begin + if not CanModify(op,inode) then limit:=0; + if (limit>0) and not BeginMutation(op) then limit:=0; + { Existing tail bytes must not become visible when a write leaves + a sparse gap within the former last data block. } + if (limit>0) and (offset>oldSize) and (oldSize mod op^.V^.BlockSize<>0) then begin + if MapBlock(op,fileCtx^.Inode,inode,oldSize div op^.V^.BlockSize,false,block) then + if block<>0 then + if ReadBlock(op,block,op^.Data) then begin + within:=oldSize mod op^.V^.BlockSize; + memset(uint32(op^.Data)+within,0,op^.V^.BlockSize-within); + WriteBlock(op,block,op^.Data); + end; + end; + end else begin + if offset>=inode.Size then limit:=0 else if limit>inode.Size-offset then limit:=inode.Size-offset; + end; + while (donelimit-done then amount:=limit-done; + if not MapBlock(op,fileCtx^.Inode,inode,(offset+done) div op^.V^.BlockSize,writing,block) then break; + if block=0 then memset(uint32(op^.Data),0,op^.V^.BlockSize) + else if (not writing) or (within<>0) or (amount<>op^.V^.BlockSize) then + if not ReadBlock(op,block,op^.Data) then break; + if writing then begin + memcpy(uint32(buffer)+done,uint32(op^.Data)+within,amount); + if not WriteBlock(op,block,op^.Data) then break; + if offset+done+amount>inode.Size then inode.Size:=offset+done+amount; + if not WriteInode(op,fileCtx^.Inode,inode) then break; + end else memcpy(uint32(op^.Data)+within,uint32(buffer)+done,amount); + Inc(done,amount); + end; + if writing and op^.Mutating and (op^.Error=eNone) then FinishMutation(op); + end; + error:=op^.Error; Transfer:=done; EndOp(op); +end; + +end. diff --git a/src/driver/storage/fs/ext2/driver.storage.fs.ext2.format.pas b/src/driver/storage/fs/ext2/driver.storage.fs.ext2.format.pas new file mode 100644 index 00000000..6c975051 --- /dev/null +++ b/src/driver/storage/fs/ext2/driver.storage.fs.ext2.format.pas @@ -0,0 +1,203 @@ +unit driver.storage.fs.ext2.format; + +interface +uses driver.storage.types; +function ValidateFormat(volume : PStorage_Volume; sectors, start : uint32; config : PFSFormatParams) : TError; +function FormatVolume(volume : PStorage_Volume; sectors, start : uint32; config : PFSFormatParams) : TError; +function ValidateLabel(text : pchar; var normalized : TVolumeLabel) : TError; +function SetLabel(volume : PStorage_Volume; text : pchar) : TError; + +implementation +uses driver.storage.fs.ext2.types, driver.storage.fs.ext2.core, + driver.storage.mgr, memory.heap, core.util, core.rand; + +function ValidateLabel(text : pchar; var normalized : TVolumeLabel) : TError; +var i : uint32; +begin + memset(uint32(@normalized),0,sizeof(normalized)); + ValidateLabel:=eInvalidArgument; if text=nil then exit; + i:=0; + while (i<16) and (text[i]<>char(0)) do begin + if ord(text[i])<32 then exit; + normalized[i]:=text[i]; Inc(i); + end; + if text[i]<>char(0) then begin ValidateLabel:=eFilenameTooLong; exit; end; + ValidateLabel:=eNone; +end; + +function Geometry(volume : PStorage_Volume; sectors, start : uint32; config : PFSFormatParams; var v : TExt2Volume) : TError; +var g, count, minimum, perBlock, tables, reserve : uint32; labelText : TVolumeLabel; +begin + Geometry:=eInvalidArgument; + memset(uint32(@v),0,sizeof(v)); + if volume=nil then exit; + if volume^.device=nil then exit; + if not volume^.device^.writable then begin Geometry:=eReadOnly; exit; end; + if (volume^.device^.sectorSize<>512) or (sectors<>volume^.sectorCount) or + (start<>volume^.sectorStart) or (start>=volume^.device^.maxSectorCount) then exit; + if sectors>volume^.device^.maxSectorCount-start then exit; + v.Volume:=volume; v.SectorsPerBlock:=8; + if config<>nil then begin + if (config^.Version<>FS_FORMAT_PARAMS_VERSION_1) or + ((config^.Flags and not uint16(FS_FORMAT_PARAM_CLUSTER_SIZE or FS_FORMAT_PARAM_FILE_COUNT or FS_FORMAT_PARAM_LABEL))<>0) then exit; + if (config^.Flags and FS_FORMAT_PARAM_CLUSTER_SIZE)<>0 then v.SectorsPerBlock:=config^.ClusterSize; + if (config^.Flags and FS_FORMAT_PARAM_LABEL)<>0 then begin + { The shared format structure carries at most eleven label bytes. } + if config^.LabelText[11]<>char(0) then exit; + Geometry:=ValidateLabel(@config^.LabelText[0],labelText); + if Geometry<>eNone then exit; + Geometry:=eInvalidArgument; + end; + end; + if (v.SectorsPerBlock<>2) and (v.SectorsPerBlock<>4) and (v.SectorsPerBlock<>8) then exit; + v.BlockSize:=v.SectorsPerBlock*512; v.BlockCount:=sectors div v.SectorsPerBlock; + v.FirstBlock:=ord(v.BlockSize=1024); v.BlocksPerGroup:=v.BlockSize*8; v.InodeSize:=128; + if v.BlockCount<64 then exit; + v.GroupCount:=(v.BlockCount-v.FirstBlock-1) div v.BlocksPerGroup+1; + v.GDTBlocks:=(v.GroupCount-1) div (v.BlockSize div 32)+1; + minimum:=v.BlocksPerGroup; + for g:=0 to v.GroupCount-1 do begin + count:=v.BlockCount-v.FirstBlock-g*v.BlocksPerGroup; + if count>v.BlocksPerGroup then count:=v.BlocksPerGroup; + if countnil then if (config^.Flags and FS_FORMAT_PARAM_FILE_COUNT)<>0 then begin + if config^.FileCount<11 then exit; + v.InodesPerGroup:=(config^.FileCount-1) div v.GroupCount+1; + if v.InodesPerGroup>v.BlockSize*8 then exit; + v.InodesPerGroup:=((v.InodesPerGroup+perBlock-1) div perBlock)*perBlock; + end; + if (v.InodesPerGroup<16) or (v.InodesPerGroup>v.BlockSize*8) then exit; + v.InodeCount:=v.InodesPerGroup*v.GroupCount; + tables:=v.InodesPerGroup div perBlock; + for g:=0 to v.GroupCount-1 do begin + count:=v.BlockCount-v.FirstBlock-g*v.BlocksPerGroup; + if count>v.BlocksPerGroup then count:=v.BlocksPerGroup; + reserve:=2+tables; + if HasSuper(g,1) then Inc(reserve,1+v.GDTBlocks); + if g=0 then Inc(reserve); { root directory } + if reserve>=count then exit; + end; + Geometry:=eNone; +end; + +function ValidateFormat(volume : PStorage_Volume; sectors, start : uint32; config : PFSFormatParams) : TError; +var v : TExt2Volume; +begin ValidateFormat:=Geometry(volume,sectors,start,config,v); end; + +function FormatVolume(volume : PStorage_Volume; sectors, start : uint32; config : PFSFormatParams) : TError; +var v : PExt2Volume; op : PExt2Op; d : PExt2Group; inode : PExt2Inode; + err : TError; g, first, count, reserve, tables, used, i, bit, rootBlock, freeBlocks : uint32; +begin + FormatVolume:=eOutOfMemory; + v:=PExt2Volume(kalloc(sizeof(TExt2Volume))); if v=nil then exit; + err:=Geometry(volume,sectors,start,config,v^); + if err<>eNone then begin kfree(void(v)); FormatVolume:=err; exit; end; + op:=PExt2Op(kalloc(sizeof(TExt2Op))); + if op=nil then begin kfree(void(v)); exit; end; + memset(uint32(op),0,sizeof(TExt2Op)); op^.V:=v; + v^.Super:=puint8(kalloc(1024)); v^.Groups:=puint8(kalloc(v^.GDTBlocks*v^.BlockSize)); + op^.Scratch:=puint8(kalloc(v^.BlockSize)); + if (v^.Super=nil) or (v^.Groups=nil) or (op^.Scratch=nil) then begin + if v^.Super<>nil then kfree(void(v^.Super)); if v^.Groups<>nil then kfree(void(v^.Groups)); + if op^.Scratch<>nil then kfree(void(op^.Scratch)); + kfree(void(op)); kfree(void(v)); exit; + end; + memset(uint32(v^.Super),0,1024); memset(uint32(v^.Groups),0,v^.GDTBlocks*v^.BlockSize); + Put32(v^.Super,0,v^.InodeCount); Put32(v^.Super,4,v^.BlockCount); + Put32(v^.Super,16,v^.InodeCount-10); Put32(v^.Super,20,v^.FirstBlock); + i:=0; if v^.BlockSize=2048 then i:=1 else if v^.BlockSize=4096 then i:=2; + Put32(v^.Super,24,i); Put32(v^.Super,28,i); + Put32(v^.Super,32,v^.BlocksPerGroup); Put32(v^.Super,36,v^.BlocksPerGroup); + Put32(v^.Super,40,v^.InodesPerGroup); Put16(v^.Super,54,$FFFF); + Put16(v^.Super,56,EXT2_MAGIC); Put16(v^.Super,58,1); Put16(v^.Super,60,1); + Put32(v^.Super,76,1); Put32(v^.Super,84,11); Put16(v^.Super,88,128); + Put32(v^.Super,96,2); Put32(v^.Super,100,3); { file types, sparse super, large file } + for i:=0 to 3 do Put32(v^.Super,104+i*4,core.rand.rand32); + v^.Super[110]:=(v^.Super[110] and $0F) or $40; + v^.Super[112]:=(v^.Super[112] and $3F) or $80; + if config<>nil then if (config^.Flags and FS_FORMAT_PARAM_LABEL)<>0 then + memcpy(uint32(@config^.LabelText[0]),uint32(v^.Super)+120,11); + tables:=v^.InodesPerGroup div (v^.BlockSize div 128); freeBlocks:=0; rootBlock:=0; + for g:=0 to v^.GroupCount-1 do begin + first:=v^.FirstBlock+g*v^.BlocksPerGroup; count:=v^.BlockCount-first; + if count>v^.BlocksPerGroup then count:=v^.BlocksPerGroup; + reserve:=0; if HasSuper(g,1) then reserve:=1+v^.GDTBlocks; + d:=PExt2Group(uint32(v^.Groups)+g*32); + d^.BlockBitmap:=first+reserve; d^.InodeBitmap:=first+reserve+1; d^.InodeTable:=first+reserve+2; + used:=reserve+2+tables; + d^.FreeInodes:=v^.InodesPerGroup; + if g=0 then begin rootBlock:=first+used; Inc(used); Dec(d^.FreeInodes,10); d^.Directories:=1; end; + d^.FreeBlocks:=count-used; Inc(freeBlocks,d^.FreeBlocks); + end; + Put32(v^.Super,12,freeBlocks); + { Invalidate the primary signature before writing metadata. Publish it last. } + memset(uint32(op^.Scratch),0,v^.BlockSize); + op^.Error:=storage_write(volume^.device,start+2,2,puint32(op^.Scratch)); + g:=0; + while (gv^.BlocksPerGroup then count:=v^.BlocksPerGroup; + d:=PExt2Group(uint32(v^.Groups)+g*32); used:=count-d^.FreeBlocks; + memset(uint32(op^.Scratch),$FF,v^.BlockSize); + for bit:=used to count-1 do op^.Scratch[bit div 8]:=op^.Scratch[bit div 8] and not (1 shl (bit mod 8)); + WriteBlock(op,d^.BlockBitmap,op^.Scratch); + memset(uint32(op^.Scratch),$FF,v^.BlockSize); + used:=0; if g=0 then used:=10; + for bit:=used to v^.InodesPerGroup-1 do op^.Scratch[bit div 8]:=op^.Scratch[bit div 8] and not (1 shl (bit mod 8)); + WriteBlock(op,d^.InodeBitmap,op^.Scratch); + memset(uint32(op^.Scratch),0,v^.BlockSize); + for i:=0 to tables-1 do begin + if not WriteBlock(op,d^.InodeTable+i,op^.Scratch) then break; + end; + if HasSuper(g,1) then begin + for i:=0 to v^.GDTBlocks-1 do + if not WriteBlock(op,first+1+i,pointer(uint32(v^.Groups)+i*v^.BlockSize)) then break; + if g<>0 then begin + memset(uint32(op^.Scratch),0,v^.BlockSize); + memcpy(uint32(v^.Super),uint32(op^.Scratch),1024); Put16(op^.Scratch,90,g); + WriteBlock(op,first,op^.Scratch); + end; + end; + Inc(g); + end; + if op^.Error=eNone then begin + memset(uint32(op^.Scratch),0,v^.BlockSize); + inode:=PExt2Inode(uint32(op^.Scratch)+128); + inode^.Mode:=EXT2_DIR or $1ED; inode^.Size:=v^.BlockSize; inode^.Links:=2; + inode^.Sectors:=v^.SectorsPerBlock; inode^.Blocks[0]:=rootBlock; + WriteBlock(op,PExt2Group(v^.Groups)^.InodeTable,op^.Scratch); + memset(uint32(op^.Scratch),0,v^.BlockSize); + Put32(op^.Scratch,0,2); Put16(op^.Scratch,4,12); op^.Scratch[6]:=1; op^.Scratch[7]:=2; op^.Scratch[8]:=ord('.'); + Put32(op^.Scratch,12,2); Put16(op^.Scratch,16,v^.BlockSize-12); op^.Scratch[18]:=2; op^.Scratch[19]:=2; + op^.Scratch[20]:=ord('.'); op^.Scratch[21]:=ord('.'); + WriteBlock(op,rootBlock,op^.Scratch); + if op^.Error=eNone then op^.Error:=storage_write(volume^.device,start+2,2,puint32(v^.Super)); + end; + err:=op^.Error; + kfree(void(op^.Scratch)); kfree(void(v^.Super)); kfree(void(v^.Groups)); kfree(void(op)); kfree(void(v)); + FormatVolume:=err; +end; + +function SetLabel(volume : PStorage_Volume; text : pchar) : TError; +var op : PExt2Op; labelText : TVolumeLabel; g, first : uint32; +begin + SetLabel:=ValidateLabel(text,labelText); if SetLabel<>eNone then exit; + op:=BeginOp(volume); if op=nil then begin SetLabel:=eOutOfMemory; exit; end; + if BeginMutation(op) then begin + memcpy(uint32(@labelText),uint32(op^.V^.Super)+120,16); + for g:=1 to op^.V^.GroupCount-1 do if HasSuper(g,Get32(op^.V^.Super,100)) then begin + first:=op^.V^.FirstBlock+g*op^.V^.BlocksPerGroup; + if not ReadBlock(op,first,op^.Scratch) then break; + memcpy(uint32(@labelText),uint32(op^.Scratch)+120,16); + if not WriteBlock(op,first,op^.Scratch) then break; + end; + if FinishMutation(op) then memcpy(uint32(@labelText),uint32(@volume^.labelText),sizeof(labelText)); + end; + SetLabel:=op^.Error; EndOp(op); +end; + +end. diff --git a/src/driver/storage/fs/ext2/driver.storage.fs.ext2.pas b/src/driver/storage/fs/ext2/driver.storage.fs.ext2.pas new file mode 100644 index 00000000..4ecade60 --- /dev/null +++ b/src/driver/storage/fs/ext2/driver.storage.fs.ext2.pas @@ -0,0 +1,209 @@ +unit driver.storage.fs.ext2; + +interface +uses driver.storage.types; +procedure init; +procedure ReleaseVolume(volume : PStorage_Volume); + +implementation +uses boot.mgr, driver.storage.fs.mgr, driver.storage.vol.mgr, + driver.storage.fs.ext2.types, driver.storage.fs.ext2.core, + driver.storage.fs.ext2.format, memory.heap, core.util, core.strings, + proc.mgr, proc.types; + +type + PWork = ^TWork; + TWork = record + Next : PWork; + Volume : PStorage_Volume; + FileCtx : PExt2File; + Directory, Name : pchar; + Offset, Count : uint32; + Buffer, Completed : puint32; + Writing : boolean; + Callback : TIOCallback; + CallbackData : pointer; + end; +var + filesystem : TFilesystem; + worker : PProcessContext = nil; + head, tail : PWork; + queued : uint32; + +procedure ReleaseVolume(volume : PStorage_Volume); +begin driver.storage.fs.ext2.core.ReleaseVolume(volume); end; + +function DoTransfer(volume : PStorage_Volume; directory, name : pchar; offset : uint32; + buffer : puint32; count : uint32; ctx : pointer; writing : boolean; var error : TError) : uint32; +var f : PExt2File; size : uint32; owned : boolean; +begin + f:=PExt2File(ctx); owned:=f=nil; + if owned then f:=PExt2File(OpenFile(volume,directory,name,writing,size)); + if f=nil then begin DoTransfer:=0; error:=eFileDoesNotExist; exit; end; + DoTransfer:=Transfer(volume,f,offset,buffer,count,writing,error); + if owned then CloseFile(f); +end; + +function ReadOffset(volume : PStorage_Volume; directory, name : pchar; offset : uint32; + buffer : puint32; count : uint32; ctx : pointer) : uint32; +var error : TError; +begin ReadOffset:=DoTransfer(volume,directory,name,offset,buffer,count,ctx,false,error); end; + +function WriteOffset(volume : PStorage_Volume; directory, name : pchar; offset : uint32; + buffer : puint32; count : uint32; ctx : pointer) : uint32; +var error : TError; +begin WriteOffset:=DoTransfer(volume,directory,name,offset,buffer,count,ctx,true,error); end; + +procedure FreeWork(work : PWork); +begin + if work^.Directory<>nil then kfree(void(work^.Directory)); + if work^.Name<>nil then kfree(void(work^.Name)); + kfree(void(work)); +end; + +procedure WorkLoop(pctx : PProcessContext); +var work : PWork; done : uint32; error : TError; callback : TIOCallback; data : pointer; +begin + while true do begin + asm pushf; cli end; + work:=head; + if work<>nil then begin + head:=work^.Next; if head=nil then tail:=nil; Dec(queued); + end else pctx^.State:=psAwaiting; + asm popf end; + if work=nil then begin + { Queue publication wakes the worker under the same CLI section. + No per-I/O process, IRQ continuation, or volatile spin is needed. } + proc.mgr.proc_yield; + continue; + end; + done:=DoTransfer(work^.Volume,work^.Directory,work^.Name,work^.Offset, + work^.Buffer,work^.Count,work^.FileCtx,work^.Writing,error); + if work^.Completed<>nil then work^.Completed^:=done; + callback:=work^.Callback; data:=work^.CallbackData; + FreeWork(work); + if callback<>nil then callback(error,data); + end; +end; + +procedure StartWorker; +begin worker:=proc.mgr.create('ext2.io',@WorkLoop,nil,1); end; + +procedure QueueTransfer(volume : PStorage_Volume; directory, name : pchar; offset : uint32; + buffer : puint32; count : uint32; ctx : pointer; completed : puint32; + callback : TIOCallback; data : pointer; writing : boolean); +var work : PWork; error : TError; +begin + if completed<>nil then completed^:=0; + error:=eNone; + if (volume=nil) or ((buffer=nil) and (count<>0)) or (count>$FFFFFFFF-offset) then error:=eInvalidArgument + else if worker=nil then error:=eDeviceNotReady; + if error<>eNone then begin if callback<>nil then callback(error,data); exit; end; + work:=PWork(kalloc(sizeof(TWork))); + if work=nil then begin if callback<>nil then callback(eOutOfMemory,data); exit; end; + memset(uint32(work),0,sizeof(TWork)); + if ctx=nil then begin + if (directory=nil) or (name=nil) then error:=eInvalidArgument else begin + work^.Directory:=stringCopy(directory); work^.Name:=stringCopy(name); + if (work^.Directory=nil) or (work^.Name=nil) then error:=eOutOfMemory; + end; + end; + if error<>eNone then begin FreeWork(work); if callback<>nil then callback(error,data); exit; end; + work^.Volume:=volume; work^.FileCtx:=PExt2File(ctx); work^.Offset:=offset; + work^.Buffer:=buffer; work^.Count:=count; work^.Completed:=completed; + work^.Writing:=writing; work^.Callback:=callback; work^.CallbackData:=data; + asm pushf; cli end; + if queued>=64 then error:=eQueueFull else begin + if tail=nil then head:=work else tail^.Next:=work; + tail:=work; Inc(queued); + if worker^.State=psAwaiting then worker^.State:=psReady; + end; + asm popf end; + if error<>eNone then begin FreeWork(work); if callback<>nil then callback(error,data); end; +end; + +procedure ReadAsync(volume : PStorage_Volume; directory, name : pchar; offset : uint32; + buffer : puint32; count : uint32; ctx : pointer; completed : puint32; callback : TIOCallback; data : pointer); +begin QueueTransfer(volume,directory,name,offset,buffer,count,ctx,completed,callback,data,false); end; + +procedure WriteAsync(volume : PStorage_Volume; directory, name : pchar; offset : uint32; + buffer : puint32; count : uint32; ctx : pointer; completed : puint32; callback : TIOCallback; data : pointer); +begin QueueTransfer(volume,directory,name,offset,buffer,count,ctx,completed,callback,data,true); end; + +function FileSize(volume : PStorage_Volume; directory, name : pchar) : uint32; +var f : pointer; size : uint32; +begin + f:=OpenFile(volume,directory,name,false,size); FileSize:=size; + if f<>nil then CloseFile(f); +end; + +procedure CreateDirectory(volume : PStorage_Volume; directory, name : pchar; attributes : uint32; status : puint32); +var op : PExt2Op; parent, number : uint32; inode : TExt2Inode; +begin + if status<>nil then status^:=ord(eOutOfMemory); + op:=BeginOp(volume); if op=nil then exit; + if Resolve(op,directory,parent,inode) then + if CreateNode(op,parent,name,true,number) then FinishMutation(op); + if status<>nil then status^:=ord(op^.Error); + EndOp(op); +end; + +procedure DeleteFile(volume : PStorage_Volume; path : pchar; status : puint32); +var op : PExt2Op; +begin + if status<>nil then status^:=ord(eOutOfMemory); + op:=BeginOp(volume); if op=nil then exit; + RemoveNode(op,path,false); if status<>nil then status^:=ord(op^.Error); EndOp(op); +end; + +procedure DeleteDirectory(volume : PStorage_Volume; path : pchar; status : puint32); +var op : PExt2Op; +begin + if status<>nil then status^:=ord(eOutOfMemory); + op:=BeginOp(volume); if op=nil then exit; + RemoveNode(op,path,true); if status<>nil then status^:=ord(op^.Error); EndOp(op); +end; + +procedure RenameFile(volume : PStorage_Volume; path, newName : pchar; status : puint32); +var op : PExt2Op; +begin + if status<>nil then status^:=ord(eOutOfMemory); + op:=BeginOp(volume); if op=nil then exit; + RenameNode(op,path,newName); if status<>nil then status^:=ord(op^.Error); EndOp(op); +end; + +procedure Detect(device : PStorage_Device); +var volume : PStorage_Volume; +begin + if device=nil then exit; + volume:=PStorage_Volume(kalloc(sizeof(TStorage_Volume))); if volume=nil then exit; + memset(uint32(volume),0,sizeof(TStorage_Volume)); + volume^.device:=device; volume^.sectorCount:=device^.maxSectorCount; volume^.sectorSize:=device^.sectorSize; + if Identify(volume) then begin + volume^.filesystem:=@filesystem; + driver.storage.vol.mgr.register_volume(device,volume); + end else kfree(void(volume)); +end; + +procedure init; +begin + memset(uint32(@filesystem),0,sizeof(filesystem)); + filesystem.sName:='EXT2'; filesystem.system_id:=$83; + filesystem.identifyCallback:=@Identify; filesystem.detectCallback:=@Detect; + filesystem.validateFormat:=@ValidateFormat; filesystem.createCallback:=@FormatVolume; + filesystem.formatParamFlags:=FS_FORMAT_PARAM_CLUSTER_SIZE or FS_FORMAT_PARAM_FILE_COUNT or FS_FORMAT_PARAM_LABEL; + filesystem.validateLabelCallback:=@ValidateLabel; filesystem.setLabelCallback:=@SetLabel; + filesystem.readDirCallback:=@ReadDirectory; filesystem.createDirCallback:=@CreateDirectory; + filesystem.deleteFileCallback:=@DeleteFile; filesystem.deleteDirCallback:=@DeleteDirectory; + filesystem.renameFileCallback:=@RenameFile; + filesystem.openFileModeCallback:=@OpenFile; filesystem.closeFileCallback:=@CloseFile; + filesystem.readOffsetCallback:=@ReadOffset; filesystem.writeOffsetCallback:=@WriteOffset; + filesystem.readOffsetAsyncCallback:=@ReadAsync; filesystem.writeOffsetAsyncCallback:=@WriteAsync; + filesystem.fileSizeCallback:=@FileSize; + driver.storage.fs.mgr.register_filesystem(@filesystem); +end; + +initialization + boot.mgr.registerBoot('driver.storage.fs.ext2',@init,'Ext2 Filesystem','driver.storage.fs.mgr'); + boot.mgr.registerBoot('driver.storage.ext2.worker',@StartWorker,'Ext2 I/O Worker','proc.mgr'); +end. diff --git a/src/driver/storage/fs/ext2/driver.storage.fs.ext2.types.pas b/src/driver/storage/fs/ext2/driver.storage.fs.ext2.types.pas new file mode 100644 index 00000000..a6032da6 --- /dev/null +++ b/src/driver/storage/fs/ext2/driver.storage.fs.ext2.types.pas @@ -0,0 +1,62 @@ +unit driver.storage.fs.ext2.types; + +interface +uses driver.storage.types; + +const + EXT2_MAGIC = $EF53; + EXT2_DIR = $4000; + EXT2_FILE = $8000; + EXT2_ROOT = 2; + +type + TExt2Inode = packed record + Mode, UID : uint16; + Size, ATime, CTime, MTime, DTime : uint32; + GID, Links : uint16; + Sectors, Flags, OSData : uint32; + Blocks : array[0..14] of uint32; + Generation, ACL, SizeHigh, Fragment : uint32; + OSExtra : array[0..11] of uint8; + end; + PExt2Inode = ^TExt2Inode; + TExt2Group = packed record + BlockBitmap, InodeBitmap, InodeTable : uint32; + FreeBlocks, FreeInodes, Directories, Pad : uint16; + Reserved : array[0..2] of uint32; + end; + PExt2Group = ^TExt2Group; + TExt2Dirent = packed record + Inode : uint32; + Length : uint16; + NameLength, FileType : uint8; + end; + PExt2Dirent = ^TExt2Dirent; + PExt2Volume = ^TExt2Volume; + PExt2File = ^TExt2File; + TExt2Volume = record + Volume : PStorage_Volume; + Super, Groups : puint8; + BlockSize, SectorsPerBlock, BlockCount, InodeCount : uint32; + FirstBlock, BlocksPerGroup, InodesPerGroup, InodeSize : uint32; + GroupCount, GDTBlocks, AllocGroup : uint32; + Busy, Writable, Poisoned, FileTypes : boolean; + Files : PExt2File; + end; + TExt2File = record + Volume : PExt2Volume; + Inode : uint32; + Next : PExt2File; + end; + { One operation owns its buffers. No DMA destination is shared with another + operation. The volume lock protects metadata and all read/modify/write. } + TExt2Op = record + V : PExt2Volume; + Scratch, Data : puint8; + Error : TError; + Mutating : boolean; + end; + PExt2Op = ^TExt2Op; + +implementation +end. -- 2.54.0 From c28b9dd77f58f91ad56144c3cebd9234bce66716 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 31 Oct 2026 20:36:52 +0000 Subject: [PATCH 15/21] Implement USB mass storage and batch OHCI bulk transfers --- .../storage/ctl/driver.storage.ctl.usb.md | 85 +- src/driver/bus/usb/driver.bus.usb.core.pas | 30 +- src/driver/bus/usb/driver.bus.usb.ehci.pas | 33 +- src/driver/bus/usb/driver.bus.usb.ohci.pas | 116 ++- src/driver/bus/usb/driver.bus.usb.types.pas | 2 + .../storage/ctl/driver.storage.ctl.usb.pas | 797 ++++++++++++++++-- src/test/test.pas | 2 + 7 files changed, 919 insertions(+), 146 deletions(-) diff --git a/doc/src/driver/storage/ctl/driver.storage.ctl.usb.md b/doc/src/driver/storage/ctl/driver.storage.ctl.usb.md index a9a95ab9..7ff06d8b 100644 --- a/doc/src/driver/storage/ctl/driver.storage.ctl.usb.md +++ b/doc/src/driver/storage/ctl/driver.storage.ctl.usb.md @@ -1,49 +1,68 @@ -# driver.storage.ctl.usb +# USB mass storage -USB Mass Storage Class driver stub. +`driver.storage.ctl.usb` implements SCSI transparent commands over USB Mass +Storage Bulk-Only Transport (interface class/subclass/protocol `08/06/50`). -## Overview +It probes INQUIRY, TEST UNIT READY and READ CAPACITY(10), registers LUN 0 with +the storage manager, and exposes READ(10), WRITE(10), REQUEST SENSE and +SYNCHRONIZE CACHE(10). Existing partition discovery and filesystem mounting +apply to the resulting USB disk. Writes complete only after cache synchronization. -This unit registers a USB class driver with the driver manager (`driver.mgr`) to claim USB Mass Storage Class interfaces (class `$08`, subclass `$06` SCSI, protocol `$50` Bulk-Only). When the USB stack enumerates a matching interface, the `load` callback is invoked. Currently the callback only logs a detection message; no I/O infrastructure is established. +## Execution and ownership -Full Bulk-Only Transport (BOT) support — including Max LUN query, endpoint setup, CBW/CSW command exchange, and `driver.storage.mgr` device registration — is planned but not yet implemented. +An attachment task probes and registers each disk after scheduling starts; boot +initialization never waits on a worker that cannot run yet. -## Boot Registration +Storage dispatch only publishes a request. One persistent `usb.storage` worker +performs I/O in task context, with one command active per device and a reusable, +page-aligned 4 KiB bounce buffer. Caller buffers are never submitted for DMA. +Commands are split at the bounce-buffer boundary; all LBA/count and buffer-end +arithmetic is checked before submission. CBW/CSW wire sizes, tags, status, +residue, and actual transfer lengths are validated. -Registered with `boot.mgr` as `driver.storage.ctl.usb` at the `device` barrier. The barrier order places `device` after `bus`, ensuring `driver.bus.usb.core.init` has run before the storage class driver registers with the driver manager. +OHCI batches bulk data into spans of at most 4 KiB per transfer descriptor, +using the endpoint's hardware toggle carry across packets. A 4 KiB full-speed +read uses one data descriptor instead of 64. Short packets still halt the chain +and report the actual byte count; interrupt endpoint packet sizing is unchanged. -## Dependencies +The worker performs reset recovery (class reset, clear bulk-IN halt, clear +bulk-OUT halt) after malformed status or phase errors. Failed SCSI commands +request sense and return storage errors, including write protection. -- `boot.mgr` +On disconnect the disk is marked removed immediately. Accepted requests are +completed with errors, and idle volumes are invalidated/unmounted by the worker; +busy volumes are retried. Storage-manager records remain stable tombstones. +Class-driver buffers and context are freed after pending hardware work drains. -- `driver.mgr` -- `memory.heap` -- `driver.storage.types` -- `io.syslog` -- `debug.tracer` -- `driver.bus.usb.core` -- `driver.bus.usb.types` -- `core.util`, `arch.x86.util` +Host controllers currently have no cancellation API. A timed-out transfer is +therefore quarantined with its private device snapshot and DMA memory. No further +commands are issued to that device. If hardware never completes, those allocations +remain retained until reboot; freeing them would permit DMA into reused memory. +The timeout uses the PIT's actual frequency and a bounded polling fallback. -## Constants +## Supported scope -| Constant | Value | Description | -|---|---|---| -| `USB_CLASS_MASS_STORAGE` | `$08` | USB interface class for mass storage | -| `USB_SC_SCSI` | `$06` | SCSI transparent command set subclass | -| `USB_PROTO_BBB` | `$50` | Bulk-Only (BOT) transport protocol | +- LUN 0 on a single-interface device with one bulk-IN and one bulk-OUT endpoint. +- Direct-access disks with power-of-two sectors from 512 through 4096 bytes. +- Capacity representable by the existing 32-bit storage-sector API; the + READ CAPACITY(16) sentinel is rejected. +- OHCI, EHCI and xHCI bulk transfers. OHCI/EHCI report actual data lengths and + terminate bulk reads on short packets. -## Functions and Procedures +UAS, additional LUNs, composite/alternate-setting interfaces, UHCI storage, +optical devices, and media replacement without USB reconnect are not supported. +The current xHCI driver lacks endpoint-ring reset support: a halted bulk endpoint +fails the device closed rather than attempting an unsafe ring reuse. -### init +## Validation -```pascal -procedure init; -``` +`UnitTest` is called by the boot test suite and is available as `USBMSCTEST`. +Tests use a memory-only mock host controller: wire layout, byte order, capacity +limits, reads, writes, sense errors, short transfers, status stalls, reset ordering, +and request bounds. They perform no disk I/O. -Constructs a `TDeviceIdentifier` matching any USB device with `bInterfaceClass = $08`, `bInterfaceSubClass = $06`, `bInterfaceProtocol = $50`, and registers the `load` callback with `driver.mgr`. +For hardware/VM checks, boot with a disposable USB disk, confirm `USBMSC` discovery +and filesystem mounting, test file read/write/read-back, then unplug while idle +and during I/O. Do not run destructive disk tests against a disk containing data. -## Notes - -- The `load` callback currently returns true unconditionally after logging a detection message. -- Once full BOT support is implemented, `load` should query the Max LUN, allocate endpoint structures, and call `driver.storage.mgr.register_device` for each logical unit. +Protocol: [USB-IF Bulk-Only Transport 1.0](https://www.usb.org/sites/default/files/usbmassbulk_10.pdf). diff --git a/src/driver/bus/usb/driver.bus.usb.core.pas b/src/driver/bus/usb/driver.bus.usb.core.pas index 1ce08a85..afebffd1 100644 --- a/src/driver/bus/usb/driver.bus.usb.core.pas +++ b/src/driver/bus/usb/driver.bus.usb.core.pas @@ -658,6 +658,7 @@ begin { Step 2: Create device at address 0 } dev := PUSBDevice(kalloc(sizeof(TUSBDevice))); + if dev = nil then begin pop_trace; exit; end; dev^.Address := 0; dev^.Speed := speed; dev^.MaxPacket0 := 8; { Default, will be updated from descriptor } @@ -667,6 +668,8 @@ begin dev^.ParentPort := parentPort; dev^.NumEndpoints := 0; dev^.Configured := false; + dev^.InterfaceNumber := 0; + dev^.InterfaceCount := 0; dev^.fnDisconnect := nil; { Step 3: Get first 8 bytes of device descriptor to learn MaxPacketSize0 } @@ -730,7 +733,14 @@ begin end; totalLen := cfgDesc.wTotalLength; + if totalLen < sizeof(TUSBConfigDescriptor) then begin + kfree(void(dev)); + pop_trace; + exit; + end; + dev^.InterfaceCount := cfgDesc.bNumInterfaces; cfgBuf := Pointer(kalloc(totalLen)); + if cfgBuf = nil then begin kfree(void(dev)); pop_trace; exit; end; status := usb_get_descriptor(dev, USB_DESC_CONFIGURATION, 0, cfgBuf, totalLen); if status <> tsSuccess then begin @@ -744,12 +754,14 @@ begin { Step 7: Parse interface and endpoint descriptors } offset := sizeof(TUSBConfigDescriptor); dev^.NumEndpoints := 0; - while offset < totalLen do begin + while offset + 2 <= totalLen do begin { Read descriptor length and type } - if PUint8(uint32(cfgBuf) + offset)^ = 0 then break; { Safety: zero-length = corrupt } + if PUint8(uint32(cfgBuf) + offset)^ < 2 then break; + if offset + PUint8(uint32(cfgBuf) + offset)^ > totalLen then break; if PUint8(uint32(cfgBuf) + offset + 1)^ = USB_DESC_ENDPOINT then begin - if dev^.NumEndpoints < USB_MAX_ENDPOINTS then begin + if (PUint8(uint32(cfgBuf) + offset)^ >= sizeof(TUSBEndpointDescriptor)) and + (dev^.NumEndpoints < USB_MAX_ENDPOINTS) then begin epDesc := PUSBEndpointDescriptor(uint32(cfgBuf) + offset); dev^.Endpoints[dev^.NumEndpoints].Address := usb_ep_number(epDesc^.bEndpointAddress); dev^.Endpoints[dev^.NumEndpoints].Direction := usb_ep_direction(epDesc^.bEndpointAddress); @@ -780,16 +792,22 @@ begin { Find the first interface descriptor for class/subclass/protocol } offset := sizeof(TUSBConfigDescriptor); ifDesc := nil; - while offset < totalLen do begin - if PUint8(uint32(cfgBuf) + offset)^ = 0 then break; - if PUint8(uint32(cfgBuf) + offset + 1)^ = USB_DESC_INTERFACE then begin + while offset + 2 <= totalLen do begin + if PUint8(uint32(cfgBuf) + offset)^ < 2 then break; + if offset + PUint8(uint32(cfgBuf) + offset)^ > totalLen then break; + if (PUint8(uint32(cfgBuf) + offset + 1)^ = USB_DESC_INTERFACE) and + (PUint8(uint32(cfgBuf) + offset)^ >= sizeof(TUSBInterfaceDescriptor)) then begin ifDesc := PUSBInterfaceDescriptor(uint32(cfgBuf) + offset); + dev^.InterfaceNumber := ifDesc^.bInterfaceNumber; break; end; offset := offset + PUint8(uint32(cfgBuf) + offset)^; end; DevID := PDeviceIdentifier(kalloc(sizeof(TDeviceIdentifier))); + if DevID = nil then begin + kfree(void(cfgBuf)); kfree(void(dev)); pop_trace; exit; + end; DevID^.Bus := biUSB; DevID^.id0 := (uint32(devDesc.idVendor) SHL 16) OR devDesc.idProduct; DevID^.id1 := devDesc.bDeviceClass; diff --git a/src/driver/bus/usb/driver.bus.usb.ehci.pas b/src/driver/bus/usb/driver.bus.usb.ehci.pas index b78652a8..56b1a53d 100644 --- a/src/driver/bus/usb/driver.bus.usb.ehci.pas +++ b/src/driver/bus/usb/driver.bus.usb.ehci.pas @@ -963,6 +963,8 @@ end; { ========================= Submit Transfer ========================= } +procedure ehci_free_qtd_chain(first : PEHCI_qTD); forward; + function ehci_submit_control(hc : PUSBHCDriver; transfer : PUSBTransfer) : boolean; var priv : PEHCI_PrivData; @@ -1004,7 +1006,7 @@ begin { SETUP qTD } qtd := ehci_alloc_qtd; - if qtd = nil then begin pop_trace; exit; end; + if qtd = nil then begin ehci_free_qtd_chain(firstqTD); pop_trace; exit; end; firstqTD := qtd; qtd^.Token := ehci_make_qtd_token(EHCI_QTD_PID_SETUP, false, 8, false, 3); ehci_fill_qtd_buffers(qtd, @transfer^.Setup, 8); @@ -1021,7 +1023,7 @@ begin while remaining > 0 do begin qtd := ehci_alloc_qtd; - if qtd = nil then begin pop_trace; exit; end; + if qtd = nil then begin ehci_free_qtd_chain(firstqTD); pop_trace; exit; end; if remaining > maxPkt then pktLen := maxPkt @@ -1034,6 +1036,7 @@ begin pid := EHCI_QTD_PID_OUT; qtd^.Token := ehci_make_qtd_token(pid, toggle, pktLen, false, 3); + qtd^.SWPad0 := pktLen; { Original data length, excluding SETUP/status } ehci_fill_qtd_buffers(qtd, Pointer(uint32(transfer^.Buffer) + offset), pktLen); qtd^.NextqTD := EHCI_QTD_T; qtd^.AltNextqTD := EHCI_QTD_T; @@ -1052,7 +1055,7 @@ begin { STATUS qTD (opposite direction, DATA1) } qtd := ehci_alloc_qtd; - if qtd = nil then begin pop_trace; exit; end; + if qtd = nil then begin ehci_free_qtd_chain(firstqTD); pop_trace; exit; end; if (transfer^.Setup.bmRequestType AND $80) <> 0 then pid := EHCI_QTD_PID_OUT @@ -1070,7 +1073,7 @@ begin { Build QH } qh := ehci_alloc_qh; - if qh = nil then begin pop_trace; exit; end; + if qh = nil then begin ehci_free_qtd_chain(firstqTD); pop_trace; exit; end; qh^.EPChars := ehci_make_qh_epchars(devAddr, 0, speedBits, maxPkt, true, false, isCtrlEP, 4); @@ -1152,7 +1155,7 @@ begin repeat qtd := ehci_alloc_qtd; - if qtd = nil then begin pop_trace; exit; end; + if qtd = nil then begin ehci_free_qtd_chain(firstqTD); pop_trace; exit; end; if firstqTD = nil then firstqTD := qtd; @@ -1164,6 +1167,7 @@ begin pktLen := 0; qtd^.Token := ehci_make_qtd_token(pid, toggle, pktLen, false, 3); + qtd^.SWPad0 := pktLen; { Original data length, excluding SETUP/status } if pktLen > 0 then ehci_fill_qtd_buffers(qtd, Pointer(uint32(transfer^.Buffer) + offset), pktLen); qtd^.NextqTD := EHCI_QTD_T; @@ -1200,7 +1204,7 @@ begin { Build QH } qh := ehci_alloc_qh; - if qh = nil then begin pop_trace; exit; end; + if qh = nil then begin ehci_free_qtd_chain(firstqTD); pop_trace; exit; end; qh^.EPChars := ehci_make_qh_epchars(devAddr, epNum, speedBits, maxPkt, true, false, false, 4); @@ -1297,6 +1301,11 @@ begin ehci_transfer_done := true; exit; end; + if (qh^.SWTransfer <> nil) and + (PUSBTransfer(qh^.SWTransfer)^.PipeType = ptBulk) and + (((qtd^.Token SHR 16) AND $7FFF) <> 0) then begin + ehci_transfer_done := true; exit; + end; qtd := qtd^.SWNext; end; ehci_transfer_done := true; @@ -1385,6 +1394,8 @@ var transfer : PUSBTransfer; head : PEHCI_QH; listIdx : uint32; + qtd : PEHCI_qTD; + residual : uint32; begin if (hc = nil) or (hc^.PrivData = nil) then exit; priv := PEHCI_PrivData(hc^.PrivData); @@ -1422,7 +1433,15 @@ begin if transfer^.Status = tsInProgress then begin if ehci_transfer_done(qh) then begin transfer^.Status := ehci_transfer_status(qh); - transfer^.ActualLen := transfer^.BufferLen; + transfer^.ActualLen := 0; + qtd := qh^.SWFirstqTD; + while qtd <> nil do begin + if (qtd^.Token AND EHCI_QTD_STS_ACTIVE) <> 0 then break; + residual := (qtd^.Token SHR 16) AND $7FFF; + if (qtd^.SWPad0 > 0) and (residual <= qtd^.SWPad0) then + inc(transfer^.ActualLen, qtd^.SWPad0 - residual); + qtd := qtd^.SWNext; + end; { Remove QH from schedule and free qTD chain } if listIdx = 0 then ehci_remove_async_qh(priv, qh) diff --git a/src/driver/bus/usb/driver.bus.usb.ohci.pas b/src/driver/bus/usb/driver.bus.usb.ohci.pas index b40e30b3..642954a0 100644 --- a/src/driver/bus/usb/driver.bus.usb.ohci.pas +++ b/src/driver/bus/usb/driver.bus.usb.ohci.pas @@ -678,6 +678,8 @@ begin end; { Build a control transfer: SETUP TD, optional DATA TDs, STATUS TD, all on a new ED } +procedure ohci_free_td_chain(firstTD : POHCI_TD); forward; + function ohci_submit_control(hc : PUSBHCDriver; transfer : PUSBTransfer) : boolean; var priv : POHCI_PrivData; @@ -713,7 +715,7 @@ begin { === SETUP TD === } td := ohci_alloc_td; - if td = nil then begin pop_trace; exit; end; + if td = nil then begin ohci_free_td_chain(firstTD); pop_trace; exit; end; firstTD := td; td^.Control := ohci_make_td_control(OHCI_TD_DP_SETUP, OHCI_TD_DT_DATA0, @@ -732,7 +734,7 @@ begin while remaining > 0 do begin td := ohci_alloc_td; - if td = nil then begin pop_trace; exit; end; + if td = nil then begin ohci_free_td_chain(firstTD); pop_trace; exit; end; if remaining > maxPkt then pktLen := maxPkt @@ -752,6 +754,7 @@ begin td^.BE := vtop(uint32(transfer^.Buffer) + offset + uint32(pktLen) - 1) else td^.BE := 0; + td^.SWPad := pktLen; { Original data length, excluding SETUP/status } td^.NextTD := 0; td^.SWTransfer := Pointer(transfer); td^.SWNext := nil; @@ -773,7 +776,7 @@ begin { === STATUS TD === } td := ohci_alloc_td; - if td = nil then begin pop_trace; exit; end; + if td = nil then begin ohci_free_td_chain(firstTD); pop_trace; exit; end; { Status stage direction is opposite of data stage } if (transfer^.Setup.bmRequestType AND $80) <> 0 then @@ -794,13 +797,13 @@ begin { Allocate a dummy/sentinel TD for the tail pointer (driver.bus.usb.ohci requires HeadP <> TailP to process) } tailTD := ohci_alloc_td; - if tailTD = nil then begin pop_trace; exit; end; + if tailTD = nil then begin ohci_free_td_chain(firstTD); pop_trace; exit; end; td^.NextTD := vtop(uint32(tailTD)); td^.SWNext := tailTD; { === Create ED === } ed := ohci_alloc_ed; - if ed = nil then begin pop_trace; exit; end; + if ed = nil then begin ohci_free_td_chain(firstTD); pop_trace; exit; end; ed^.Control := ohci_make_ed_control(devAddr, 0, OHCI_ED_DIR_FROM_TD, speedBit, maxPkt, false); @@ -825,6 +828,18 @@ begin pop_trace; end; +{ A general TD covers at most two physical pages. A 4 KiB span is safe + even for an unaligned buffer; intermediate TDs must end on packet boundaries. } +function ohci_async_length(remaining, maxPkt : uint32; bulk : boolean) : uint32; +var limit : uint32; +begin + limit := maxPkt; + if bulk and (maxPkt > 0) and (maxPkt <= 4096) then + limit := 4096 - (4096 mod maxPkt); + if remaining < limit then limit := remaining; + ohci_async_length := limit; +end; + { Build TDs for an interrupt or bulk transfer on a new ED } function ohci_submit_async(hc : PUSBHCDriver; transfer : PUSBTransfer) : boolean; var @@ -882,24 +897,25 @@ begin { Build TD chain } repeat td := ohci_alloc_td; - if td = nil then begin pop_trace; exit; end; + if td = nil then begin ohci_free_td_chain(firstTD); pop_trace; exit; end; if firstTD = nil then firstTD := td; - if remaining > maxPkt then - pktLen := maxPkt - else if remaining > 0 then - pktLen := remaining - else - pktLen := 0; + pktLen := ohci_async_length(remaining, maxPkt, transfer^.PipeType = ptBulk); + + { Hardware advances the carry after every packet in a bulk TD. } + if transfer^.PipeType = ptBulk then toggle := OHCI_TD_DT_FROM_ED; td^.Control := ohci_make_td_control(dirPID, toggle, - OHCI_TD_DI_NONE, true); - td^.CBP := vtop(uint32(transfer^.Buffer) + offset); - if pktLen > 0 then + OHCI_TD_DI_NONE, transfer^.PipeType <> ptBulk); + if pktLen > 0 then begin + td^.CBP := vtop(uint32(transfer^.Buffer) + offset); td^.BE := vtop(uint32(transfer^.Buffer) + offset + uint32(pktLen) - 1) - else + end else begin + td^.CBP := 0; td^.BE := 0; + end; + td^.SWPad := pktLen; { Original data length } td^.NextTD := 0; td^.SWTransfer := Pointer(transfer); td^.SWNext := nil; @@ -916,13 +932,8 @@ begin else toggle := OHCI_TD_DT_DATA1; - if remaining > maxPkt then begin - offset := offset + maxPkt; - remaining := remaining - maxPkt; - end else begin - offset := offset + remaining; - remaining := 0; - end; + inc(offset, pktLen); + dec(remaining, pktLen); until remaining = 0; { Mark last TD with immediate interrupt } @@ -930,24 +941,28 @@ begin prevTD^.Control := (prevTD^.Control AND (NOT OHCI_TD_DI_MASK)) OR OHCI_TD_DI_IMMEDIATE; { Update endpoint toggle } - if toggle = OHCI_TD_DT_DATA1 then - transfer^.Endpoint^.Toggle := 1 - else - transfer^.Endpoint^.Toggle := 0; + if transfer^.PipeType <> ptBulk then begin + if toggle = OHCI_TD_DT_DATA1 then + transfer^.Endpoint^.Toggle := 1 + else + transfer^.Endpoint^.Toggle := 0; + end; { Allocate tail sentinel TD } tailTD := ohci_alloc_td; - if tailTD = nil then begin pop_trace; exit; end; + if tailTD = nil then begin ohci_free_td_chain(firstTD); pop_trace; exit; end; prevTD^.NextTD := vtop(uint32(tailTD)); prevTD^.SWNext := tailTD; { Create ED } ed := ohci_alloc_ed; - if ed = nil then begin pop_trace; exit; end; + if ed = nil then begin ohci_free_td_chain(firstTD); pop_trace; exit; end; ed^.Control := ohci_make_ed_control(devAddr, epNum, edDir, speedBit, maxPkt, false); ed^.HeadP := vtop(uint32(firstTD)); + if transfer^.PipeType = ptBulk then + ed^.HeadP := ed^.HeadP or ((uint32(transfer^.Endpoint^.Toggle) and 1) shl 1); ed^.TailP := vtop(uint32(tailTD)); ed^.NextED := 0; ed^.SWTransfer := Pointer(transfer); @@ -1078,6 +1093,22 @@ begin io.syslog.logln('driver.bus.usb.ohci', 'Hardware interrupts enabled.'); end; +{ Data bytes completed by one TD. SWPad excludes SETUP and status bytes. } +function ohci_td_data_length(td : POHCI_TD) : uint32; +var residual : uint32; cc : uint8; +begin + ohci_td_data_length := 0; + cc := ohci_td_cc(td); + if (td^.SWPad = 0) or ((cc <> OHCI_CC_NO_ERROR) and (cc <> OHCI_CC_DATA_UNDERRUN)) then exit; + residual := 0; + if td^.CBP <> 0 then begin + residual := (td^.BE AND $FFF) + 1; + if (td^.CBP AND $FFFFF000) <> (td^.BE AND $FFFFF000) then inc(residual, 4096); + residual := residual - (td^.CBP AND $FFF); + end; + if residual <= td^.SWPad then ohci_td_data_length := td^.SWPad - residual; +end; + procedure ohci_poll(hc : PUSBHCDriver); var priv : POHCI_PrivData; @@ -1133,31 +1164,40 @@ begin if (ed^.HeadP AND $FFFFFFF0) = (ed^.TailP AND $FFFFFFF0) then begin { All TDs consumed — walk chain via virtual SWNext to check CCs } transfer^.Status := tsSuccess; - transfer^.ActualLen := transfer^.BufferLen; + transfer^.ActualLen := 0; td := POHCI_TD(ed^.SWPad1); { Virtual firstTD } while td <> nil do begin cc := ohci_td_cc(td); + inc(transfer^.ActualLen, ohci_td_data_length(td)); if (cc <> OHCI_CC_NO_ERROR) and (cc <> OHCI_CC_NOT_ACCESSED) then begin transfer^.Status := ohci_cc_to_status(cc); break; end; td := td^.SWNext; end; + if transfer^.PipeType = ptBulk then + transfer^.Endpoint^.Toggle := (ed^.HeadP shr 1) and 1; + transfer^.HCPriv := nil; ohci_remove_ed(sentinel, ed); ohci_free_td_chain(POHCI_TD(ed^.SWPad1)); ohci_free_ed(ed); end else if (ed^.HeadP AND $01) <> 0 then begin { ED is halted — walk virtual chain to find the error CC } transfer^.Status := tsStall; + transfer^.ActualLen := 0; td := POHCI_TD(ed^.SWPad1); { Virtual firstTD } while td <> nil do begin cc := ohci_td_cc(td); + inc(transfer^.ActualLen, ohci_td_data_length(td)); if (cc <> OHCI_CC_NO_ERROR) and (cc <> OHCI_CC_NOT_ACCESSED) then begin transfer^.Status := ohci_cc_to_status(cc); break; end; td := td^.SWNext; end; + if transfer^.PipeType = ptBulk then + transfer^.Endpoint^.Toggle := (ed^.HeadP shr 1) and 1; + transfer^.HCPriv := nil; ohci_remove_ed(sentinel, ed); ohci_free_td_chain(POHCI_TD(ed^.SWPad1)); ohci_free_ed(ed); @@ -1413,6 +1453,12 @@ begin Assert((tdCtrl AND OHCI_TD_DP_MASK) = OHCI_TD_DP_OUT, 'td dp=OUT'); Assert((tdCtrl AND OHCI_TD_DT_MASK) = OHCI_TD_DT_FROM_ED, 'td dt=FROM_ED'); + Assert(ohci_async_length(4096,64,true)=4096, 'bulk page uses one TD, not 64'); + Assert(ohci_async_length(8192,64,true)=4096, 'bulk TD bounded to two pages'); + Assert(ohci_async_length(31,64,true)=31, 'bulk CBW retains short length'); + Assert(ohci_async_length(0,64,true)=0, 'bulk zero length packet'); + Assert(ohci_async_length(4096,64,false)=64, 'interrupt packet sizing unchanged'); + { === Condition Code Mapping === } Assert(ohci_cc_to_status(OHCI_CC_NO_ERROR) = tsSuccess, 'cc NO_ERROR->tsSuccess'); Assert(ohci_cc_to_status(OHCI_CC_CRC) = tsCRCError, 'cc CRC->tsCRCError'); @@ -1441,6 +1487,16 @@ begin Assert(td^.CBP = 0, 'alloc_td zeroed cbp'); Assert(td^.NextTD = 0, 'alloc_td zeroed nexttd'); Assert(td^.BE = 0, 'alloc_td zeroed be'); + td^.SWPad := 4096; + td^.Control := OHCI_CC_NO_ERROR shl OHCI_TD_CC_SHIFT; + Assert(ohci_td_data_length(td)=4096, 'multi-packet TD complete length'); + td^.Control := OHCI_CC_DATA_UNDERRUN shl OHCI_TD_CC_SHIFT; + td^.CBP := $1200; td^.BE := $1FFF; + Assert(ohci_td_data_length(td)=512, 'multi-packet TD short read length'); + td^.CBP := $1F00; td^.BE := $8EFF; + Assert(ohci_td_data_length(td)=0, 'two noncontiguous pages residual'); + td^.CBP := $8100; + Assert(ohci_td_data_length(td)=512, 'short read across physical page boundary'); ohci_free_td(td); { === HCCA Size / Alignment === } diff --git a/src/driver/bus/usb/driver.bus.usb.types.pas b/src/driver/bus/usb/driver.bus.usb.types.pas index 3d6a8441..984421b3 100644 --- a/src/driver/bus/usb/driver.bus.usb.types.pas +++ b/src/driver/bus/usb/driver.bus.usb.types.pas @@ -249,6 +249,8 @@ type Endpoints : array[0..USB_MAX_ENDPOINTS-1] of TUSBEndpoint; NumEndpoints : uint8; Configured : boolean; + InterfaceNumber : uint8; { First interface, alternate setting zero } + InterfaceCount : uint8; { Class drivers may reject composite devices } fnDisconnect : TUSBDisconnectCallback; { Called before device is freed on unplug } end; diff --git a/src/driver/storage/ctl/driver.storage.ctl.usb.pas b/src/driver/storage/ctl/driver.storage.ctl.usb.pas index b7d694f5..5c9654f9 100644 --- a/src/driver/storage/ctl/driver.storage.ctl.usb.pas +++ b/src/driver/storage/ctl/driver.storage.ctl.usb.pas @@ -1,92 +1,749 @@ -// Copyright 2021 Aaron Hance -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -{ - Driver->Bus->driver.bus.usb->USB_Storage - driver.bus.usb Mass Storage Class Driver (stub). - - Registers with driver.mgr as a driver.bus.usb class driver matching - mass storage devices (class $08, subclass $06 SCSI, protocol $50 BBB). - Currently a skeleton that recognises the device but performs no I/O — - full Bulk-Only Transport support is planned. - - @author(Aaron Hance ) -} +{ USB Mass Storage: SCSI transparent commands over Bulk-Only Transport. + LUN 0, one interface, READ/WRITE(10), 512..4096 byte sectors. + A persistent worker owns all storage I/O. No caller buffer is ever DMA-visible. + Protocol reference: https://www.usb.org/sites/default/files/usbmassbulk_10.pdf } unit driver.storage.ctl.usb; interface - -uses - boot.mgr, - driver.mgr, - memory.heap, - driver.storage.types, - io.syslog, - debug.tracer, - driver.bus.usb.core, - driver.bus.usb.types, - core.util, arch.x86.util; - procedure init; +procedure UnitTest; implementation -{ ========================= Constants ========================= } +uses boot.mgr, driver.mgr, memory.heap, driver.storage.types, + driver.storage.mgr, driver.bus.usb.core, driver.bus.usb.types, + proc.mgr, proc.types, arch.x86.bda, arch.x86.isr.tmr0, + core.util, arch.x86.util, io.syslog, io.stdio, core.strings, + driver.storage.vfs, driver.storage.vol.mgr, core.ds.lists; const - USB_CLASS_MASS_STORAGE = $08; - USB_SC_SCSI = $06; { SCSI transparent command set } - USB_PROTO_BBB = $50; { Bulk-Only (BBB) transport } + CBW_SIGNATURE = $43425355; + CSW_SIGNATURE = $53425355; + BUFFER_SIZE = 4096; + TRANSFER_TICKS = (PIT_INPUT_HZ div PIT_DIVISOR) * 5; -{ ========================= Class driver callback ========================= } +type + TCBW = packed record + Signature, Tag, DataLength : uint32; + Flags, Lun, CommandLength : uint8; + Command : array[0..15] of uint8; + end; + TCSW = packed record + Signature, Tag, Residue : uint32; + Status : uint8; + end; + PCSW = ^TCSW; + PMSDevice = ^TMSDevice; + TMSDevice = record + Next : PMSDevice; + Original : PUSBDevice; + USB : PUSBDevice; { Private snapshot survives physical disconnect. } + Disk : PStorage_Device; { Canonical manager record, retained as tombstone. } + InEP, OutEP : PUSBEndpoint; + ControlEP : TUSBEndpoint; + Transfer : TUSBTransfer; + Buffer : pointer; { Page aligned, private DMA scratch, also used for control. } + CBW : TCBW; + CSW : TCSW; + Request : PIORequest; + Tag : uint32; + Offline, Ready, InFlight, Attaching, VolumesDetached : boolean; + end; -{ Called by driver.mgr when a mass-storage interface is matched. - For now we just log it. } -function load(ptr : void) : boolean; +var + Devices : PMSDevice; + Worker : PProcessContext; + AttachBusy : boolean; + +function be32(p : puint8) : uint32; begin - push_trace('driver.storage.ctl.usb.load'); - io.syslog.logln('driver.bus.usb-Storage', 'Mass-storage device detected (stub - no I/O yet).'); - - { TODO: read Max LUN, set up Bulk-Only endpoints, register with - driver.storage.mgr via driver.storage.mgr.register_device. } - - load := true; - pop_trace; + be32 := (uint32(p[0]) shl 24) or (uint32(p[1]) shl 16) or + (uint32(p[2]) shl 8) or uint32(p[3]); end; -{ ========================= Init ========================= } +procedure put32(p : puint8; value : uint32); +begin + p[0] := value shr 24; p[1] := value shr 16; + p[2] := value shr 8; p[3] := value; +end; + +function valid_capacity(last, size : uint32) : boolean; +begin + valid_capacity := (last <> $FFFFFFFF) and (size >= 512) and + (size <= BUFFER_SIZE) and ((size and (size - 1)) = 0); +end; + +function valid_csw(csw : PCSW; tag, requested, actual : uint32) : boolean; +begin + valid_csw := (csw^.Signature = CSW_SIGNATURE) and (csw^.Tag = tag) and + (csw^.Status <= 2) and (csw^.Residue <= requested) and + (actual = sizeof(TCSW)); +end; + +function transfer_pending(c : PMSDevice) : boolean; +var s : TUSBTransferStatus; +begin + { Read status afresh: completion may have been delivered by an ISR. } + s := PUSBTransfer(@c^.Transfer)^.Status; + transfer_pending := c^.InFlight and ((s = tsInProgress) or (s = tsNotStarted)); +end; + +procedure poll_controller(c : PMSDevice); +var flags : uint32; +begin + { Poll and its descriptor cleanup must not be preempted by another poll. } + asm pushfd; cli; pop flags end; + if c^.USB^.HC^.fnPoll <> nil then c^.USB^.HC^.fnPoll(c^.USB^.HC); + if (flags and $200) <> 0 then asm sti end; +end; + +function run_transfer(c : PMSDevice) : TUSBTransferStatus; +var started, spins, flags : uint32; submitted : boolean; +begin + run_transfer := tsTimeout; + if c^.Offline then exit; + c^.Transfer.Status := tsNotStarted; + c^.Transfer.ActualLen := 0; + c^.Transfer.HCPriv := nil; + c^.InFlight := true; + { fnSubmit can configure xHCI endpoints and wait for command completion; + never call it under the storage manager's IRQ-disabled dispatch lock. } + asm pushfd; pop flags end; + if c^.USB^.HC^.HCType <> USB_HC_XHCI then asm cli end; + if c^.Offline then submitted := false + else submitted := c^.USB^.HC^.fnSubmit(c^.USB^.HC, @c^.Transfer); + if (flags and $200) <> 0 then asm sti end; + if not submitted then begin + c^.InFlight := false; + exit; + end; + started := puint32(@Counters.c32)^; + spins := 0; + while transfer_pending(c) do begin + poll_controller(c); + if not transfer_pending(c) then break; + inc(spins); + if (c^.Original = nil) or + (puint32(@Counters.c32)^ - started >= TRANSFER_TICKS) or + (spins >= 10000000) then begin + { No HC cancellation contract exists yet. Fail closed and retain the + private transfer/buffers until completion; NEVER free live DMA. } + c^.Offline := true; + if c^.Disk <> nil then c^.Disk^.removed := true; + io.syslog.logln('USBMSC', 'Transfer timed out/disconnected; device offline, DMA quarantined.'); + exit; + end; + asm pushfd; pop flags end; + if (flags and $200) <> 0 then asm hlt end; + end; + c^.InFlight := false; + run_transfer := c^.Transfer.Status; +end; + +function bulk(c : PMSDevice; ep : PUSBEndpoint; buffer : pointer; + length : uint32; var actual : uint32) : TUSBTransferStatus; +var toggle, packets : uint32; status : TUSBTransferStatus; +begin + toggle := ep^.Toggle; + c^.Transfer.Device := c^.USB; + c^.Transfer.Endpoint := ep; + c^.Transfer.Direction := ep^.Direction; + c^.Transfer.PipeType := ptBulk; + c^.Transfer.Buffer := buffer; + c^.Transfer.BufferLen := length; + status := run_transfer(c); + actual := c^.Transfer.ActualLen; + if (status = tsSuccess) and (ep^.MaxPacket > 0) then begin + packets := actual div ep^.MaxPacket; + if ((actual mod ep^.MaxPacket) <> 0) or (actual < length) or (length = 0) then inc(packets); + ep^.Toggle := toggle xor (packets and 1); + end; + bulk := status; +end; + +function control(c : PMSDevice; kind, request : uint8; index, length : uint16) : TUSBTransferStatus; +begin + c^.Transfer.Device := c^.USB; + c^.Transfer.Endpoint := @c^.ControlEP; + c^.Transfer.Direction := dirSetup; + c^.Transfer.PipeType := ptControl; + c^.Transfer.Buffer := c^.Buffer; + c^.Transfer.BufferLen := length; + c^.Transfer.Setup := make_setup_packet(kind, request, 0, index, length); + control := run_transfer(c); +end; + +function clear_halt(c : PMSDevice; ep : PUSBEndpoint) : boolean; +var address : uint16; +begin + address := ep^.Address; + if ep^.Direction = dirIn then address := address or $80; + clear_halt := control(c, $02, USB_REQ_CLEAR_FEATURE, address, 0) = tsSuccess; + if clear_halt then ep^.Toggle := 0; + { xHCI also needs Reset Endpoint/Set TR Dequeue after a halted ring. + Until the HC exposes that operation, fail closed instead of reusing it. } + if c^.USB^.HC^.HCType = USB_HC_XHCI then clear_halt := false; +end; + +procedure recover(c : PMSDevice); +var ok : boolean; +begin + if c^.Offline then exit; + ok := control(c, $21, $FF, c^.USB^.InterfaceNumber, 0) = tsSuccess; + if ok then ok := clear_halt(c, c^.InEP); + if ok then ok := clear_halt(c, c^.OutEP); + if not ok then c^.Offline := true; +end; + +procedure command(c : PMSDevice; opcode, length : uint8; bytes : uint32; input : boolean); +begin + memset(uint32(@c^.CBW), 0, sizeof(TCBW)); + inc(c^.Tag); + c^.CBW.Signature := CBW_SIGNATURE; + c^.CBW.Tag := c^.Tag; + c^.CBW.DataLength := bytes; + if input then c^.CBW.Flags := $80; + c^.CBW.CommandLength := length; + c^.CBW.Command[0] := opcode; +end; + +{ 0=passed, 1=SCSI command failed (request sense), 2=transport failure. + Only one CBW/data/CSW sequence may be in flight on an interface. } +function transport(c : PMSDevice; var transferred : uint32) : uint8; +var status : TUSBTransferStatus; + actual, offset, count : uint32; + ep : PUSBEndpoint; +begin + transport := 2; transferred := 0; + if c^.CBW.DataLength > BUFFER_SIZE then exit; + status := bulk(c, c^.OutEP, @c^.CBW, sizeof(TCBW), actual); + if (status <> tsSuccess) or (actual <> sizeof(TCBW)) then begin recover(c); exit; end; + if c^.CBW.Flags = $80 then ep := c^.InEP else ep := c^.OutEP; + offset := 0; + while offset < c^.CBW.DataLength do begin + count := c^.CBW.DataLength - offset; + { One bounded data transfer per command, at most one page of DMA. } + status := bulk(c, ep, pointer(uint32(c^.Buffer) + offset), count, actual); + if status = tsStall then begin + if not clear_halt(c, ep) then begin recover(c); exit; end; + break; + end; + if (status <> tsSuccess) or (actual > count) then begin recover(c); exit; end; + inc(offset, actual); + if actual < count then break; + end; + transferred := offset; + memset(uint32(@c^.CSW), 0, sizeof(TCSW)); + status := bulk(c, c^.InEP, @c^.CSW, sizeof(TCSW), actual); + if status = tsStall then begin + if clear_halt(c, c^.InEP) then + status := bulk(c, c^.InEP, @c^.CSW, sizeof(TCSW), actual); + end; + if (status <> tsSuccess) or not valid_csw(@c^.CSW, c^.CBW.Tag, c^.CBW.DataLength, actual) then begin + recover(c); exit; + end; + if c^.CSW.Status = 2 then begin recover(c); exit; end; + if c^.CSW.Status = 1 then begin transport := 1; exit; end; + if (c^.CSW.Residue <> c^.CBW.DataLength - offset) then begin recover(c); exit; end; + transport := 0; +end; + +function sense(c : PMSDevice) : TError; +var actual : uint32; +begin + sense := eIOError; + command(c, $03, 6, 18, true); + c^.CBW.Command[4] := 18; + memset(uint32(c^.Buffer), 0, 18); + if transport(c, actual) <> 0 then exit; + if actual < 14 then exit; + if (puint8(c^.Buffer)[0] and $7E) <> $70 then exit; + case puint8(c^.Buffer)[2] and $0F of + 2: sense := eDeviceNotReady; + 3: sense := eBadSector; + 5: sense := eNotSupported; + 6: sense := eDeviceNotReady; + 7: sense := eReadOnly; + end; +end; + +function execute(c : PMSDevice; required : uint32) : TError; +var actual : uint32; resultCode : uint8; +begin + resultCode := transport(c, actual); + execute := eIOError; + if resultCode = 1 then execute := sense(c) + else if (resultCode = 0) and (actual = required) then execute := eNone; + if c^.Offline then execute := eIOTimeout; + if c^.Original = nil then execute := eDeviceRemoved; +end; + +function probe(c : PMSDevice; var sectors, sectorSize : uint32) : boolean; +var status : TUSBTransferStatus; attempt : uint32; error : TError; +begin + probe := false; + puint8(c^.Buffer)^ := 0; + status := control(c, $A1, $FE, c^.USB^.InterfaceNumber, 1); + { A STALL is explicitly allowed for a single-LUN device. } + if (status <> tsSuccess) and (status <> tsStall) then exit; + if status = tsSuccess then begin + if (c^.Transfer.ActualLen <> 1) or (puint8(c^.Buffer)^ > 15) then exit; + if puint8(c^.Buffer)^ <> 0 then + io.syslog.logln('USBMSC', 'Multiple LUNs reported; exposing LUN 0 only.'); + end; + command(c, $12, 6, 36, true); + c^.CBW.Command[4] := 36; + if execute(c, 36) <> eNone then exit; + if (puint8(c^.Buffer)[0] and $1F) <> 0 then exit; { Direct-access disks only. } + error := eDeviceNotReady; + for attempt := 1 to 5 do begin + command(c, $00, 6, 0, false); + error := execute(c, 0); + if error <> eDeviceNotReady then break; + psleep((PIT_INPUT_HZ div PIT_DIVISOR) div 5); + end; + if error <> eNone then exit; + command(c, $25, 10, 8, true); + if execute(c, 8) <> eNone then exit; + sectors := be32(puint8(c^.Buffer)); + sectorSize := be32(puint8(uint32(c^.Buffer) + 4)); + if not valid_capacity(sectors, sectorSize) then exit; + inc(sectors); + probe := true; +end; + +function valid_request(d : PStorage_Device; r : PIORequest) : boolean; +begin + valid_request := false; + if (r^.Buffer = nil) or (r^.SectorCount = 0) or (d^.sectorSize = 0) then exit; + if r^.LBA >= d^.maxSectorCount then exit; + if r^.SectorCount > d^.maxSectorCount - r^.LBA then exit; + if r^.SectorCount > $FFFFFFFF div d^.sectorSize then exit; + if uint32(r^.Buffer) > $FFFFFFFF - r^.SectorCount * d^.sectorSize then exit; + valid_request := true; +end; + +procedure dispatch(d : PStorage_Device; r : PIORequest); +var c : PMSDevice; +begin + { Called with interrupts masked: publish work only, no USB calls/waits. } + if d^.removed or (d^.controllerId0 = 0) then begin + driver.storage.mgr.complete_io(r, false, eDeviceRemoved); exit; + end; + c := PMSDevice(d^.controllerId0); + if c^.Offline or (c^.Original = nil) then begin + driver.storage.mgr.complete_io(r, false, eDeviceRemoved); exit; + end; + if not valid_request(d, r) then begin + driver.storage.mgr.complete_io(r, false, eInvalidArgument); exit; + end; + if (r^.RequestType = ioWrite) and not d^.writable then begin + driver.storage.mgr.complete_io(r, false, eReadOnly); exit; + end; + if c^.Request <> nil then begin + driver.storage.mgr.complete_io(r, false, eQueueFull); exit; + end; + c^.Disk := d; + c^.Request := r; +end; + +procedure process_request(c : PMSDevice); +var r : PIORequest; offset, blocks, bytes, doneBlocks : uint32; + error : TError; +begin + r := c^.Request; + if r = nil then exit; + error := eNone; doneBlocks := 0; offset := 0; + if c^.Offline or (c^.Original = nil) then error := eDeviceRemoved; + while (doneBlocks < r^.SectorCount) and (error = eNone) do begin + blocks := r^.SectorCount - doneBlocks; + if blocks > BUFFER_SIZE div c^.Disk^.sectorSize then + blocks := BUFFER_SIZE div c^.Disk^.sectorSize; + bytes := blocks * c^.Disk^.sectorSize; + if r^.RequestType = ioRead then command(c, $28, 10, bytes, true) + else begin + command(c, $2A, 10, bytes, false); + memcpy(uint32(r^.Buffer) + offset, uint32(c^.Buffer), bytes); + end; + put32(@c^.CBW.Command[2], r^.LBA + doneBlocks); + c^.CBW.Command[7] := blocks shr 8; + c^.CBW.Command[8] := blocks; + error := execute(c, bytes); + if error = eNone then begin + if r^.RequestType = ioRead then + memcpy(uint32(c^.Buffer), uint32(r^.Buffer) + offset, bytes); + inc(doneBlocks, blocks); inc(offset, bytes); + end; + end; + { Complete writes only after the device has committed its cache. } + if (error = eNone) and (r^.RequestType = ioWrite) then begin + command(c, $35, 10, 0, false); + error := execute(c, 0); + end; + if error = eReadOnly then c^.Disk^.writable := false; + r^.ByteCount := offset; + c^.Request := nil; + driver.storage.mgr.complete_io(r, error = eNone, error); +end; + +procedure disconnect(dev : PUSBDevice); +var c : PMSDevice; flags : uint32; +begin + asm pushfd; cli; pop flags end; + c := Devices; + while c <> nil do begin + if c^.Original = dev then begin + c^.Original := nil; + c^.Offline := true; + if c^.Disk <> nil then c^.Disk^.removed := true; + break; + end; + c := c^.Next; + end; + if (flags and $200) <> 0 then asm sti end; + io.syslog.logln('USBMSC', 'Disk disconnected; further I/O rejected.'); +end; + +function detach_volumes(c : PMSDevice) : boolean; +var i : uint32; v : PStorage_Volume; +begin + detach_volumes := true; + if c^.Disk = nil then exit; + if c^.Disk^.metadataBusy or (c^.Disk^.activeCount <> 0) then begin + detach_volumes := false; exit; + end; + if c^.Disk^.volumes = nil then exit; + i := 0; + while i < DL_Size(c^.Disk^.volumes) do begin + v := PStorage_Volume(puint32(DL_Get(c^.Disk^.volumes, i))^); + if (v <> nil) and ((v^.lifecycleFlags and STORAGE_VOLUME_FLAG_MOUNTED) <> 0) then begin + if driver.storage.vol.mgr.volume_is_busy(v) then detach_volumes := false + else if not driver.storage.vfs.InvalidateVolume(v) then detach_volumes := false; + end; + inc(i); + end; +end; + +procedure worker_loop(ctx : PProcessContext); +var c, next, prev, scan : PMSDevice; flags : uint32; +begin + while true do begin + c := Devices; + while c <> nil do begin + next := c^.Next; + if c^.Ready then process_request(c); + if c^.Offline and c^.InFlight and not c^.Attaching then begin + poll_controller(c); + if not transfer_pending(c) then c^.InFlight := false; + end; + if c^.Offline and not c^.InFlight and (c^.Request = nil) and + c^.Ready and not c^.Attaching then begin + if c^.Disk <> nil then c^.Disk^.removed := true; + if not c^.VolumesDetached then c^.VolumesDetached := detach_volumes(c); + if c^.Buffer <> nil then begin + kfree_aligned(c^.Buffer); c^.Buffer := nil; + end; + if (c^.Original = nil) and c^.VolumesDetached then begin + { Keep the manager's removed-device/volume tombstones stable; + release every class-driver allocation once DMA is finished. } + asm pushfd; cli; pop flags end; + prev := nil; scan := Devices; + while (scan <> nil) and (scan <> c) do begin prev := scan; scan := scan^.Next; end; + if prev = nil then Devices := c^.Next else prev^.Next := c^.Next; + if c^.Disk <> nil then c^.Disk^.controllerId0 := 0; + if (flags and $200) <> 0 then asm sti end; + kfree(void(c^.USB)); kfree_aligned(c); + end; + end; + c := next; + end; + proc.mgr.proc_sleep_ms(1); + end; +end; + +procedure attach_worker(ctx : PProcessContext); +var c : PMSDevice; d : PStorage_Device; sectors, sectorSize, i, flags : uint32; + claimed : boolean; +begin + c := PMSDevice(ctx^.Local); + { xHCI command submission and manager registration are not reentrant. } + repeat + asm pushfd; cli; pop flags end; + claimed := not AttachBusy; + if claimed then AttachBusy := true; + if (flags and $200) <> 0 then asm sti end; + if not claimed then proc.mgr.proc_sleep_ms(1); + until claimed; + d := PStorage_Device(kalloc(sizeof(TStorage_Device))); + if d = nil then begin + c^.Offline := true; c^.Ready := true; c^.Attaching := false; AttachBusy := false; exit; + end; + if not probe(c, sectors, sectorSize) then begin + c^.Offline := true; c^.Ready := true; + kfree(void(d)); + c^.Attaching := false; AttachBusy := false; + io.syslog.logln('USBMSC', 'Probe failed or unsupported disk geometry.'); + exit; + end; + memset(uint32(d), 0, sizeof(TStorage_Device)); + d^.volumes := DL_New(sizeof(pointer)); + if d^.volumes = nil then begin + kfree(void(d)); c^.Offline := true; c^.Ready := true; c^.Attaching := false; AttachBusy := false; exit; + end; + d^.controller := ControllerUSB; + d^.controllerId0 := uint32(c); + d^.writable := true; + d^.sectorSize := sectorSize; + d^.maxSectorCount := sectors; + d^.maxActive := 1; + d^.mergeMaxSectors := BUFFER_SIZE div sectorSize; + d^.dispatchRead := @dispatch; d^.dispatchWrite := @dispatch; + c^.Ready := true; + driver.storage.mgr.register_device(d); + { Manager copies the record; never keep the temporary descriptor. } + i := 0; + while i < driver.storage.mgr.get_device_count do begin + if driver.storage.mgr.get_device(i)^.controllerId0 = uint32(c) then begin + c^.Disk := driver.storage.mgr.get_device(i); break; + end; + inc(i); + end; + kfree(void(d)); + c^.Attaching := false; AttachBusy := false; + io.syslog.logln('USBMSC', 'LUN 0 registered with storage manager.'); + +end; + +function load(ptr : void) : boolean; +var dev : PUSBDevice; c : PMSDevice; + i, flags : uint32; +begin + load := false; + dev := PUSBDevice(ptr); + if (dev = nil) or (Worker = nil) then exit; + if (dev^.HC = nil) or (dev^.HC^.fnSubmit = nil) then exit; + { Core currently binds the first interface and flattens alternate settings. + Reject ambiguous endpoint sets rather than talking to the wrong pipe. } + if (dev^.InterfaceCount <> 1) or (dev^.NumEndpoints <> 2) or + (dev^.Speed = USB_SPEED_LOW) then exit; + if dev^.HC^.HCType = USB_HC_UHCI then begin + io.syslog.logln('USBMSC', 'UHCI storage requires HC queue/cleanup support.'); exit; + end; + c := PMSDevice(kalloc_aligned(sizeof(TMSDevice), 4096)); + if c = nil then exit; + memset(uint32(c), 0, sizeof(TMSDevice)); + c^.USB := PUSBDevice(kalloc(sizeof(TUSBDevice))); + c^.Buffer := kalloc_aligned(BUFFER_SIZE, 4096); + if (c^.USB = nil) or (c^.Buffer = nil) then begin + if c^.USB <> nil then kfree(void(c^.USB)); + kfree_aligned(c^.Buffer); kfree_aligned(c); exit; + end; + memcpy(uint32(dev), uint32(c^.USB), sizeof(TUSBDevice)); + c^.Original := dev; + c^.Attaching := true; + for i := 0 to 1 do begin + if (c^.USB^.Endpoints[i].PipeType = ptBulk) and + (c^.USB^.Endpoints[i].Address <> 0) and + (c^.USB^.Endpoints[i].MaxPacket > 0) and + (c^.USB^.Endpoints[i].MaxPacket <= 1024) then begin + if c^.USB^.Endpoints[i].Direction = dirIn then c^.InEP := @c^.USB^.Endpoints[i] + else c^.OutEP := @c^.USB^.Endpoints[i]; + end; + end; + if (c^.InEP = nil) or (c^.OutEP = nil) then begin + kfree_aligned(c^.Buffer); kfree(void(c^.USB)); kfree_aligned(c); exit; + end; + c^.ControlEP.PipeType := ptControl; + c^.ControlEP.Direction := dirSetup; + c^.ControlEP.MaxPacket := dev^.MaxPacket0; + { Publish before probing so disconnect can invalidate the private context. } + asm pushfd; cli; pop flags end; + c^.Next := Devices; Devices := c; + dev^.fnDisconnect := @disconnect; + if (flags and $200) <> 0 then asm sti end; + { The scheduler starts only after boot.mgr.run. Probe and register from + a task so partition discovery can wait for the persistent I/O worker. } + if proc.mgr.create('usb.attach', @attach_worker, void(c), 1) = nil then begin + c^.Offline := true; c^.Ready := true; c^.Attaching := false; exit; + end; + load := true; +end; + +{ Memory-only host controller used by UnitTest; never touches a real disk. } +type + PMock = ^TMock; + TMock = record + HC : TUSBHCDriver; + USB : TUSBDevice; + Disk : TStorage_Device; + Request : TIORequest; + C : TMSDevice; + Stage, Fault, Controls, DataBytes : uint32; + GoodWrite, GoodRecovery : boolean; + end; + +function mock_submit(hc : PUSBHCDriver; t : PUSBTransfer) : boolean; +var m : PMock; csw : PCSW; i : uint32; +begin + m := PMock(hc^.PrivData); + mock_submit := true; + t^.Status := tsSuccess; t^.ActualLen := t^.BufferLen; + if t^.PipeType = ptControl then begin + if t^.Setup.bRequest = $FE then begin puint8(t^.Buffer)^ := 0; exit; end; + case m^.Controls of + 0: m^.GoodRecovery := (t^.Setup.bRequest = $FF) and (t^.Setup.wIndex = 3); + 1: m^.GoodRecovery := m^.GoodRecovery and (t^.Setup.bRequest = 1) and (t^.Setup.wIndex = $81); + 2: m^.GoodRecovery := m^.GoodRecovery and (t^.Setup.bRequest = 1) and (t^.Setup.wIndex = 2); + end; + inc(m^.Controls); + exit; + end; + case m^.Stage of + 0: begin + m^.Stage := 2; + m^.DataBytes := 0; + if m^.C.CBW.DataLength <> 0 then m^.Stage := 1; + if m^.Fault = 6 then begin t^.ActualLen := 30; m^.Stage := 0; end; + end; + 1: begin + if m^.C.CBW.Flags = $80 then begin + memset(uint32(t^.Buffer), $A5, t^.BufferLen); + case m^.C.CBW.Command[0] of + $12: puint8(t^.Buffer)[0] := 0; + $25: begin put32(puint8(t^.Buffer), 8191); put32(puint8(uint32(t^.Buffer)+4), 512); end; + $03: begin memset(uint32(t^.Buffer),0,t^.BufferLen); puint8(t^.Buffer)[0]:=$70; puint8(t^.Buffer)[2]:=7; end; + end; + end else begin + m^.GoodWrite := true; + if t^.BufferLen > 0 then for i := 0 to t^.BufferLen-1 do + if puint8(t^.Buffer)[i] <> $5A then m^.GoodWrite := false; + end; + if m^.Fault = 4 then t^.ActualLen := t^.BufferLen div 2; + m^.DataBytes := t^.ActualLen; + m^.Stage := 2; + end; + 2: begin + if m^.Fault = 5 then begin m^.Fault := 0; t^.Status := tsStall; t^.ActualLen := 0; exit; end; + csw := PCSW(t^.Buffer); + csw^.Signature := CSW_SIGNATURE; csw^.Tag := m^.C.CBW.Tag; + csw^.Residue := m^.C.CBW.DataLength - m^.DataBytes; + csw^.Status := 0; + case m^.Fault of + 1: inc(csw^.Tag); + 2: csw^.Status := 2; + 3: begin csw^.Status := 1; m^.Fault := 0; end; + 7: inc(csw^.Residue); + 8: t^.ActualLen := 12; + end; + m^.Stage := 0; + end; + end; +end; + +procedure UnitTest; +var passed, failed, actual, sectors, size : uint32; csw : TCSW; bytes : array[0..3] of uint8; m : PMock; + procedure Assert(ok : boolean; name : pchar); + begin + if ok then inc(passed) else begin + inc(failed); io.syslog.logln('USBMSC FAIL', name); + end; + end; +begin + passed := 0; failed := 0; + Assert(sizeof(TCBW) = 31, 'CBW wire size'); + Assert(sizeof(TCSW) = 13, 'CSW wire size'); + put32(@bytes[0], $12345678); + Assert((bytes[0] = $12) and (bytes[3] = $78), 'SCSI byte order'); + Assert(be32(@bytes[0]) = $12345678, 'capacity byte order'); + Assert(valid_capacity(0, 512), 'single sector'); + Assert(valid_capacity($FFFFFFFE, 4096), 'largest representable sector count'); + Assert(not valid_capacity($FFFFFFFF, 512), 'reject READ CAPACITY(16) sentinel'); + Assert(not valid_capacity(1, 0), 'reject zero block size'); + Assert(not valid_capacity(1, 513), 'reject non-power-of-two block size'); + Assert(not valid_capacity(1, 8192), 'reject oversized block size'); + csw.Signature := CSW_SIGNATURE; csw.Tag := 7; csw.Residue := 0; csw.Status := 0; + Assert(valid_csw(@csw, 7, 512, 13), 'valid CSW'); + Assert(not valid_csw(@csw, 8, 512, 13), 'reject stale tag'); + Assert(not valid_csw(@csw, 7, 512, 12), 'reject short CSW'); + csw.Residue := 513; + Assert(not valid_csw(@csw, 7, 512, 13), 'reject excessive residue'); + csw.Residue := 0; csw.Status := 3; + Assert(not valid_csw(@csw, 7, 512, 13), 'reject reserved status'); + csw.Status := 0; csw.Signature := 0; + Assert(not valid_csw(@csw, 7, 512, 13), 'reject signature'); + m := PMock(kalloc(sizeof(TMock))); + Assert(m <> nil, 'mock allocation'); + if m <> nil then begin + memset(uint32(m), 0, sizeof(TMock)); + m^.HC.fnSubmit := @mock_submit; m^.HC.HCType := USB_HC_EHCI; m^.HC.PrivData := m; + m^.USB.HC := @m^.HC; m^.USB.InterfaceNumber := 3; + m^.USB.Endpoints[0].Address := 1; m^.USB.Endpoints[0].Direction := dirIn; + m^.USB.Endpoints[1].Address := 2; m^.USB.Endpoints[1].Direction := dirOut; + m^.USB.Endpoints[0].MaxPacket := 512; m^.USB.Endpoints[1].MaxPacket := 512; + m^.C.USB := @m^.USB; m^.C.Original := @m^.USB; + m^.C.InEP := @m^.USB.Endpoints[0]; m^.C.OutEP := @m^.USB.Endpoints[1]; + m^.C.Buffer := kalloc_aligned(BUFFER_SIZE, 4096); + Assert(m^.C.Buffer <> nil, 'mock DMA scratch allocation'); + if m^.C.Buffer <> nil then begin + Assert(probe(@m^.C, sectors, size) and (sectors = 8192) and (size = 512), 'probe SCSI disk'); + command(@m^.C, $28, 10, 512, true); + Assert((execute(@m^.C,512) = eNone) and (puint8(m^.C.Buffer)[511] = $A5), 'read data phase'); + command(@m^.C, $2A, 10, 512, false); memset(uint32(m^.C.Buffer),$5A,512); + Assert((execute(@m^.C,512) = eNone) and m^.GoodWrite, 'write data phase'); + m^.Fault := 3; command(@m^.C, $2A, 10, 512, false); + Assert(execute(@m^.C,512) = eReadOnly, 'command failure requests sense'); + m^.Fault := 4; command(@m^.C, $28, 10, 512, true); + Assert(execute(@m^.C,512) = eIOError, 'short read is not full success'); + m^.Fault := 5; command(@m^.C, $28, 10, 512, true); + Assert(execute(@m^.C,512) = eNone, 'CSW stall cleared and retried'); + m^.Fault := 1; m^.Controls := 0; command(@m^.C, $28, 10, 512, true); + Assert((transport(@m^.C,actual) = 2) and (m^.Controls = 3) and m^.GoodRecovery, 'bad tag triggers ordered reset recovery'); + m^.Fault := 2; m^.Controls := 0; command(@m^.C, $28, 10, 512, true); + Assert((transport(@m^.C,actual) = 2) and m^.GoodRecovery, 'phase error reset recovery'); + m^.Fault := 6; m^.Controls := 0; command(@m^.C, $28, 10, 512, true); + Assert((transport(@m^.C,actual) = 2) and m^.GoodRecovery, 'short CBW reset recovery'); + m^.Fault := 7; command(@m^.C, $28, 10, 512, true); + Assert(transport(@m^.C,actual) = 2, 'inconsistent CSW residue rejected'); + m^.Fault := 8; command(@m^.C, $28, 10, 512, true); + Assert(transport(@m^.C,actual) = 2, 'short CSW rejected'); + m^.Fault := 0; command(@m^.C, $35, 10, 0, false); + Assert(execute(@m^.C,0) = eNone, 'cache synchronization without data stage'); + m^.Disk.sectorSize := 512; m^.Disk.maxSectorCount := 8192; + m^.Request.Buffer := m^.C.Buffer; m^.Request.LBA := 8191; m^.Request.SectorCount := 1; + Assert(valid_request(@m^.Disk,@m^.Request), 'last sector valid'); + m^.Request.SectorCount := 2; + Assert(not valid_request(@m^.Disk,@m^.Request), 'cross-end request rejected'); + m^.Request.SectorCount := 0; + Assert(not valid_request(@m^.Disk,@m^.Request), 'zero-sector request rejected'); + m^.Request.LBA := $FFFFFFFE; m^.Request.SectorCount := 4; + Assert(not valid_request(@m^.Disk,@m^.Request), 'wrapped LBA rejected'); + m^.Request.LBA := 0; m^.Request.SectorCount := 1; m^.Request.Buffer := pointer($FFFFFF00); + Assert(not valid_request(@m^.Disk,@m^.Request), 'wrapped buffer rejected'); + kfree_aligned(m^.C.Buffer); + end; + kfree(void(m)); + end; + io.syslog.log('USBMSC', 'Tests passed: '); io.syslog.writeint(passed); + io.syslog.writestring(' failed: '); io.syslog.writeintln(failed); +end; + +procedure test_command(params : PParamList; stdin_buf, stdout_buf, stderr_buf : POutBuf); +begin + UnitTest; + io.stdio.bufWriteStrLn(stdout_buf, 'USB mass storage protocol tests written to serial log.'); +end; procedure init; -var - MSID : TDeviceIdentifier; +var id : TDeviceIdentifier; begin - push_trace('driver.storage.ctl.usb.init'); - io.syslog.logln('driver.bus.usb-Storage', 'INIT BEGIN.'); - - MSID.Bus := biUSB; - MSID.id0 := idANY; { Any VID:PID } - MSID.id1 := $FFFFFFFF; { Any device class } - MSID.id2 := USB_CLASS_MASS_STORAGE;{ bInterfaceClass = $08 } - MSID.id3 := USB_SC_SCSI; { bInterfaceSubClass = $06 } - MSID.id4 := USB_PROTO_BBB; { bInterfaceProtocol = $50 } - MSID.ex := nil; - - driver.mgr.register_driver('driver.bus.usb Storage Driver', @MSID, @load); - - io.syslog.logln('driver.bus.usb-Storage', 'INIT END.'); - pop_trace; + io.stdio.registerCommand('USBMSCTEST', @test_command, 'Test USB mass storage protocol without disk I/O'); + Worker := proc.mgr.create('usb.storage', @worker_loop, nil, 1); + if Worker = nil then exit; + id.Bus := biUSB; id.id0 := idANY; id.id1 := $FFFFFFFF; + id.id2 := $08; id.id3 := $06; id.id4 := $50; id.ex := nil; + driver.mgr.register_driver('USB Mass Storage', @id, @load); end; initialization boot.mgr.registerBoot('driver.storage.ctl.usb', @init, 'USB Mass Storage Driver', BOOT_MGR_BARRIER_DEVICE); - end. diff --git a/src/test/test.pas b/src/test/test.pas index ea33ad07..520b7402 100644 --- a/src/test/test.pas +++ b/src/test/test.pas @@ -37,6 +37,7 @@ uses driver.storage.metatest, driver.storage.perftest, driver.storage.ctl.ahci, + driver.storage.ctl.usb, core.enc.fnv1a, core.enc.djb2, core.ds.bloom, @@ -76,6 +77,7 @@ begin driver.storage.metatest.UnitTest; driver.storage.perftest.UnitTest; driver.storage.ctl.ahci.UnitTest; + driver.storage.ctl.usb.UnitTest; core.enc.fnv1a.UnitTest; core.enc.djb2.UnitTest; core.ds.bloom.UnitTest; -- 2.54.0 From d318eca26206ec9540efd53bddf98b76e7923855 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 31 Oct 2026 20:46:48 +0000 Subject: [PATCH 16/21] Refactor VFS shared operations and remove unused legacy storage wrappers --- doc/src/driver/storage/driver.storage.mgr.md | 11 +- src/driver/storage/driver.storage.mgr.pas | 29 +- src/driver/storage/driver.storage.vfs.pas | 403 +++++++------------ 3 files changed, 151 insertions(+), 292 deletions(-) diff --git a/doc/src/driver/storage/driver.storage.mgr.md b/doc/src/driver/storage/driver.storage.mgr.md index f92a72d6..c4b42d91 100644 --- a/doc/src/driver/storage/driver.storage.mgr.md +++ b/doc/src/driver/storage/driver.storage.mgr.md @@ -14,7 +14,7 @@ The I/O model operates as follows: - `storage_read` / `storage_write` are convenience wrappers that build a stack-local `TIORequest` and call `submit_io_wait`. - `storage_read_async` / `storage_write_async` build and submit a request with a caller-supplied callback, returning immediately. -Legacy `storage_read_legacy` / `storage_write_legacy` procedure wrappers are retained for callers not yet migrated to the function-returning API. +Blocking callers use `storage_read` / `storage_write` and check the returned `TError`. ## Dependencies @@ -205,15 +205,6 @@ function storage_write_async(device : PStorage_Device; addr : uint32; sectors : Non-blocking write. Analogous to `storage_read_async`. -### storage_read_legacy / storage_write_legacy - -```pascal -procedure storage_read_legacy(device : PStorage_Device; addr : uint32; sectors : uint32; buffer : puint32); -procedure storage_write_legacy(device : PStorage_Device; addr : uint32; sectors : uint32; buffer : puint32); -``` - -Drop-in procedure replacements for callers using the old void-returning API. Forward to `storage_read` / `storage_write` and discard the error code. Marked for removal once all callers are migrated. - ## Notes - `dispatch_next` is an internal procedure called with interrupts disabled. It dequeues the head of the CFIFO, allocates a heap copy via `ioreq_copy` (for ISR safety), and invokes the appropriate `dispatchRead` or `dispatchWrite` on the device. If neither dispatch pointer is set, the request is failed with `eDeviceNotFound`. diff --git a/src/driver/storage/driver.storage.mgr.pas b/src/driver/storage/driver.storage.mgr.pas index 588e7d8d..5c4688b7 100644 --- a/src/driver/storage/driver.storage.mgr.pas +++ b/src/driver/storage/driver.storage.mgr.pas @@ -19,14 +19,14 @@ and provides the submit_io / complete_io request-based I/O path. Device drivers call register_device() when they discover a new device. - Phase 2 I/O model: + Request-based I/O model: - submit_io enqueues a TIORequest into the device's per-device CFIFO, - dispatches to hardware, and sleeps the calling task (proc_await). - - complete_io is called from ISR/completion context, wakes the caller. + dispatches to hardware, and returns without waiting for completion. + - complete_io delivers callbacks from the driver's completion context. - storage_read / storage_write are thin wrappers that build a stack-local - TIORequest, call submit_io, and return the resulting TError. - - Legacy sync/async callbacks are still supported as fallback for drivers - not yet migrated to the TDriverDispatch interface. + TIORequest, call submit_io_wait, and return the resulting TError. + - Drivers must provide the TDriverDispatch interface; there is no legacy + callback fallback. @author(Aaron Hance ) } @@ -112,12 +112,6 @@ function storage_read_async(device : PStorage_Device; addr : uint32; sectors : u function storage_write_async(device : PStorage_Device; addr : uint32; sectors : uint32; buffer : puint32; callback : TIOCallback; callbackData : pointer) : TError; -{ Legacy wrappers — procedure signatures matching the old API. - Forward to the new functions, silently discard errors. - TODO: migrate all callers and remove these. } -procedure storage_read_legacy(device : PStorage_Device; addr : uint32; sectors : uint32; buffer : puint32); -procedure storage_write_legacy(device : PStorage_Device; addr : uint32; sectors : uint32; buffer : puint32); - implementation uses @@ -744,17 +738,6 @@ begin storage_write_async := submit_io(@req); end; -{ Legacy procedure wrappers — drop-in replacements for old callers } -procedure storage_read_legacy(device : PStorage_Device; addr : uint32; sectors : uint32; buffer : puint32); -begin - storage_read(device, addr, sectors, buffer); -end; - -procedure storage_write_legacy(device : PStorage_Device; addr : uint32; sectors : uint32; buffer : puint32); -begin - storage_write(device, addr, sectors, buffer); -end; - initialization boot.mgr.registerBoot('driver.storage.mgr', @init, 'Storage Manager', 'driver.storage.vfs'); diff --git a/src/driver/storage/driver.storage.vfs.pas b/src/driver/storage/driver.storage.vfs.pas index 0c1a5d33..90968cd9 100644 --- a/src/driver/storage/driver.storage.vfs.pas +++ b/src/driver/storage/driver.storage.vfs.pas @@ -1479,62 +1479,37 @@ begin debug.tracer.push_trace('driver.storage.vfs.OpenFile.exit'); end; -function WriteFile(FileHandle : TFileHandle; Position : uint32; Buffer : puint8; Length : uint32) : uint32; -var - wait : TVFSSyncWaitCtx; +{ One blocking bridge for both directions. The async callback owns completion; + check Done with interrupts masked before parking to avoid a lost wakeup. } +function TransferFileSync(FileHandle : TFileHandle; Position : uint32; + Buffer : puint8; Length : uint32; Writing : boolean) : uint32; +var wait : TVFSSyncWaitCtx; begin - debug.tracer.push_trace('driver.storage.vfs.WriteFile.enter'); - WriteFile := 0; - - memset(uint32(@wait), 0, sizeof(TVFSSyncWaitCtx)); + TransferFileSync := 0; + memset(uint32(@wait), 0, sizeof(wait)); wait.Error := eUnknown; wait.Process := CurrentProcess; - - WriteFileAsync(FileHandle, Position, Buffer, Length, @wait.BytesDone, @vfs_sync_complete, @wait); + if Writing then + WriteFileAsync(FileHandle, Position, Buffer, Length, @wait.BytesDone, @vfs_sync_complete, @wait) + else + ReadFileAsync(FileHandle, Position, Buffer, Length, @wait.BytesDone, @vfs_sync_complete, @wait); asm pushf; cli end; - if puint32(@wait.Done)^ = 0 then begin - if CurrentProcess <> nil then - CurrentProcess^.State := psAwaiting; - end; + if puint32(@wait.Done)^ = 0 then + if wait.Process <> nil then wait.Process^.State := psAwaiting; asm popf end; + while puint32(@wait.Done)^ = 0 do asm hlt end; + if wait.Error = eNone then TransferFileSync := wait.BytesDone; +end; - while puint32(@wait.Done)^ = 0 do - asm hlt end; - - if wait.Error = eNone then - WriteFile := wait.BytesDone; - - debug.tracer.push_trace('driver.storage.vfs.WriteFile.exit'); +function WriteFile(FileHandle : TFileHandle; Position : uint32; Buffer : puint8; Length : uint32) : uint32; +begin + WriteFile := TransferFileSync(FileHandle, Position, Buffer, Length, true); end; function ReadFile(FileHandle : TFileHandle; Position : uint32; Buffer : puint8; Length : uint32) : uint32; -var - wait : TVFSSyncWaitCtx; begin - debug.tracer.push_trace('driver.storage.vfs.ReadFile.enter'); - ReadFile := 0; - - memset(uint32(@wait), 0, sizeof(TVFSSyncWaitCtx)); - wait.Error := eUnknown; - wait.Process := CurrentProcess; - - ReadFileAsync(FileHandle, Position, Buffer, Length, @wait.BytesDone, @vfs_sync_complete, @wait); - - asm pushf; cli end; - if puint32(@wait.Done)^ = 0 then begin - if CurrentProcess <> nil then - CurrentProcess^.State := psAwaiting; - end; - asm popf end; - - while puint32(@wait.Done)^ = 0 do - asm hlt end; - - if wait.Error = eNone then - ReadFile := wait.BytesDone; - - debug.tracer.push_trace('driver.storage.vfs.ReadFile.exit'); + ReadFile := TransferFileSync(FileHandle, Position, Buffer, Length, false); end; function CloseFile(Filehandle : TFileHandle) : TError; @@ -2217,138 +2192,57 @@ end; { ===================== Public File/Directory Management API ================ } -function DeleteFile(Path : pchar; Error : PError) : TError; +{ Synchronous filesystem hooks consume the local status before returning. + This helper owns both resolved path strings and the joined relative path. } +function DeletePath(Path : pchar; IsDirectory : boolean) : TError; var - vol : PStorage_Volume; - dir : pchar; - fname : pchar; - fullPath : pchar; - tmpPath : pchar; - status : puint32; - errCode : TError; + vol : PStorage_Volume; + dir, name, fullPath, prefix : pchar; + status : uint32; + supported : boolean; begin - debug.tracer.push_trace('driver.storage.vfs.DeleteFile.enter'); - DeleteFile := eUnknown; - if Error <> nil then Error^ := eUnknown; + DeletePath := eInvalidArgument; + if Path = nil then exit; + DeletePath := eInvalidPath; + if not ResolveFilePath(Path, PPStorage_Volume(@vol), PPChar(@dir), PPChar(@name)) then exit; - if Path = nil then begin - DeleteFile := eInvalidArgument; - if Error <> nil then Error^ := eInvalidArgument; - debug.tracer.push_trace('driver.storage.vfs.DeleteFile.exit'); - exit; + supported := false; + if vol^.filesystem <> nil then begin + if IsDirectory then supported := vol^.filesystem^.deleteDirCallback <> nil + else supported := vol^.filesystem^.deleteFileCallback <> nil; end; - - if not ResolveFilePath(Path, PPStorage_Volume(@vol), PPChar(@dir), PPChar(@fname)) then begin - DeleteFile := eInvalidPath; - if Error <> nil then Error^ := eInvalidPath; - debug.tracer.push_trace('driver.storage.vfs.DeleteFile.exit'); - exit; + DeletePath := eNotSupported; + if supported then begin + if (dir <> nil) and (stringSize(dir) > 0) then begin + prefix := stringConcat(dir, '/'); + fullPath := stringConcat(prefix, name); + kfree(void(prefix)); + end else fullPath := stringCopy(name); + DeletePath := eOutOfMemory; + if fullPath <> nil then begin + status := 0; + if IsDirectory then vol^.filesystem^.deleteDirCallback(vol, fullPath, @status) + else vol^.filesystem^.deleteFileCallback(vol, fullPath, @status); + DeletePath := TError(status); + if TError(status) = eNone then + VFS_OnMutation(weDeleted, Path, Path, vol, fullPath); + kfree(void(fullPath)); + end; end; + kfree(void(dir)); + kfree(void(name)); +end; - if (vol^.filesystem = nil) or (vol^.filesystem^.deleteFileCallback = nil) then begin - DeleteFile := eNotSupported; - if Error <> nil then Error^ := eNotSupported; - kfree(void(dir)); - kfree(void(fname)); - debug.tracer.push_trace('driver.storage.vfs.DeleteFile.exit'); - exit; - end; - - status := puint32(kalloc(4)); - status^ := 0; - - { Build the full relative path within the volume: dir/fname } - if (dir <> nil) and (stringSize(dir) > 0) then begin - tmpPath := stringConcat(dir, '/'); - fullPath := stringConcat(tmpPath, fname); - kfree(void(tmpPath)); - kfree(void(dir)); - dir := nil; - end else begin - if dir <> nil then kfree(void(dir)); - fullPath := stringCopy(fname); - end; - - vol^.filesystem^.deleteFileCallback(vol, fullPath, status); - errCode := TError(status^); - - DeleteFile := errCode; - if Error <> nil then Error^ := errCode; - - if errCode = eNone then - VFS_OnMutation(weDeleted, Path, Path, vol, fullPath); - - kfree(puint32(status)); - kfree(void(fullPath)); - kfree(void(fname)); - debug.tracer.push_trace('driver.storage.vfs.DeleteFile.exit'); +function DeleteFile(Path : pchar; Error : PError) : TError; +begin + DeleteFile := DeletePath(Path, false); + if Error <> nil then Error^ := DeleteFile; end; function DeleteDirectory(Path : pchar; Error : PError) : TError; -var - vol : PStorage_Volume; - dir : pchar; - dirName : pchar; - fullPath : pchar; - tmpPath : pchar; - status : puint32; - errCode : TError; begin - debug.tracer.push_trace('driver.storage.vfs.DeleteDirectory.enter'); - DeleteDirectory := eUnknown; - if Error <> nil then Error^ := eUnknown; - - if Path = nil then begin - DeleteDirectory := eInvalidArgument; - if Error <> nil then Error^ := eInvalidArgument; - debug.tracer.push_trace('driver.storage.vfs.DeleteDirectory.exit'); - exit; - end; - - if not ResolveFilePath(Path, PPStorage_Volume(@vol), PPChar(@dir), PPChar(@dirName)) then begin - DeleteDirectory := eInvalidPath; - if Error <> nil then Error^ := eInvalidPath; - debug.tracer.push_trace('driver.storage.vfs.DeleteDirectory.exit'); - exit; - end; - - if (vol^.filesystem = nil) or (vol^.filesystem^.deleteDirCallback = nil) then begin - DeleteDirectory := eNotSupported; - if Error <> nil then Error^ := eNotSupported; - kfree(void(dir)); - kfree(void(dirName)); - debug.tracer.push_trace('driver.storage.vfs.DeleteDirectory.exit'); - exit; - end; - - status := puint32(kalloc(4)); - status^ := 0; - - { Build full relative path for the directory } - if (dir <> nil) and (stringSize(dir) > 0) then begin - tmpPath := stringConcat(dir, '/'); - fullPath := stringConcat(tmpPath, dirName); - kfree(void(tmpPath)); - kfree(void(dir)); - dir := nil; - end else begin - if dir <> nil then kfree(void(dir)); - fullPath := stringCopy(dirName); - end; - - vol^.filesystem^.deleteDirCallback(vol, fullPath, status); - errCode := TError(status^); - - DeleteDirectory := errCode; - if Error <> nil then Error^ := errCode; - - if errCode = eNone then - VFS_OnMutation(weDeleted, Path, Path, vol, fullPath); - - kfree(puint32(status)); - kfree(void(fullPath)); - kfree(void(dirName)); - debug.tracer.push_trace('driver.storage.vfs.DeleteDirectory.exit'); + DeleteDirectory := DeletePath(Path, true); + if Error <> nil then Error^ := DeleteDirectory; end; function CreateDirectory(Path : pchar; Error : PError) : TError; @@ -2519,94 +2413,59 @@ end; { ===================== Async Delete ======================================== } -procedure DeleteFileAsync(Path : pchar; Error : PError; Callback : TIOCallback; CallbackData : pointer); +procedure CompletePathOperation(Error : PError; ResultCode : TError; + Callback : TIOCallback; CallbackData : pointer); +begin + if Error <> nil then Error^ := ResultCode; + if Callback <> nil then Callback(ResultCode, CallbackData); +end; + +procedure DeletePathAsync(Path : pchar; IsDirectory : boolean; Error : PError; + Callback : TIOCallback; CallbackData : pointer); var ctx : PVFSPathAsyncCtx; worker : proc.types.PProcessContext; + workerName : pchar; begin - debug.tracer.push_trace('driver.storage.vfs.DeleteFileAsync.enter'); if Error <> nil then Error^ := eUnknown; - if Path = nil then begin - if Error <> nil then Error^ := eInvalidArgument; - if Callback <> nil then Callback(eInvalidArgument, CallbackData); + CompletePathOperation(Error, eInvalidArgument, Callback, CallbackData); exit; end; - ctx := PVFSPathAsyncCtx(kalloc(sizeof(TVFSPathAsyncCtx))); if ctx = nil then begin - if Error <> nil then Error^ := eOutOfMemory; - if Callback <> nil then Callback(eOutOfMemory, CallbackData); + CompletePathOperation(Error, eOutOfMemory, Callback, CallbackData); exit; end; memset(uint32(ctx), 0, sizeof(TVFSPathAsyncCtx)); ctx^.Path := stringCopy(Path); if ctx^.Path = nil then begin kfree(void(ctx)); - if Error <> nil then Error^ := eOutOfMemory; - if Callback <> nil then Callback(eOutOfMemory, CallbackData); + CompletePathOperation(Error, eOutOfMemory, Callback, CallbackData); exit; end; ctx^.ErrorPtr := Error; - ctx^.IsDirectory := false; + ctx^.IsDirectory := IsDirectory; ctx^.UserCallback := Callback; ctx^.UserData := CallbackData; - worker := proc.mgr.create('vfs.delf', @vfs_pathop_worker, void(ctx), 1); + if IsDirectory then workerName := 'vfs.deld' else workerName := 'vfs.delf'; + worker := proc.mgr.create(workerName, @vfs_pathop_worker, void(ctx), 1); + { A published worker owns ctx and may already have freed it. } if worker = nil then begin - if ctx^.Path <> nil then - kfree(void(ctx^.Path)); + kfree(void(ctx^.Path)); kfree(void(ctx)); - if Error <> nil then Error^ := eOutOfMemory; - if Callback <> nil then Callback(eOutOfMemory, CallbackData); - exit; + CompletePathOperation(Error, eOutOfMemory, Callback, CallbackData); end; +end; - debug.tracer.push_trace('driver.storage.vfs.DeleteFileAsync.exit'); +procedure DeleteFileAsync(Path : pchar; Error : PError; Callback : TIOCallback; CallbackData : pointer); +begin + DeletePathAsync(Path, false, Error, Callback, CallbackData); end; procedure DeleteDirectoryAsync(Path : pchar; Error : PError; Callback : TIOCallback; CallbackData : pointer); -var - ctx : PVFSPathAsyncCtx; - worker : proc.types.PProcessContext; begin - debug.tracer.push_trace('driver.storage.vfs.DeleteDirectoryAsync.enter'); - if Error <> nil then Error^ := eUnknown; - - if Path = nil then begin - if Error <> nil then Error^ := eInvalidArgument; - if Callback <> nil then Callback(eInvalidArgument, CallbackData); - exit; - end; - - ctx := PVFSPathAsyncCtx(kalloc(sizeof(TVFSPathAsyncCtx))); - if ctx = nil then begin - if Error <> nil then Error^ := eOutOfMemory; - if Callback <> nil then Callback(eOutOfMemory, CallbackData); - exit; - end; - memset(uint32(ctx), 0, sizeof(TVFSPathAsyncCtx)); - ctx^.Path := stringCopy(Path); - if ctx^.Path = nil then begin - kfree(void(ctx)); - if Error <> nil then Error^ := eOutOfMemory; - if Callback <> nil then Callback(eOutOfMemory, CallbackData); - exit; - end; - ctx^.ErrorPtr := Error; - ctx^.IsDirectory := true; - ctx^.UserCallback := Callback; - ctx^.UserData := CallbackData; - worker := proc.mgr.create('vfs.deld', @vfs_pathop_worker, void(ctx), 1); - if worker = nil then begin - if ctx^.Path <> nil then - kfree(void(ctx^.Path)); - kfree(void(ctx)); - if Error <> nil then Error^ := eOutOfMemory; - if Callback <> nil then Callback(eOutOfMemory, CallbackData); - exit; - end; - - debug.tracer.push_trace('driver.storage.vfs.DeleteDirectoryAsync.exit'); + DeletePathAsync(Path, true, Error, Callback, CallbackData); end; { ===================== Async Directory Listing ============================= } @@ -3067,39 +2926,6 @@ begin end; end; -{ Free a directory listing map that was freshly allocated by volumeGetDirectories. - Only call this when the map is caller-owned (i.e. the current directory is a DRIVE). } -procedure freeOwnedDirListing(Map : PHashMap); -var - i : uint32; - Item : PHashItem; - nextItem : PHashItem; - vfsObj : PVFSObject; -begin - if Map = nil then exit; - for i := 0 to Map^.Size - 1 do begin - Item := Map^.Table[i]; - while Item <> nil do begin - nextItem := Item^.Next; - { Free the PVFSObject and its ObjectName string that we allocated } - if Item^.Data <> nil then begin - vfsObj := PVFSObject(Item^.Data); - if vfsObj^.ObjectName <> nil then - kfree(void(vfsObj^.ObjectName)); - kfree(Item^.Data); - end; - { Free the key string that was stringCopy'd by volumeGetDirectories } - if Item^.Key <> nil then - kfree(void(Item^.Key)); - { Free the PHashItem node itself } - kfree(void(Item)); - Item := nextItem; - end; - Map^.Table[i] := nil; - end; - kfree(void(Map)); -end; - procedure VFS_COMMAND_LS(params : PParamList; stdin_buf, stdout_buf, stderr_buf: POutBuf); var Map : PHashMap; @@ -3677,10 +3503,43 @@ begin inc(puint32(userdata)^); end; +type + PDeleteTest = ^TDeleteTest; + TDeleteTest = record + Files, Directories, Completions : uint32; + ExpectedPath : pchar; + ResultCode, CompletedError : TError; + PathMatches : boolean; + end; + +procedure UTestDeleteFile(vol : PStorage_Volume; path : pchar; status : puint32); +var state : PDeleteTest; +begin + state := PDeleteTest(vol^.fsPrivate); + inc(state^.Files); + state^.PathMatches := stringEquals(path, state^.ExpectedPath); + status^ := uint32(state^.ResultCode); +end; + +procedure UTestDeleteDirectory(vol : PStorage_Volume; path : pchar; status : puint32); +begin + UTestDeleteFile(vol, path, status); + dec(PDeleteTest(vol^.fsPrivate)^.Files); + inc(PDeleteTest(vol^.fsPrivate)^.Directories); +end; + +procedure UTestDeleteComplete(error : TError; userdata : pointer); +begin + inc(PDeleteTest(userdata)^.Completions); + PDeleteTest(userdata)^.CompletedError := error; +end; + procedure UnitTest; var mountVolA, mountVolB : PStorage_Volume; mountFS : PFilesystem; + mountDevice : PStorage_Device; + deleteState : TDeleteTest; passed, failed : uint32; result : TIsPathValid; errCode : TError; @@ -3760,6 +3619,32 @@ begin Assert(mountDefaultVolume(mountVolA) = eNone,'label mount succeeds'); obj := GetObjectFromPath('/disk/__MOUNT_TEST__'); Assert((obj <> nil) and (obj^.Reference = void(mountVolA)),'label mount points to correct volume'); + { Exercise shared deletion against a memory-only filesystem. } + mountDevice := PStorage_Device(kalloc(sizeof(TStorage_Device))); + memset(uint32(mountDevice), 0, sizeof(TStorage_Device)); + memset(uint32(@deleteState), 0, sizeof(deleteState)); + mountVolA^.device := mountDevice; + mountVolA^.fsPrivate := @deleteState; + Assert(DeleteFile(nil, @errCode)=eInvalidArgument, 'delete nil path'); + Assert(errCode=eInvalidArgument, 'delete error output matches result'); + Assert(DeleteDirectory('/disk/__MOUNT_TEST__/entry', nil)=eNotSupported, 'delete missing directory hook'); + mountFS^.deleteFileCallback := @UTestDeleteFile; + mountFS^.deleteDirCallback := @UTestDeleteDirectory; + deleteState.ExpectedPath := 'entry'; + deleteState.ResultCode := eNone; + Assert(DeleteFile('/disk/__MOUNT_TEST__/entry', @errCode)=eNone, 'delete root file succeeds'); + Assert((deleteState.Files=1) and (deleteState.Directories=0) and deleteState.PathMatches, 'delete routes file hook with relative path'); + deleteState.ExpectedPath := 'nested/entry'; + deleteState.ResultCode := eReadOnly; + Assert(DeleteDirectory('/disk/__MOUNT_TEST__/nested/entry', @errCode)=eReadOnly, 'delete directory propagates filesystem failure'); + Assert((errCode=eReadOnly) and (deleteState.Directories=1) and deleteState.PathMatches, 'delete directory hook receives nested path'); + DeleteFileAsync(nil, @errCode, @UTestDeleteComplete, @deleteState); + Assert((deleteState.Completions=1) and (deleteState.CompletedError=eInvalidArgument) and (errCode=eInvalidArgument), 'async file rejection completes exactly once'); + DeleteDirectoryAsync(nil, nil, @UTestDeleteComplete, @deleteState); + Assert((deleteState.Completions=2) and (deleteState.CompletedError=eInvalidArgument), 'async directory rejection supports nil error output'); + mountVolA^.fsPrivate := nil; + mountVolA^.device := nil; + kfree(void(mountDevice)); Assert(mountDefaultVolume(mountVolB) = eNone,'duplicate labels both mount'); obj := GetObjectFromPath('/disk/__MOUNT_TEST__-vol4294967293'); Assert((obj <> nil) and (obj^.Reference = void(mountVolB)),'duplicate label gets ID suffix'); -- 2.54.0 From 6c7619bc2cb49c44221c04e6def2736afcd01e7e Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 31 Oct 2026 20:50:31 +0000 Subject: [PATCH 17/21] Keep development plan local and remove it from version control --- .gitignore | 3 +- plan.md | 500 ----------------------------------------------------- 2 files changed, 2 insertions(+), 501 deletions(-) delete mode 100644 plan.md diff --git a/.gitignore b/.gitignore index ccf7807c..e02fde13 100644 --- a/.gitignore +++ b/.gitignore @@ -15,8 +15,9 @@ localenv.json dockerout.txt AGENTS.md lessons_learnt.md +/plan.md *.log wasuro src/core/core.version.pas site/* -!site/.gitkeep \ No newline at end of file +!site/.gitkeep diff --git a/plan.md b/plan.md deleted file mode 100644 index b4c20cb5..00000000 --- a/plan.md +++ /dev/null @@ -1,500 +0,0 @@ -# FAT32 Rewrite Plan - -Implement this entire plan without stopping. - -## Goal - -Replace the current FAT32 runtime I/O architecture with a simpler, faster design where open-time metadata work builds a cached runtime view, and steady-state file reads and writes consume that cached view with direct storage I/O. - -The rewrite should make the common path thin enough that contiguous, preopened reads are near raw block I/O performance, while preserving correctness for fragmented files, append/growth, and metadata updates. - -## Immediate Direction - -1. Rename the current FAT32 implementation file to `driver.storage.fs.fat32.old.pas`. -2. Create a new `driver.storage.fs.fat32.pas` as the public registration/entry unit. -3. Move the new implementation into a dedicated `fat32/` folder with multiple focused units instead of another monolithic source file. -4. Rebuild the internals from a simpler architecture instead of continuing to patch the old design. - -The old file remains as a reference during migration and can be deleted after parity is reached. - -## Core Problems To Fix - -1. The current runtime path is overcomplicated. -2. Ordinary file I/O pays too much control-plane overhead. -3. Fragmented files are treated as a special slow path instead of a normal case. -4. The async API still hides synchronous behavior in some cases. -5. FAT and directory metadata work leaks into the steady-state transfer loop. -6. The current source layout is too large to maintain comfortably as one unit. - -## Design Principles - -1. Fragmentation is normal, not a special case. -2. File I/O should be run-map driven. -3. FAT is metadata, not the runtime transfer engine. -4. The common path must avoid `kalloc`/`kfree`. -5. Async APIs must actually be async. -6. The same architecture must handle 1 run or 100 runs. -7. Metadata work should be deferred or isolated from the hot path. -8. Ownership and lifetime rules must be explicit at the file, volume, and transfer levels. - -## Architectural Layers - -The new design should keep three layers separate so FAT policy does not leak back into the transfer engine. - -### A. FAT Metadata Layer - -Responsible for: - -- boot record and FAT geometry interpretation -- FAT entry reads/writes -- FAT chain traversal -- directory entry lookup -- raw directory entry locator resolution -- directory entry update/commit -- cluster allocation/free -- run-map build/rebuild -- corruption detection and validation - -### B. Open-File State Layer - -Responsible for: - -- pointer/reference to the mounted volume -- pointer/reference to shared immutable `TFATVolumeInfo` -- first cluster -- committed byte size -- allocated cluster count -- cached run map -- logical dirty state -- reusable scratch sector buffer -- sequential cursor hints -- cached raw directory entry location -- open-handle lifetime and teardown bookkeeping - -### C. Transfer Layer - -Responsible for: - -- offset/length to run slicing -- direct storage I/O submission -- partial-sector read-modify-write handling -- async continuation/chaining -- completed-byte accounting -- EOF/capacity bounds enforcement - -The transfer layer must not perform FAT scans, directory searches, or cluster allocation from IRQ/completion context. - -## Runtime View: Runs, Not FAT Walks - -Define the runtime view explicitly: - -- run = contiguous range of file clusters on disk -- run map = ordered in-memory array of all file runs for one open handle - -The run map is the authoritative transfer view for that handle. FAT is consulted only during open, growth, explicit rebuild/validation, or metadata commit. - -Fragmentation only changes how many runs are walked. It must not select a different architecture. - -## Volume Geometry And Shared State - -Do not copy boot/FAT geometry into every file handle. - -Use a shared per-volume immutable record such as `TFATVolumeInfo` containing: - -- sector size -- sectors per cluster -- FAT start -- FAT size -- data start -- root cluster -- max cluster -- other immutable geometry/cache references needed for fast translation - -Each open file should hold: - -- `Volume` -- pointer/reference to `TFATVolumeInfo` - -## Open File State - -Each open file should own a compact cached state record such as `TFATOpenFile` containing: - -- `Volume` -- `VolumeInfo` -- `FirstCluster` -- `ByteSize` -- `AllocClusters` -- run map -- sequential cursor hint -- reusable scratch sector buffer -- dirty allocation / dirty size / dirty dir-entry flags -- raw directory entry locator - -This state replaces repeated rediscovery and repeated FAT walking during normal I/O. - -### Raw Directory Entry Locator - -Every open file must cache enough information to update the on-disk directory entry without rescanning the parent directory. The cached form should include either: - -- parent directory first cluster plus raw entry location data - -or a fully resolved raw location such as: - -- raw directory entry cluster -- raw sector LBA -- entry index within sector - -This is required to avoid regressions in first-cluster and size updates. - -### File Size And Allocation State - -The plan must keep these rules explicit: - -- `ByteSize` = committed logical EOF -- `AllocClusters` = current allocated chain length -- allocated capacity = `AllocClusters * ClusterSize` -- writes may use existing allocated capacity without allocation growth -- `ByteSize` advances only from actual completed writes, never intended request length - -This directly protects against partial-growth and short-write corruption. - -### Empty File Creation Model - -For a newly created empty file: - -- `FirstCluster` may be `0` -- run map is initially empty -- `ByteSize = 0` -- `AllocClusters = 0` -- raw directory entry locator is known immediately -- first allocation happens only when a write requires storage - -## Run-Map Lifetime Rules - -This needs to be defined up front: - -1. Each open file owns its own run map. -2. The run map is built once at open and then trusted for that handle unless the same handle grows or explicitly rebuilds it. -3. Writes through that handle update that handle's run map incrementally in memory. -4. Other open handles are not kept coherent in the first rewrite. Their run maps and sizes are treated as open-time snapshots until reopen. -5. Concurrent write opens for the same file should be disallowed for the first rewrite, or explicitly treated as unsupported/undefined. Prefer rejecting the second writable open. -6. Flush/close commits directory entry and FAT changes from the handle's current state; optional full rebuild-and-validate before commit is a later hardening step, not part of the hot path. - -## Unified Transfer Engine - -Implement one transfer engine for both reads and writes. - -It should: - -- accept open-file state, buffer, offset, length, mode, callback -- translate byte ranges into run slices -- issue direct storage I/O chunk by chunk -- handle completion by advancing precomputed transfer state -- use scratch-sector read-modify-write only for unaligned edges -- complete without separate aligned/fragmented architectures - -The old special-case direct path should become the normal path. - -## Async Execution Model - -For the first rewrite, define one concrete model: - -- open/build-run-map may remain synchronous -- read/write entrypoints submit async storage I/O and return immediately -- transfer progression uses a small pooled `TFATTransferCtx` -- IRQ/completion callbacks only advance precomputed transfer state and schedule the next I/O or final completion -- IRQ/completion callbacks must not call helpers that can perform blocking FAT or directory I/O -- close/flush may remain synchronous initially if that keeps the contract honest and simple - -This keeps async behavior explicit and avoids hidden blocking inside async hooks. - -## Rare Heavy Work Only - -Only these operations should do heavier FAT or metadata work: - -- opening a file -- building or rebuilding a run map -- allocating/extending clusters -- updating directory entries -- flushing FAT or metadata -- partial-sector read-modify-write preparation -- corruption validation/failure handling - -These must not dominate steady-state transfer throughput. - -## Corruption And Validation Rules - -When FAT traversal or run-map build finds: - -- out-of-range cluster -- cycle -- bad-cluster marker -- premature EOC relative to stored size - -the first rewrite should fail the open or mark the file invalid for transfer. It should not attempt automatic repair. - -All corruption-handling loops must be bounded by hard limits derived from FAT size / max cluster count. - -## Required Runtime Invariants - -Invariant 1 - -For any open file: - -`sum(run.length_clusters) = AllocClusters` - -Invariant 2 - -Committed logical size must satisfy: - -`ByteSize <= AllocClusters * ClusterSize` - -Invariant 3 - -On successful flush/close: - -- directory entry first cluster matches open state -- directory entry size matches committed bytes -- FAT chain matches the run map if dirty allocation occurred - -Invariant 4 - -No async transfer may outlive: - -- file handle -- volume object -- transfer context backing storage - -Invariant 5 - -The run map is the authoritative transfer view for the handle; FAT is not re-walked in the steady-state transfer path. - -## What To Remove From The New Design - -The new implementation should avoid carrying forward these ideas: - -- worker-centric per-chunk file transfer orchestration -- separate fast path and general path for aligned vs fragmented I/O -- synchronous fallbacks hidden inside async hooks -- repeated FAT-chain walking during ordinary sequential transfer -- hot-path temporary allocations -- parent-directory rescans just to commit file size or first cluster - -## Planned Source Layout - -The rewrite should move FAT32 internals into a dedicated folder so the code remains readable and reviewable. - -Planned shape: - -- `src/driver/storage/fs/driver.storage.fs.fat32.pas` for public registration and top-level hook wiring -- `src/driver/storage/fs/driver.storage.fs.fat32.old.pas` for the temporary legacy reference -- `src/driver/storage/fs/fat32/` for focused internal units - -Likely internal unit split: - -- volume / geometry -- FAT metadata helpers -- directory lookup and raw entry location -- open-file state and handle lifetime -- run-map construction -- transfer engine -- allocation/growth -- flush/sync - -Exact file names can be finalized during Phase 0, but the rewrite should not collapse back into one large source file. - -## Migration Plan - -### Phase 0: File Split And Scaffolding - -1. Rename existing FAT32 unit source to `driver.storage.fs.fat32.old.pas`. -2. Create a new `driver.storage.fs.fat32.pas` that preserves the public filesystem registration contract. -3. Create the `src/driver/storage/fs/fat32/` folder and split new internals into focused units from the start. - -### Phase 1A: Volume Geometry And FAT Primitives - -Build and validate: - -- `TFATVolumeInfo` -- shared geometry decode -- FAT entry read helper -- bounded FAT chain traversal helper -- run-map builder -- raw directory entry locator helper - -At the end of this phase, the core metadata primitives are usable without the old runtime architecture. - -### Phase 1B: Minimal Read-Only Mount/Open - -Build the new unit with only: - -- type definitions needed by the filesystem manager -- boot record and directory entry structures -- volume identify/detect hooks -- basic open/close hooks -- read-only run-map construction - -At the end of this phase, the new unit should compile, mount a FAT32 volume, open files, and build read-only run maps. - -### Phase 2: Read Path First - -Implement a unified run-driven read engine: - -- build run map at open -- read from runs directly -- support multi-run files without architectural fallback -- use a pooled transfer context -- use a reusable scratch sector buffer only for unaligned reads - -Acceptance target: - -- aligned sequential reads on contiguous preopened files should be near raw `STORBENCH` read speed -- fragmented reads should scale mainly with run count, not collapse architecturally - -### Phase 3: Writes Within Existing Allocation - -Implement overwrite and in-capacity write support on the same transfer model: - -- direct writes over existing runs -- partial-sector handling through reusable scratch buffer -- metadata dirtied in memory, not rewritten every chunk -- logical EOF advanced only from actual completed bytes - -Acceptance target: - -- aligned writes over preallocated files use the same thin transfer engine as reads -- no per-chunk metadata rewrite - -### Phase 4: Allocation And Growth - -Implement append and extension logic: - -- allocate clusters when writes exceed current capacity -- extend the open-file run map incrementally -- prefer contiguous growth when possible -- keep allocation outside the steady-state transfer loop -- handle first-cluster update for empty files - -Acceptance target: - -- append-heavy writes remain much closer to preallocated write performance than today -- fragmented growth still remains run-driven - -### Phase 5: Metadata Commit Rules - -Implement clean flush/close behavior: - -- FAT dirty tracking -- directory entry update on close or explicit sync -- first-cluster update for newly created files -- byte-size commit from actual completed bytes only -- handle/volume teardown rules for in-flight transfers - -Acceptance target: - -- no regression in crash-safety compared with current design assumptions -- no hot-path metadata thrash - -### Phase 6: Async Contract Cleanup - -Make the async API honest and documented: - -- async hooks always return immediately -- no synchronous disk I/O hidden inside async entrypoints -- callback context is consistent and documented -- FD lifetime and teardown rules are enforced -- close/unmount/process-exit races are explicitly handled - -Acceptance target: - -- `ReadFileAsync` and `WriteFileAsync` are genuinely async for all supported cases -- no blocking FAT work is re-entered from IRQ/completion context - -## Required Supporting Structures - -The new implementation should include: - -- `TFATVolumeInfo` for shared immutable geometry -- `TFATOpenFile` with run map, logical state, raw directory entry locator, and reusable scratch buffer -- pooled `TFATTransferCtx` -- compact run representation -- read/write helpers that work from run maps, not repeated FAT scans -- explicit flush/sync helpers - -It should not require a per-volume worker just to progress ordinary file transfers. - -## Compatibility Requirements - -The new implementation should preserve compatibility with: - -- `driver.storage.fs.mgr` -- `driver.storage.vfs` -- current filesystem hook signatures in `driver.storage.types` -- current FAT32 on-disk format - -The rewrite should change the internal architecture, not the external filesystem contract, unless a later cleanup intentionally revises that contract. - -## Performance Targets - -Track both open/close cost and steady-state transfer throughput. - -Initial realistic targets: - -- reads on contiguous preopened files should be near raw block I/O throughput -- writes on preallocated files should be much closer to raw than today -- append/growth will remain measurably slower due to FAT and directory updates -- the remaining gap to raw should be mostly metadata/allocation cost, not control-plane overhead - -Longer-term target: - -- once FAT32 overhead is reduced, remaining limits should mainly be in storage/AHCI - -## Correctness Requirements - -Must explicitly preserve: - -- correct handling of fragmented files -- correct file size updates -- correct first-cluster updates -- correct directory entry location handling -- no shared DMA-buffer hazards -- no blocking FAT work from IRQ callbacks -- no use-after-free during close/unmount/process teardown -- bounded handling of bad or cyclic FAT chains -- correct behavior for empty files with `FirstCluster = 0` - -## Validation Plan - -1. Compile after each phase. -2. Smoke-test mount/open/read on a known FAT32 volume. -3. Measure open/close cost separately from transfer throughput. -4. Run `IOTEST` on a fresh 4 KB cluster FAT32 volume. -5. Run `STORBENCH` on the same disk and compare. -6. Test fragmented-file read/write behavior. -7. Test create, append, close, reopen, and verify. -8. Test writable-open rejection or unsupported-path behavior for concurrent writers. -9. Test close-during-async and volume invalidation edge cases. -10. Test corruption detection on invalid or cyclic cluster chains. - -## Non-Goals For The First Rewrite - -These are explicitly not required for the first clean rewrite: - -- perfect crash-consistency semantics -- journaled metadata behavior -- aggressive readahead/writeback cache -- cross-handle cache coherency for concurrent file writers -- automatic FAT repair - -Those can come after the architecture is clean. - -## Definition Of Success - -The rewrite is successful when: - -1. FAT32 ordinary file I/O is run-driven by default. -2. The old direct path no longer exists as a separate idea because it became the main path. -3. Async hooks are genuinely async. -4. Hot-path allocator churn is removed. -5. Directory entry updates no longer require parent-directory rescans during ordinary file writes. -6. FAT32 throughput is no longer dominated by internal orchestration overhead. -- 2.54.0 From 53cc758e604f9cbaea65f0ba240378b6c6a86797 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 31 Oct 2026 20:56:15 +0000 Subject: [PATCH 18/21] Add standard license headers and Aaron Hance attribution --- .../storage/ctl/driver.storage.ctl.usb.md | 17 +++++++++++++++++ doc/src/driver/storage/driver.storage.mgr.md | 17 +++++++++++++++++ src/driver/bus/usb/driver.bus.usb.core.pas | 15 +++++++++++++++ src/driver/bus/usb/driver.bus.usb.ehci.pas | 15 +++++++++++++++ src/driver/bus/usb/driver.bus.usb.ohci.pas | 15 +++++++++++++++ src/driver/bus/usb/driver.bus.usb.types.pas | 15 +++++++++++++++ .../storage/ctl/driver.storage.ctl.usb.pas | 18 ++++++++++++++++++ src/driver/storage/driver.storage.mgr.pas | 1 + src/driver/storage/driver.storage.vfs.pas | 1 + src/proc/proc.mgr.pas | 14 ++++++++++++++ src/test/test.pas | 17 +++++++++++++++++ 11 files changed, 145 insertions(+) diff --git a/doc/src/driver/storage/ctl/driver.storage.ctl.usb.md b/doc/src/driver/storage/ctl/driver.storage.ctl.usb.md index 7ff06d8b..166553be 100644 --- a/doc/src/driver/storage/ctl/driver.storage.ctl.usb.md +++ b/doc/src/driver/storage/ctl/driver.storage.ctl.usb.md @@ -1,3 +1,20 @@ + + # USB mass storage `driver.storage.ctl.usb` implements SCSI transparent commands over USB Mass diff --git a/doc/src/driver/storage/driver.storage.mgr.md b/doc/src/driver/storage/driver.storage.mgr.md index c4b42d91..ae0241f0 100644 --- a/doc/src/driver/storage/driver.storage.mgr.md +++ b/doc/src/driver/storage/driver.storage.mgr.md @@ -1,3 +1,20 @@ + + # driver.storage.mgr Physical storage device registry and I/O dispatch manager. diff --git a/src/driver/bus/usb/driver.bus.usb.core.pas b/src/driver/bus/usb/driver.bus.usb.core.pas index afebffd1..3009b020 100644 --- a/src/driver/bus/usb/driver.bus.usb.core.pas +++ b/src/driver/bus/usb/driver.bus.usb.core.pas @@ -1,6 +1,21 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + { Driver->Bus->driver.bus.usb->USBCore - driver.bus.usb Core Enumeration, Transfer API & HC Management. + @author(Aaron Hance ) @author(Kieron Morris ) } unit driver.bus.usb.core; diff --git a/src/driver/bus/usb/driver.bus.usb.ehci.pas b/src/driver/bus/usb/driver.bus.usb.ehci.pas index 56b1a53d..bc1173a2 100644 --- a/src/driver/bus/usb/driver.bus.usb.ehci.pas +++ b/src/driver/bus/usb/driver.bus.usb.ehci.pas @@ -1,3 +1,17 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + { Driver->Bus->driver.bus.usb->driver.bus.usb.ehci - Enhanced Host Controller Interface Driver. @@ -8,6 +22,7 @@ driver.bus.usb.ehci also handles companion controller (driver.bus.usb.uhci/driver.bus.usb.ohci) routing for full/low-speed devices via the CONFIGFLAG and PORTSC port-owner bits. + @author(Aaron Hance ) @author(Kieron Morris ) } unit driver.bus.usb.ehci; diff --git a/src/driver/bus/usb/driver.bus.usb.ohci.pas b/src/driver/bus/usb/driver.bus.usb.ohci.pas index 642954a0..3fec817a 100644 --- a/src/driver/bus/usb/driver.bus.usb.ohci.pas +++ b/src/driver/bus/usb/driver.bus.usb.ohci.pas @@ -1,3 +1,17 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + { Driver->Bus->driver.bus.usb->driver.bus.usb.ohci - Open Host Controller Interface Driver. @@ -5,6 +19,7 @@ driver.bus.usb.ohci uses Endpoint Descriptors (ED), Transfer Descriptors (TD), and a Host Controller Communication Area (HCCA) for scheduling and completion. + @author(Aaron Hance ) @author(Kieron Morris ) } unit driver.bus.usb.ohci; diff --git a/src/driver/bus/usb/driver.bus.usb.types.pas b/src/driver/bus/usb/driver.bus.usb.types.pas index 984421b3..a6f38572 100644 --- a/src/driver/bus/usb/driver.bus.usb.types.pas +++ b/src/driver/bus/usb/driver.bus.usb.types.pas @@ -1,6 +1,21 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + { Driver->Bus->driver.bus.usb->USBTypes - Shared driver.bus.usb Types, Constants & HC Abstraction. + @author(Aaron Hance ) @author(Kieron Morris ) } unit driver.bus.usb.types; diff --git a/src/driver/storage/ctl/driver.storage.ctl.usb.pas b/src/driver/storage/ctl/driver.storage.ctl.usb.pas index 5c9654f9..e229e651 100644 --- a/src/driver/storage/ctl/driver.storage.ctl.usb.pas +++ b/src/driver/storage/ctl/driver.storage.ctl.usb.pas @@ -1,7 +1,25 @@ +// Copyright 2021 Aaron Hance +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + { USB Mass Storage: SCSI transparent commands over Bulk-Only Transport. LUN 0, one interface, READ/WRITE(10), 512..4096 byte sectors. A persistent worker owns all storage I/O. No caller buffer is ever DMA-visible. Protocol reference: https://www.usb.org/sites/default/files/usbmassbulk_10.pdf } +{ + @author(Aaron Hance ) +} unit driver.storage.ctl.usb; interface diff --git a/src/driver/storage/driver.storage.mgr.pas b/src/driver/storage/driver.storage.mgr.pas index 5c4688b7..1d713084 100644 --- a/src/driver/storage/driver.storage.mgr.pas +++ b/src/driver/storage/driver.storage.mgr.pas @@ -1,4 +1,5 @@ // Copyright 2021 Aaron Hance +// Copyright 2026 Aaron Hance // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/driver/storage/driver.storage.vfs.pas b/src/driver/storage/driver.storage.vfs.pas index 90968cd9..e819cbc6 100644 --- a/src/driver/storage/driver.storage.vfs.pas +++ b/src/driver/storage/driver.storage.vfs.pas @@ -1,4 +1,5 @@ // Copyright 2021 Kieron Morris +// Copyright 2026 Aaron Hance // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/proc/proc.mgr.pas b/src/proc/proc.mgr.pas index 68d78563..f7a61113 100644 --- a/src/proc/proc.mgr.pas +++ b/src/proc/proc.mgr.pas @@ -1,3 +1,17 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + { ProcessManager - Preemptive process management for the Asuro kernel. diff --git a/src/test/test.pas b/src/test/test.pas index 520b7402..3f239a19 100644 --- a/src/test/test.pas +++ b/src/test/test.pas @@ -1,3 +1,20 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ + @author(Aaron Hance ) +} unit test; interface -- 2.54.0 From cebad0b583861b43c2920a21b3ee704392229628 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 1 Nov 2026 20:22:07 +0000 Subject: [PATCH 19/21] Fix USB volume discovery, automounting, and file saves --- src/app/app.diskutil.pas | 24 +++++---- src/app/app.notepad.pas | 6 ++- src/app/app.volcmd.pas | 4 +- .../storage/ctl/driver.storage.ctl.usb.pas | 48 +++++++++++++++-- .../storage/driver.storage.metatest.pas | 53 ++++++++++++++++++- src/driver/storage/driver.storage.types.pas | 1 + src/driver/storage/driver.storage.vfs.pas | 45 +++++++++++++++- .../fs/fat32/driver.storage.fs.fat32.vol.pas | 35 +++++++++--- 8 files changed, 189 insertions(+), 27 deletions(-) diff --git a/src/app/app.diskutil.pas b/src/app/app.diskutil.pas index 719c887b..df6d4202 100644 --- a/src/app/app.diskutil.pas +++ b/src/app/app.diskutil.pas @@ -1200,7 +1200,8 @@ begin vol := findVolumeForPartition(device, part.LBA_start); if vol <> nil then begin freeB := uint64(vol^.freeSectors) * vol^.sectorSize; - freeStr := uiConcat(formatSizeStr(freeB), ' free'); + if (vol^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) <> 0 then freeStr := 'Free: unknown' + else freeStr := uiConcat(formatSizeStr(freeB), ' free'); lbl := lv_label_create(row); lv_label_set_text(lbl, freeStr); lv_obj_set_style_text_color(lbl, lv_color_make(100, 180, 130), 0); @@ -1240,7 +1241,6 @@ var dw : sint32; i : uint32; totalBytes : uint64; - usedSectors : uint64; freeBytes : uint64; volCnt : uint32; devVolCnt : uint32; @@ -1274,22 +1274,18 @@ begin totalBytes := uint64(device^.maxSectorCount) * device^.sectorSize; addInfoRow(detail, dw, 'Total Size', formatSizePrecise(totalBytes)); - { Compute free space from in-memory volume data (no I/O) } - usedSectors := 0; + { Use the same cached partition geometry as Add Partition. Volumes may + still be undergoing filesystem probing after the device is published. } devVolCnt := 0; volCnt := driver.storage.vol.mgr.get_volume_count(); if volCnt > 0 then for i := 0 to volCnt - 1 do begin vol := driver.storage.vol.mgr.get_volume(i); if (vol <> nil) and (vol^.device = device) then begin devVolCnt := devVolCnt + 1; - usedSectors := usedSectors + vol^.sectorCount; end; end; - if device^.maxSectorCount > usedSectors then - freeBytes := uint64(device^.maxSectorCount - usedSectors) * device^.sectorSize - else - freeBytes := 0; - addInfoRow(detail, dw, 'Free Space', formatSizePrecise(freeBytes)); + freeBytes := uint64(driver.storage.vol.mgr.get_free_sector_count(device)) * device^.sectorSize; + addInfoRow(detail, dw, 'Unallocated Space', formatSizePrecise(freeBytes)); addInfoRow(detail, dw, 'Volumes', uiInt(devVolCnt)); { Usage bar } @@ -1352,7 +1348,8 @@ begin { Free space } freeB := uint64(vol^.freeSectors) * vol^.sectorSize; - freeStr := uiConcat(formatSizeStr(freeB), ' free'); + if (vol^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) <> 0 then freeStr := 'Free: unknown' + else freeStr := uiConcat(formatSizeStr(freeB), ' free'); lbl := lv_label_create(row); lv_label_set_text(lbl, freeStr); lv_obj_set_style_text_color(lbl, lv_color_make(100, 180, 130), 0); @@ -1494,6 +1491,10 @@ begin usedBytes := totalBytes - freeBytes; addInfoRow(detail, dw, 'Total Size', formatSizePrecise(totalBytes)); + if (vol^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) <> 0 then begin + addInfoRow(detail, dw, 'Free Space', 'Unknown'); + addInfoRow(detail, dw, 'Used Space', 'Unknown'); + end else begin addInfoRow(detail, dw, 'Free Space', formatSizePrecise(freeBytes)); addInfoRow(detail, dw, 'Used Space', formatSizePrecise(usedBytes)); @@ -1502,6 +1503,7 @@ begin addInfoRow(detail, dw, 'Usage', uiConcat(uiInt(pctUsed), '%')); addUsageBar(detail, dw, usedBytes, totalBytes); end; + end; addInfoRow(detail, dw, 'Boot Drive', boolStr(vol^.isBootDrive)); diff --git a/src/app/app.notepad.pas b/src/app/app.notepad.pas index 3a24563d..0a524c56 100644 --- a/src/app/app.notepad.pas +++ b/src/app/app.notepad.pas @@ -355,8 +355,10 @@ begin proc.mgr.kill(state^.pid); state^.pid := 0; end; - freeState(state); + if state^.ta <> nil then lv_group_remove_obj(state^.ta); + state^.ta := nil; driver.video.windows.destroyWindow(wid); + freeState(state); end; { ============================================================ @@ -440,7 +442,7 @@ begin written := driver.storage.vfs.WriteFile(fHandle, 0, puint8(buf), len); driver.storage.vfs.CloseFile(fHandle); kfree(void(buf)); - if written = 0 then begin + if written <> len then begin io.syslog.logln('NOTEPAD', 'saveFile: WriteFile returned 0'); showErrorMsgbox('Save Failed', 'File write failed. Check filesystem.'); updateStatusBar(state); diff --git a/src/app/app.volcmd.pas b/src/app/app.volcmd.pas index 6e3196b2..aa7983e0 100644 --- a/src/app/app.volcmd.pas +++ b/src/app/app.volcmd.pas @@ -179,7 +179,9 @@ begin print('Size: '); printsize(vol^.sectorCount * vol^.sectorSize); println(''); - print('Free: '); printsize(vol^.freeSectors * vol^.sectorSize); println(''); + print('Free: '); + if (vol^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) <> 0 then println('Unknown') + else begin printsize(vol^.freeSectors * vol^.sectorSize); println(''); end; print('Boot Drive: '); if vol^.isBootDrive then println('yes') else println('no'); diff --git a/src/driver/storage/ctl/driver.storage.ctl.usb.pas b/src/driver/storage/ctl/driver.storage.ctl.usb.pas index e229e651..c02e00f9 100644 --- a/src/driver/storage/ctl/driver.storage.ctl.usb.pas +++ b/src/driver/storage/ctl/driver.storage.ctl.usb.pas @@ -66,6 +66,7 @@ type Request : PIORequest; Tag : uint32; Offline, Ready, InFlight, Attaching, VolumesDetached : boolean; + SyncUnsupported : boolean; end; var @@ -276,6 +277,9 @@ begin if transport(c, actual) <> 0 then exit; if actual < 14 then exit; if (puint8(c^.Buffer)[0] and $7E) <> $70 then exit; + io.syslog.log('USBMSC', 'SCSI sense key='); io.syslog.writeint(puint8(c^.Buffer)[2] and $0F); + io.syslog.writestring(' ASC='); io.syslog.writeint(puint8(c^.Buffer)[12]); + io.syslog.writestring(' ASCQ='); io.syslog.writeintln(puint8(c^.Buffer)[13]); case puint8(c^.Buffer)[2] and $0F of 2: sense := eDeviceNotReady; 3: sense := eBadSector; @@ -286,14 +290,36 @@ begin end; function execute(c : PMSDevice; required : uint32) : TError; -var actual : uint32; resultCode : uint8; +var actual : uint32; resultCode, opcode : uint8; begin + opcode := c^.CBW.Command[0]; resultCode := transport(c, actual); execute := eIOError; if resultCode = 1 then execute := sense(c) else if (resultCode = 0) and (actual = required) then execute := eNone; if c^.Offline then execute := eIOTimeout; if c^.Original = nil then execute := eDeviceRemoved; + if execute <> eNone then begin + io.syslog.log('USBMSC', 'Command failed: opcode='); io.syslog.writeint(opcode); + io.syslog.writestring(' error='); io.syslog.writeintln(ord(execute)); + end; +end; + +function sync_cache(c : PMSDevice) : TError; +begin + sync_cache := eNone; + if c^.SyncUnsupported then exit; + command(c, $35, 10, 0, false); + sync_cache := execute(c, 0); + { Some removable flash devices reject SYNCHRONIZE CACHE with ILLEGAL + REQUEST despite accepting WRITE(10). This optional command must not turn + an acknowledged data write into a zero-byte failure. Other errors remain + failures; do not retry or suppress an unsuccessful WRITE(10). } + if sync_cache = eNotSupported then begin + c^.SyncUnsupported := true; + io.syslog.logln('USBMSC', 'SYNCHRONIZE CACHE unsupported; using device write completion.'); + sync_cache := eNone; + end; end; function probe(c : PMSDevice; var sectors, sectorSize : uint32) : boolean; @@ -393,10 +419,10 @@ begin inc(doneBlocks, blocks); inc(offset, bytes); end; end; - { Complete writes only after the device has committed its cache. } + { Flush devices which support it; unsupported removable media use the + WRITE command completion contract. } if (error = eNone) and (r^.RequestType = ioWrite) then begin - command(c, $35, 10, 0, false); - error := execute(c, 0); + error := sync_cache(c); end; if error = eReadOnly then c^.Disk^.writable := false; r^.ByteCount := offset; @@ -597,6 +623,7 @@ type Request : TIORequest; C : TMSDevice; Stage, Fault, Controls, DataBytes : uint32; + SenseKey : uint8; GoodWrite, GoodRecovery : boolean; end; @@ -629,7 +656,8 @@ begin case m^.C.CBW.Command[0] of $12: puint8(t^.Buffer)[0] := 0; $25: begin put32(puint8(t^.Buffer), 8191); put32(puint8(uint32(t^.Buffer)+4), 512); end; - $03: begin memset(uint32(t^.Buffer),0,t^.BufferLen); puint8(t^.Buffer)[0]:=$70; puint8(t^.Buffer)[2]:=7; end; + $03: begin memset(uint32(t^.Buffer),0,t^.BufferLen); puint8(t^.Buffer)[0]:=$70; + if m^.SenseKey = 0 then puint8(t^.Buffer)[2]:=7 else puint8(t^.Buffer)[2]:=m^.SenseKey; end; end; end else begin m^.GoodWrite := true; @@ -726,6 +754,16 @@ begin Assert(transport(@m^.C,actual) = 2, 'short CSW rejected'); m^.Fault := 0; command(@m^.C, $35, 10, 0, false); Assert(execute(@m^.C,0) = eNone, 'cache synchronization without data stage'); + m^.Fault := 3; m^.SenseKey := 3; + Assert(sync_cache(@m^.C) = eBadSector, 'flush media errors propagate'); + Assert(not m^.C.SyncUnsupported, 'media error does not disable flush'); + m^.Fault := 3; m^.SenseKey := 5; + Assert(sync_cache(@m^.C) = eNone, 'unsupported flush accepts completed write'); + sectors := m^.C.Tag; + Assert((sync_cache(@m^.C) = eNone) and (m^.C.Tag = sectors), 'unsupported flush is not repeatedly submitted'); + m^.Fault := 3; command(@m^.C, $2A, 10, 512, false); + Assert(execute(@m^.C,512) = eNotSupported, 'unsupported data write remains a failure'); + m^.Fault := 0; m^.SenseKey := 0; m^.Disk.sectorSize := 512; m^.Disk.maxSectorCount := 8192; m^.Request.Buffer := m^.C.Buffer; m^.Request.LBA := 8191; m^.Request.SectorCount := 1; Assert(valid_request(@m^.Disk,@m^.Request), 'last sector valid'); diff --git a/src/driver/storage/driver.storage.metatest.pas b/src/driver/storage/driver.storage.metatest.pas index bd09ffe5..0b7a4ad8 100644 --- a/src/driver/storage/driver.storage.metatest.pas +++ b/src/driver/storage/driver.storage.metatest.pas @@ -15,6 +15,7 @@ type TMock = record Data : puint8; Writes, FailWrite, Callbacks, Depth, MaxDepth : uint32; + MaxReadSectors, FailReadLBA : uint32; Error : TError; OutOfBounds, FailRead : boolean; end; @@ -27,7 +28,10 @@ begin (request^.SectorCount > device^.maxSectorCount-request^.LBA) then begin m^.OutOfBounds := true; complete_io(request, false, eInvalidArgument); exit; end; - if (request^.RequestType <> ioWrite) and m^.FailRead then begin + if request^.RequestType <> ioWrite then + if request^.SectorCount > m^.MaxReadSectors then m^.MaxReadSectors := request^.SectorCount; + if (request^.RequestType <> ioWrite) and (m^.FailRead or + ((m^.FailReadLBA <> 0) and (request^.LBA = m^.FailReadLBA))) then begin Inc(m^.Depth); if m^.Depth > m^.MaxDepth then m^.MaxDepth := m^.Depth; complete_io(request,false,eIOError); Dec(m^.Depth); exit; end; @@ -197,6 +201,53 @@ begin mock^.FailWrite := 0; Assert(remove_partition(device,0) = eNone,'delete commits'); Assert(get_volume_count() = oldCount,'delete detaches volume after write'); + { More than eight FAT sectors, with a partial final batch and FAT padding. } + kfree(void(mock^.Data)); + mock^.Data := puint8(kalloc(4096*512)); + if mock^.Data <> nil then begin + memset(uint32(mock^.Data),0,1500*512); + device^.maxSectorCount := 1500; + volume^.sectorStart := 0; volume^.sectorCount := 1500; + Assert(create_volume(volume,1500,0,@params) = eNone,'format multi-batch FAT fixture'); + mock^.MaxReadSectors := 0; + Assert(identify_volume(volume),'identify multi-batch FAT'); + Assert(volume^.freeSectors = 1454,'batched count excludes padding and allocated clusters'); + Assert(mock^.MaxReadSectors = 8,'FAT scan batches eight sectors'); + mock^.FailReadLBA := 40; + Assert(not identify_volume(volume),'failure in final FAT batch rejects probe'); + mock^.FailReadLBA := 0; + Assert(not mock^.OutOfBounds,'batched scan stays within device'); + device^.maxSectorCount := 4096; volume^.sectorCount := 4096; + Assert(create_volume(volume,4096,0,@params) = eNone,'format large FAT fixture'); + mock^.FailReadLBA := 32; mock^.MaxReadSectors := 0; + Assert(identify_volume(volume),'large FAT mounts without reading allocation table'); + Assert((volume^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) <> 0,'missing FSInfo reports unknown free space'); + Assert(mock^.MaxReadSectors = 1,'large volume discovery uses only metadata sector reads'); + PBootRecord(mock^.Data)^.FSInfoCluster := 1; + puint32(uint32(mock^.Data)+512)^ := $41615252; + puint32(uint32(mock^.Data)+512+484)^ := $61417272; + puint32(uint32(mock^.Data)+512+488)^ := 100; + puint32(uint32(mock^.Data)+512+508)^ := $AA550000; + Assert(identify_volume(volume),'valid FSInfo avoids FAT scan'); + Assert((volume^.freeSectors = 100) and + ((volume^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) = 0),'FSInfo count displayed'); + puint32(uint32(mock^.Data)+512+488)^ := $FFFFFFFF; + Assert(identify_volume(volume),'unknown FSInfo count does not prevent mount'); + Assert((volume^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) <> 0,'unknown FSInfo count stays unknown'); + puint32(uint32(mock^.Data)+512+488)^ := 4096; + Assert(identify_volume(volume),'out-of-range FSInfo count does not prevent mount'); + Assert((volume^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) <> 0,'out-of-range FSInfo count rejected'); + puint32(uint32(mock^.Data)+512+488)^ := 0; + Assert(identify_volume(volume),'full volume FSInfo accepted'); + Assert((volume^.freeSectors = 0) and + ((volume^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) = 0),'known zero differs from unknown'); + puint32(uint32(mock^.Data)+512+508)^ := 0; + Assert(identify_volume(volume),'invalid FSInfo signature does not prevent mount'); + Assert((volume^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) <> 0,'invalid FSInfo signature rejected'); + mock^.FailReadLBA := 1; + Assert(identify_volume(volume),'optional FSInfo read failure does not prevent mount'); + mock^.FailReadLBA := 0; + end else Assert(false,'allocate multi-batch FAT fixture'); kfree(device^.cachedMBR); CFIFO_Free(device^.requestQueue); kfree(void(mock^.Data)); kfree(void(mock)); kfree(void(volume)); kfree(void(device)); kfree(void(br)); io.syslog.log('STORMETA','Passed: '); io.syslog.writeint(passed); diff --git a/src/driver/storage/driver.storage.types.pas b/src/driver/storage/driver.storage.types.pas index 134c9345..283b07e3 100644 --- a/src/driver/storage/driver.storage.types.pas +++ b/src/driver/storage/driver.storage.types.pas @@ -357,6 +357,7 @@ const STORAGE_VOLUME_FLAG_FORMATTING = 2; STORAGE_VOLUME_FLAG_INVALIDATING = 4; STORAGE_VOLUME_FLAG_REMOVED = 8; + STORAGE_VOLUME_FLAG_FREE_UNKNOWN = 16; implementation diff --git a/src/driver/storage/driver.storage.vfs.pas b/src/driver/storage/driver.storage.vfs.pas index e819cbc6..54a8682a 100644 --- a/src/driver/storage/driver.storage.vfs.pas +++ b/src/driver/storage/driver.storage.vfs.pas @@ -1501,6 +1501,10 @@ begin asm popf end; while puint32(@wait.Done)^ = 0 do asm hlt end; if wait.Error = eNone then TransferFileSync := wait.BytesDone; + if Writing and (wait.Error <> eNone) then begin + io.syslog.log('VFS', 'Write failed: error='); io.syslog.writeint(ord(wait.Error)); + io.syslog.writestring(' bytes='); io.syslog.writeintln(wait.BytesDone); + end; end; function WriteFile(FileHandle : TFileHandle; Position : uint32; Buffer : puint8; Length : uint32) : uint32; @@ -3288,6 +3292,22 @@ begin debug.tracer.push_trace('driver.storage.vfs.VFS_COMMAND_UMOUNT.exit'); end; +{ Installed at the boot mount phase, after the VFS is ready. Registration + happens in the discovery task (usb.attach for USB), not an IRQ or UI callback. + Mounting only creates VFS objects; filesystem probing has already completed. } +var autoMountWatch : uint32 = 0; + +procedure mount_added_volume(event : TStorageLifecycleEvent; device : PStorage_Device; + volume : PStorage_Volume; error : TError; userdata : pointer); +begin + if (event <> sleVolumeAdded) or (error <> eNone) then exit; + if (device = nil) or (volume = nil) then exit; + if device^.removed or (volume^.filesystem = nil) then exit; + if driver.storage.vol.mgr.volume_is_busy(volume) then exit; + if mountDefaultVolume(volume) <> eNone then + io.syslog.logln('VFS','Could not automatically mount newly discovered volume.'); +end; + { Auto-mount all discovered volumes into /disk/ and honour persistent mounts } procedure auto_mount_volumes(); @@ -3312,6 +3332,11 @@ var begin debug.tracer.push_trace('driver.storage.vfs.auto_mount_volumes.enter'); + { Subscribe even when no disks were present at boot. Avoid duplicate watches + if the boot mount pass is invoked again. } + if autoMountWatch = 0 then + autoMountWatch := driver.storage.vol.mgr.watch_lifecycle(@mount_added_volume, nil); + volCount := driver.storage.vol.mgr.get_volume_count(); if volCount = 0 then begin debug.tracer.push_trace('driver.storage.vfs.auto_mount_volumes.exit'); @@ -3658,7 +3683,25 @@ begin obj := GetObjectFromPath('/disk/__RENAMED_TEST__'); Assert((obj <> nil) and (obj^.Reference = void(mountVolA)),'renamed mount points to same volume'); InvalidateVolume(mountVolA); InvalidateVolume(mountVolB); - kfree(void(mountVolA)); kfree(void(mountVolB)); kfree(void(mountFS)); + { Exercise real registration notifications after the boot mount pass. } + mountDevice := PStorage_Device(kalloc(sizeof(TStorage_Device))); + memset(uint32(mountDevice),0,sizeof(TStorage_Device)); + mountDevice^.writable := true; + mountVolA^.device := mountDevice; mountVolB^.device := mountDevice; + memcpy(uint32(pchar('__HOTPLUG_TEST__')),uint32(@mountVolA^.labelText[0]),17); + mountVolB^.labelText := mountVolA^.labelText; + mountVolA^.filesystem := nil; + driver.storage.vol.mgr.register_volume(mountDevice,mountVolA); + Assert(GetObjectFromPath('/disk/__HOTPLUG_TEST__') = nil,'unrecognized hotplug volume is not mounted'); + mountVolB^.filesystem := mountFS; + driver.storage.vol.mgr.register_volume(mountDevice,mountVolB); + obj := GetObjectFromPath('/disk/__HOTPLUG_TEST__'); + Assert((obj <> nil) and (obj^.Reference = void(mountVolB)),'registration automatically mounts recognized volume'); + Assert((mountVolB^.lifecycleFlags and STORAGE_VOLUME_FLAG_MOUNTED) <> 0,'hotplug mount publishes mounted state'); + driver.storage.vol.mgr.delete_volume(mountVolA); + driver.storage.vol.mgr.delete_volume(mountVolB); + Assert(GetObjectFromPath('/disk/__HOTPLUG_TEST__') = nil,'hotplug removal clears mount'); + kfree(void(mountDevice)); kfree(void(mountFS)); watchFired := 0; io.syslog.logln('VFS', 'Unit tests starting...'); diff --git a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.vol.pas b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.vol.pas index 4c1021df..89e01cc1 100644 --- a/src/driver/storage/fs/fat32/driver.storage.fs.fat32.vol.pas +++ b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.vol.pas @@ -62,24 +62,47 @@ begin end; function countFreeFATClusters(volume : PStorage_Volume; br : PBootRecord) : uint32; -var limit, sectorIdx, entryIdx, clusterNum, count : uint32; buffer : puint32; +var limit, sectorIdx, entryIdx, clusterNum, count, sectors, batch : uint32; buffer : puint32; begin countFreeFATClusters := $FFFFFFFF; limit := FAT32ClusterLimit(br); if limit <= 2 then exit; - buffer := puint32(kalloc(512)); + volume^.lifecycleFlags := volume^.lifecycleFlags and (not STORAGE_VOLUME_FLAG_FREE_UNKNOWN); + { Match the USB storage transfer buffer: eight sectors per command instead + of a CBW/data/CSW round trip for every 512 bytes of a large FAT. } + buffer := puint32(kalloc(4096)); if buffer = nil then exit; + { FSInfo is optional, and its count is advisory. Validate every signature, + the reserved-sector location, and the count before displaying it. } + if (br^.FSInfoCluster > 0) and (br^.FSInfoCluster < br^.rsvSectors) then begin + if storage_read(volume^.device, volume^.sectorStart + br^.FSInfoCluster, 1, buffer) = eNone then + if (buffer[0] = $41615252) and (buffer[121] = $61417272) and + (buffer[127] = $AA550000) and (buffer[122] <= limit - 2) then begin + countFreeFATClusters := buffer[122]; kfree(buffer); exit; + end; + end; count := 0; sectorIdx := 0; - while sectorIdx * 128 < limit do begin - if storage_read(volume^.device, volume^.sectorStart + br^.rsvSectors + sectorIdx, 1, buffer) <> eNone then begin + sectors := (limit + 127) div 128; + { Mounting must not wait for an O(disk-size) allocation scan. Small FATs + remain cheap to count; larger volumes without FSInfo mount with unknown + free space. Allocation always consults the actual FAT, never this hint. } + if sectors > 16 then begin + volume^.lifecycleFlags := volume^.lifecycleFlags or STORAGE_VOLUME_FLAG_FREE_UNKNOWN; + countFreeFATClusters := 0; + kfree(buffer); exit; + end; + while sectorIdx < sectors do begin + batch := sectors - sectorIdx; + if batch > 8 then batch := 8; + if storage_read(volume^.device, volume^.sectorStart + br^.rsvSectors + sectorIdx, batch, buffer) <> eNone then begin kfree(buffer); exit; end; - for entryIdx := 0 to 127 do begin + for entryIdx := 0 to batch * 128 - 1 do begin clusterNum := sectorIdx * 128 + entryIdx; if clusterNum >= limit then break; if (clusterNum >= 2) and ((buffer[entryIdx] and $0FFFFFFF) = 0) then Inc(count); end; - Inc(sectorIdx); + Inc(sectorIdx, batch); end; kfree(buffer); countFreeFATClusters := count; -- 2.54.0 From 10691a9e03bf0519d6941f900755d083be6a29bf Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 7 Nov 2026 10:18:33 +0000 Subject: [PATCH 20/21] fix file headers --- src/driver/bus/usb/driver.bus.usb.core.pas | 2 +- src/driver/bus/usb/driver.bus.usb.ehci.pas | 2 +- src/driver/bus/usb/driver.bus.usb.ohci.pas | 2 +- src/driver/bus/usb/driver.bus.usb.types.pas | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/driver/bus/usb/driver.bus.usb.core.pas b/src/driver/bus/usb/driver.bus.usb.core.pas index 3009b020..60ff1031 100644 --- a/src/driver/bus/usb/driver.bus.usb.core.pas +++ b/src/driver/bus/usb/driver.bus.usb.core.pas @@ -1,4 +1,4 @@ -// Copyright 2026 Aaron Hance +// Copyright 2026 Kieron Morris // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/driver/bus/usb/driver.bus.usb.ehci.pas b/src/driver/bus/usb/driver.bus.usb.ehci.pas index bc1173a2..880ab29b 100644 --- a/src/driver/bus/usb/driver.bus.usb.ehci.pas +++ b/src/driver/bus/usb/driver.bus.usb.ehci.pas @@ -1,4 +1,4 @@ -// Copyright 2026 Aaron Hance +// Copyright 2026 Kieron Morris // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/driver/bus/usb/driver.bus.usb.ohci.pas b/src/driver/bus/usb/driver.bus.usb.ohci.pas index 3fec817a..528d2623 100644 --- a/src/driver/bus/usb/driver.bus.usb.ohci.pas +++ b/src/driver/bus/usb/driver.bus.usb.ohci.pas @@ -1,4 +1,4 @@ -// Copyright 2026 Aaron Hance +// Copyright 2026 Kieron Morris // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/driver/bus/usb/driver.bus.usb.types.pas b/src/driver/bus/usb/driver.bus.usb.types.pas index a6f38572..cc54b8e2 100644 --- a/src/driver/bus/usb/driver.bus.usb.types.pas +++ b/src/driver/bus/usb/driver.bus.usb.types.pas @@ -1,4 +1,4 @@ -// Copyright 2026 Aaron Hance +// Copyright 2026 Kieron Morris // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. -- 2.54.0 From e8e40615641f32610b886be0d35577bc9ce985cf Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 7 Nov 2026 10:19:01 +0000 Subject: [PATCH 21/21] fix file header --- src/proc/proc.mgr.pas | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/proc/proc.mgr.pas b/src/proc/proc.mgr.pas index f7a61113..ad933bd0 100644 --- a/src/proc/proc.mgr.pas +++ b/src/proc/proc.mgr.pas @@ -1,4 +1,4 @@ -// Copyright 2026 Aaron Hance +// Copyright 2026 Kieron Morris // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. -- 2.54.0