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/doc/src/driver/storage/ctl/driver.storage.ctl.usb.md b/doc/src/driver/storage/ctl/driver.storage.ctl.usb.md index a9a95ab9..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,49 +1,85 @@ -# driver.storage.ctl.usb + -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. +# USB mass storage -## Boot Registration +`driver.storage.ctl.usb` implements SCSI transparent commands over USB Mass +Storage Bulk-Only Transport (interface class/subclass/protocol `08/06/50`). -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. +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. -## Dependencies +## Execution and ownership -- `boot.mgr` +An attachment task probes and registers each disk after scheduling starts; boot +initialization never waits on a worker that cannot run yet. -- `driver.mgr` -- `memory.heap` -- `driver.storage.types` -- `io.syslog` -- `debug.tracer` -- `driver.bus.usb.core` -- `driver.bus.usb.types` -- `core.util`, `arch.x86.util` +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. -## Constants +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. -| 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 | +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. -## Functions and Procedures +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. -### init +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. -```pascal -procedure init; -``` +## Supported scope -Constructs a `TDeviceIdentifier` matching any USB device with `bInterfaceClass = $08`, `bInterfaceSubClass = $06`, `bInterfaceProtocol = $50`, and registers the `load` callback with `driver.mgr`. +- 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. -## Notes +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. -- 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. +## Validation + +`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. + +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. + +Protocol: [USB-IF Bulk-Only Transport 1.0](https://www.usb.org/sites/default/files/usbmassbulk_10.pdf). diff --git a/doc/src/driver/storage/driver.storage.mgr.md b/doc/src/driver/storage/driver.storage.mgr.md index 7b3c84cf..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. @@ -14,7 +31,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,17 +222,42 @@ 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`. - 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/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/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/app/app.diskutil.pas b/src/app/app.diskutil.pas index fe62b997..df6d4202 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 @@ -32,7 +33,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; @@ -58,10 +60,33 @@ 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; + label_target_id, label_target_index : uint32; + fmt_param_error_lbl: Plv_obj; add_textarea : Plv_obj; 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; + 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; { ============================================================ Forward declarations @@ -73,45 +98,91 @@ 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; +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 ============================================================ } -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; @@ -125,6 +196,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 @@ -246,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; @@ -282,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 @@ -360,6 +435,84 @@ begin add_textarea := nil; add_err_lbl := nil; fmt_dropdown := nil; + fmt_param_area := nil; + fmt_cluster_input := nil; + fmt_filecount_input := nil; + fmt_label_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; + 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 + 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; + 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; +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; { ============================================================ @@ -377,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; @@ -431,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 @@ -453,12 +606,11 @@ 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); - - 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; @@ -473,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; @@ -518,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 @@ -526,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); @@ -552,12 +698,98 @@ 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). } -procedure fmt_done_cb(error : TError; userdata : pointer); + Only sets completion flags; LVGL work happens later on the UI poll timer. } +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; + +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 + releaseTextTemps; + 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(sel_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, '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_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, '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; @@ -566,22 +798,115 @@ var selIdx : uint32; fs : PFilesystem; vol : PStorage_Volume; + cbCtx : PFmtUICallbackCtx; + params : TFSFormatParams; + 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; 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_obj_add_event_cb(lv_msgbox_add_footer_button(active_mbox, 'OK'), @add_part_cancel_cb, LV_EVENT_CLICKED, nil); + 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 (stringSize(text) > 5) or (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 (stringSize(text) > 5) or (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; + + 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; - { 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 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, + @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, '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.'); + lv_msgbox_add_text(active_mbox, 'The volume will remount automatically when complete.'); + end; end; procedure fmt_cancel_cb(e: Plv_event); cdecl; @@ -668,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:'); @@ -678,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; @@ -709,6 +1034,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 @@ -739,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 } @@ -750,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; @@ -760,6 +1094,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); @@ -775,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 } @@ -787,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 @@ -811,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); @@ -830,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); @@ -854,8 +1199,9 @@ 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; + 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); @@ -894,9 +1240,8 @@ var btn : Plv_obj; dw : sint32; i : uint32; - totalBytes : uint32; - usedSectors : uint32; - freeBytes : uint32; + totalBytes : uint64; + freeBytes : uint64; volCnt : uint32; devVolCnt : uint32; vol : PStorage_Volume; @@ -905,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; @@ -922,30 +1267,26 @@ 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; + { 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(); - 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; - usedSectors := usedSectors + vol^.sectorCount; end; end; - if device^.maxSectorCount > usedSectors then - freeBytes := (device^.maxSectorCount - usedSectors) * device^.sectorSize - else - freeBytes := 0; - addInfoRow(detail, dw, 'Free Space', formatSizePrecise(freeBytes)); - addInfoRow(detail, dw, 'Volumes', intToString(devVolCnt)); + 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 } if totalBytes > 0 then @@ -959,7 +1300,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; @@ -983,20 +1324,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); @@ -1005,8 +1347,9 @@ 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; + 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); @@ -1038,13 +1381,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; @@ -1065,17 +1460,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 @@ -1083,23 +1481,29 @@ 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)); + 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)); 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; + end; addInfoRow(detail, dw, 'Boot Drive', boolStr(vol^.isBootDrive)); @@ -1116,8 +1520,16 @@ 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); + 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); end; @@ -1177,8 +1589,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); @@ -1187,12 +1599,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); @@ -1213,7 +1625,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); @@ -1227,7 +1639,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; @@ -1246,14 +1658,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); @@ -1292,6 +1706,19 @@ 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; + 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; @@ -1390,15 +1817,28 @@ 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; pending_del_slot := -1; active_mbox := nil; fmt_dropdown := nil; + 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; 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); + 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; @@ -1415,6 +1855,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'); @@ -1428,9 +1893,21 @@ begin pending_del_slot := -1; active_mbox := nil; fmt_dropdown := nil; + 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; active_sidebar_btn := nil; + fmt_poll_timer := nil; + 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; end; diff --git a/src/app/app.filebrowser.pas b/src/app/app.filebrowser.pas index bca299c0..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 } @@ -1860,15 +1910,20 @@ 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); - 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 new file mode 100644 index 00000000..4156a96d --- /dev/null +++ b/src/app/app.iotest.pas @@ -0,0 +1,609 @@ +{ + App->IOTest - Write and read-back a file to exercise offset-based I/O. + + Usage: IOTEST [size] [path] + Default size: 25 MB + Default path: /disk/vol0/IOTEST.BIN + + 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 ) +} +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, + proc.mgr, proc.types; + +const + 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/vol2/IOTEST.BIN'; + +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 + 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; + +{ 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 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; + fileSize : uint32; + ok : boolean; + i : uint32; + t0, t1 : uint32; + elapsed : uint32; + kbps : uint32; + 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 + fileSize := DEFAULT_FILE_SIZE; + path := DEFAULT_PATH; + sizeParam := getParam(0, Params); + pathParam := getParam(1, Params); + + 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 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 '); + 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.bufWriteStr(stderr_buf, 'ERROR: Could not create file: '); + writeErrorName(stderr_buf, fErr); + io.stdio.bufWriteNewLine(stderr_buf); + kfree(puint32(buf)); + kfree(puint32(verBuf)); + exit; + end; + + t0 := arch.x86.bda.Counters.c32; + tLast := t0; + bytesInSec := 0; + ioTicks := 0; + prepTicks := 0; + ioTicksInSec := 0; + offset := 0; + ok := true; + 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.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 + 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; + 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 := ioTicks; + io.stdio.bufWriteStr(stdout_buf, 'Write I/O complete: '); + if elapsed > 0 then begin + 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 + 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.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; + end; + + t0 := arch.x86.bda.Counters.c32; + tLast := t0; + bytesInSec := 0; + ioTicks := 0; + verifyTicks := 0; + ioTicksInSec := 0; + offset := 0; + ok := true; + 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; + + { 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); + 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; + 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; + 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 := ioTicks; + io.stdio.bufWriteStr(stdout_buf, 'Read I/O complete: '); + if elapsed > 0 then begin + 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, '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 ==================== } + 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)); +end; + +procedure init(); +begin + debug.tracer.push_trace('iotest.init'); + io.stdio.registerCommand('IOTEST', @run, 'Write and read-verify a file. Usage: IOTEST [size] [path]'); +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/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.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/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/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/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/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/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/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/bus/usb/driver.bus.usb.core.pas b/src/driver/bus/usb/driver.bus.usb.core.pas index 1ce08a85..60ff1031 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 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->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; @@ -658,6 +673,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 +683,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 +748,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 +769,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 +807,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..880ab29b 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 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->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; @@ -963,6 +978,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 +1021,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 +1038,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 +1051,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 +1070,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 +1088,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 +1170,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 +1182,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 +1219,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 +1316,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 +1409,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 +1448,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..528d2623 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 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->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; @@ -678,6 +693,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 +730,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 +749,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 +769,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 +791,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 +812,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 +843,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 +912,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 +947,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 +956,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 +1108,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 +1179,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 +1468,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 +1502,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..cc54b8e2 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 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->Bus->driver.bus.usb->USBTypes - Shared driver.bus.usb Types, Constants & HC Abstraction. + @author(Aaron Hance ) @author(Kieron Morris ) } unit driver.bus.usb.types; @@ -249,6 +264,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/ahci/driver.storage.ctl.ahci.pas b/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas index 8468e214..fd1fff29 100644 --- a/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas +++ b/src/driver/storage/ctl/ahci/driver.storage.ctl.ahci.pas @@ -43,19 +43,20 @@ var ahciControllers : PDList; page_base : puint32; +procedure UnitTest; 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; { 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 +69,29 @@ 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 + if a < b then + min_u32 := a + else + min_u32 := b; +end; + procedure init(); var devID : TDeviceIdentifier; @@ -81,124 +105,67 @@ 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; -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 + 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; + +function stop_port(port : PHBA_Port) : boolean; 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) + stop_port := wait_register(@port^.cmd,$8000,0,250); + if not stop_port then exit; 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; + stop_port := wait_register(@port^.cmd,$4000,0,250); end; -procedure start_port(port : PHBA_Port); +function start_port(port : PHBA_Port) : boolean; begin - // Wait for CR (bit 15) to clear before starting - while (port^.cmd and $8000) <> 0 do begin - end; - - // Enable FRE (bit 4) first so port can receive FIS + 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 @@ -206,7 +173,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; @@ -215,35 +184,25 @@ 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; - //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); @@ -254,7 +213,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 @@ -268,7 +228,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); @@ -279,18 +240,37 @@ 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; - //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 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 + 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 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; + end; + log_port_failure(i,port,'device unavailable after bounded retries'); end; controller^.mio^.int_status := $FFFFFFFF; @@ -298,7 +278,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; @@ -312,8 +292,16 @@ var b8 : puint16; storageDev : PStorage_Device; storageDev_sectorSize : uint32; + ncqDepth : uint32; + 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; + device^.supportsNCQ := false; + device^.queueDepth := 1; //clear any pending interrupts device^.port^.int_status := $FFFFFFFF; @@ -329,9 +317,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) @@ -357,69 +343,38 @@ 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 := $40000003; + 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'); + if not stop_port(device^.port) then fail_controller(controller,eIOTimeout); kfree(buffer); exit; end; + device^.port^.int_status := $FFFFFFFF; { 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 := 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 and ((controller^.mio^.capabilites and (1 shl 30)) <> 0); + 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); - 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 @@ -442,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)); @@ -450,20 +406,24 @@ 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 := device^.queueDepth; 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)'); @@ -474,8 +434,7 @@ begin io.syslog.writestringln(': sector count is 0, skipping registration.'); end; - { Free the write-test buffer allocated above } - kfree(buffer); + end; @@ -516,10 +475,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); @@ -528,7 +487,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); @@ -540,40 +501,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); @@ -600,416 +544,294 @@ begin base^.int_status := $FFFFFFFF; check_ports(controller); + load := not controller^.offline; +end; + +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 + 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; + end; +end; + +procedure fail_controller(controller : PAHCI_Controller; error : TError); +var j, command : uint32; pci : PPCI_Device; device : PAHCI_Device; +begin + 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; + 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; procedure ahci_isr(); -var - i : uint32; - j : uint32; - k : uint32; - ctrl_is : uint32; - port_is : uint32; - port : PHBA_Port; - controller : PAHCI_Controller; - device : PAHCI_Device; - success : boolean; +var i,j,ctrlIS,portIS : uint32; controller : PAHCI_Controller; device : PAHCI_Device; 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 } - 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); - end; - end; - end; - - { Acknowledge port interrupt by writing back the read value } - port^.int_status := port_is; + 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; - - { Acknowledge controller interrupt } - controller^.mio^.int_status := ctrl_is; end; end; +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 - i : uint32; - cmd_issue : uint32; +var flags, slot, active, count : 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; - end; - end; - { All 32 slots busy } find_cmd_slot := $FFFFFFFF; -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); -begin - userdata^ := 1; -end; - -{ ---- Async send functions ---- } - -function send_read_dma_async(device : PAHCI_Device; lba : uint64; count : uint32; buffer : puint32; completion : TIOCompletion; userdata : puint32) : boolean; -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 := false; - - 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 } - - 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^.command := ATA_CMD_READ_DMA_EXT; - 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; - fis^.countl := sec_count and $FF; - fis^.counth := (sec_count shr 8) 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 read)'); - exit; + 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; - - { 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; - - { Issue command \u2014 returns immediately } - device^.port^.cmd_issue := device^.port^.cmd_issue or (1 shl slot); - - send_read_dma_async := true; + if (flags and $200) <> 0 then asm sti end; end; - -function send_write_dma_async(device : PAHCI_Device; lba : uint64; count : uint32; buffer : puint32; completion : TIOCompletion; userdata : puint32) : boolean; -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; +{ 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 - send_write_dma_async := false; - - 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 } - - 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; + 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; - 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_WRITE_DMA_EXT; - 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; - fis^.countl := sec_count and $FF; - fis^.counth := (sec_count shr 8) 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 write)'); - exit; +{ 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; - - { 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); - - send_write_dma_async := true; -end; - -//read atapi -function read_atapi_async(device : PAHCI_Device; lba : uint64; count : uint32; buffer : puint32; completion : TIOCompletion; userdata : puint32) : boolean; -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 := false; - - prdt_count := (count div 4194304) + 1; - if prdt_count > 32 then exit; - - slot := find_cmd_slot(device); - if slot = $FFFFFFFF then exit; - - 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)'); - exit; - 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; - - { 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 := true; + 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; -{ 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_dma_async(device : PAHCI_Device; lba : uint64; count : uint32; buffer : puint32; completion : TIOCompletion; userdata : puint32) : TError; +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; +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; +begin read_atapi_async := submit_command(device,lba,count,buffer,2,completion,userdata); end; + +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; + 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); - 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; + 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; + 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 ---------- } @@ -1020,60 +842,188 @@ 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 } procedure ahci_dispatch_read(device : PStorage_Device; request : PIORequest); 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); - 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 + 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); - 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 + 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); - 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; + +{ 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.*'); 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..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,8 +270,16 @@ 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; + 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; @@ -285,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; @@ -335,4 +347,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 a7ebe107..fc50021b 100644 --- a/src/driver/storage/ctl/driver.storage.ctl.ram.pas +++ b/src/driver/storage/ctl/driver.storage.ctl.ram.pas @@ -82,38 +82,12 @@ 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; fileName : pchar; offset : uint32; - buffer : puint32; byteCount : uint32) : uint32; + buffer : puint32; byteCount : uint32; + ctx : pointer) : uint32; var clean : pchar; idx : sint32; @@ -133,6 +107,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 +257,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/ctl/driver.storage.ctl.usb.pas b/src/driver/storage/ctl/driver.storage.ctl.usb.pas index b7d694f5..c02e00f9 100644 --- a/src/driver/storage/ctl/driver.storage.ctl.usb.pas +++ b/src/driver/storage/ctl/driver.storage.ctl.usb.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. @@ -11,82 +12,794 @@ // 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 } { - 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 ) } 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; + SyncUnsupported : 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; + 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; + 5: sense := eNotSupported; + 6: sense := eDeviceNotReady; + 7: sense := eReadOnly; + end; +end; + +function execute(c : PMSDevice; required : uint32) : TError; +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; +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; + { Flush devices which support it; unsupported removable media use the + WRITE command completion contract. } + if (error = eNone) and (r^.RequestType = ioWrite) then begin + error := sync_cache(c); + 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; + SenseKey : uint8; + 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; + 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; + 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^.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'); + 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/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.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.fdtable.pas b/src/driver/storage/driver.storage.fdtable.pas index 91cfb76a..50430d40 100644 --- a/src/driver/storage/driver.storage.fdtable.pas +++ b/src/driver/storage/driver.storage.fdtable.pas @@ -37,17 +37,17 @@ 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 } 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 } + FSPrivate : pointer; { opaque FS-level per-file metadata (from openFileCallback) } + FSCloseHook : pointer; { procedure(pointer) — frees FSPrivate on close } end; PFDTable = ^TFDTable; @@ -71,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. @@ -81,6 +84,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; @@ -93,15 +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; - if entry^.DataBuffer <> nil then begin - kfree(entry^.DataBuffer); - entry^.DataBuffer := nil; - end; + { 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; @@ -116,9 +128,8 @@ begin end; entry^.InUse := false; - entry^.Loaded := false; + entry^.AsyncRefs := 0; entry^.DataSize := 0; - entry^.StreamOff := 0; entry^.DeviceOps := nil; entry^.DeviceData := nil; end; @@ -153,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; @@ -174,4 +213,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.iorequest.pas b/src/driver/storage/driver.storage.iorequest.pas index 7b8a8aed..293b2e11 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 - record, NOT the buffer it points to). Used by dispatch_next to create an - ISR-safe copy from the CFIFO value-element. } +{ Copy an existing TIORequest into a new pooled allocation (deep copy of the + 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 +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,11 +127,14 @@ 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; end; + req^.MergeNext := nil; + req^.OriginalSectors := sectors; + req^.QueuedTick := 0; req^.DispatchTick := 0; req^.RequestType := reqType; req^.State := iosPending; req^.Device := device; @@ -75,19 +152,21 @@ 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; 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.metatest.pas b/src/driver/storage/driver.storage.metatest.pas new file mode 100644 index 00000000..0b7a4ad8 --- /dev/null +++ b/src/driver/storage/driver.storage.metatest.pas @@ -0,0 +1,268 @@ +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, Depth, MaxDepth : uint32; + MaxReadSectors, FailReadLBA : 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 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; + 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),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; + 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'); + { 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); + 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 0ab06d60..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. @@ -19,14 +20,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 ) } @@ -35,6 +36,7 @@ unit driver.storage.mgr; interface uses + arch.x86.bda, boot.mgr, core.ds.cfifo, core.ds.types, @@ -59,6 +61,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; @@ -68,7 +71,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. } @@ -110,12 +113,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 @@ -135,6 +132,16 @@ 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; + Device : PStorage_Device; + 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 +159,64 @@ 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; + +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; +begin + ctx := PMBRWriteAsyncData(userdata); + if ctx = nil then exit; + 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; + +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'); @@ -184,9 +249,14 @@ begin { Initialise new Phase 2 fields } device^.removed := false; - device^.requestQueue := CFIFO_New(SizeOf(TIORequest), 32); - device^.activeRequest := nil; + 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; 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. } @@ -290,20 +360,21 @@ begin end; end; -procedure write_mbr(device : PStorage_Device; mbr : PMaster_Boot_Record); -var - cache : puint32; +function write_mbr(device : PStorage_Device; mbr : PMaster_Boot_Record) : TError; +var buf : puint32; begin - push_trace('driver.storage.mgr.write_mbr'); - if not device^.writable then exit; - { Update cache } - if device^.cachedMBR <> nil then - kfree(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); - storage_write(device, 0, 1, puint32(mbr)); + 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 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; @@ -314,22 +385,32 @@ end; procedure write_mbr_async(device : PStorage_Device; mbr : PMaster_Boot_Record; callback : TIOCallback; callbackData : pointer); -var - cache : puint32; +var ctx : PMBRWriteAsyncData; buf : puint32; err : TError; begin - push_trace('driver.storage.mgr.write_mbr_async'); - if not device^.writable then begin - if callback <> nil then callback(eReadOnly, 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; - { Update cache } - if device^.cachedMBR <> nil then - kfree(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); - storage_write_async(device, 0, 1, puint32(mbr), callback, callbackData); + 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(buf); device^.mbrWriteBusy := false; + if callback <> nil then callback(eOutOfMemory, callbackData); + exit; + end; + 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 mbr_write_async_callback(err, ctx); end; { ========================================================================== } @@ -340,46 +421,78 @@ end; Called with interrupts disabled (CLI context) or from ISR context. } procedure dispatch_next(device : PStorage_Device); var - reqBuf : TIORequest; - heapReq : PIORequest; + heapReq, nextReq, tail : PIORequest; + peek : pointer; + merged, bytes, ticks : uint32; begin - if device^.activeRequest <> nil then exit; { hardware busy } - if not CFIFO_Dequeue(device^.requestQueue, @reqBuf) then exit; { queue empty } + 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 } + if heapReq = nil then + break; - { 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; + { 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; + if device^.activeCount > device^.Stats.ActivePeak then device^.Stats.ActivePeak := device^.activeCount; - 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); + 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; - ioWrite: begin - if device^.dispatchWrite <> nil then - device^.dispatchWrite(device, heapReq) - else - complete_io(heapReq, false, eDeviceNotFound); - end; end; + device^.dispatching := false; end; function submit_io(request : PIORequest) : TError; var - device : PStorage_Device; + 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; @@ -399,11 +512,35 @@ begin request^.State := iosPending; request^.Error := eNone; - { Enqueue and attempt dispatch — must be ISR-safe } - asm pushf; cli end; - CFIFO_Enqueue(device^.requestQueue, @request^); + { 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 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); - asm popf end; + if (flags and $200) <> 0 then asm sti end; submit_io := eNone; end; @@ -457,35 +594,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; - - { Clear active slot on the device } - if device <> nil then - device^.activeRequest := nil; - - { 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; { ========================================================================== } @@ -598,17 +739,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.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.test.pas b/src/driver/storage/driver.storage.test.pas index efd3f656..b99a8a7f 100644 --- a/src/driver/storage/driver.storage.test.pas +++ b/src/driver/storage/driver.storage.test.pas @@ -46,7 +46,7 @@ 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; @@ -476,7 +476,7 @@ begin end; { ============================================================ - init — register the STORTEST and DISKTEST terminal commands + init — register the storage terminal commands ============================================================ } procedure init; begin diff --git a/src/driver/storage/driver.storage.types.pas b/src/driver/storage/driver.storage.types.pas index c630b1f6..283b07e3 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 } @@ -89,7 +105,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.) } @@ -126,6 +142,37 @@ type PDirectory_Entry = ^TDirectory_Entry; PDrive_Error = ^TDrive_Error; PIORequest = ^TIORequest; + PFSFormatParams = ^TFSFormatParams; + + TStorageLifecycleEvent = ( + sleVolumeAdded, + sleVolumeMounted, + sleVolumeUnmounted, + sleVolumeFormatStarted, + sleVolumeFormatCompleted, + sleVolumeInvalidating, + sleVolumeInvalidated, + sleVolumeRemoved, + sleDeviceAdded, + sleDeviceRemoved, + sleVolumeLabelChanged + ); + + TStorageLifecycleCallback = procedure(event : TStorageLifecycleEvent; + device : PStorage_Device; + volume : PStorage_Volume; + error : TError; + userdata : pointer); + + TVolumeLabel = array[0..32] of char; { cached, NUL-terminated display name } + + TFSFormatParams = record + Version : uint16; + Flags : uint16; + ClusterSize : uint32; + FileCount : uint32; + LabelText : array[0..11] of char; + end; { === Driver dispatch type (Phase 2+) === } @@ -134,45 +181,59 @@ 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 } - 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); + 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. - 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. } - 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; + { 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); { === 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); - PPDeleteDirAsyncHook = procedure(volume : PStorage_Volume; path : pchar; status : puint32; callback : TIOCallback; callbackData : pointer); { === I/O Request core.types === } @@ -193,11 +254,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; @@ -211,22 +283,27 @@ 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 } + 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; 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 } + Stats : TStorageStats; { cold tail: wrapping counters, timer ticks } end; { Filesystem driver descriptor } TFilesystem = record sName : pchar; system_id : uint8; - writeCallback : PPWriteHook; - readCallback : PPReadHook; createCallback : PPCreateHook; + validateFormat : PPCreateHook; { pure preflight, before invalidation } detectCallback : PPDetectHook; createDirCallback : PPCreateDirHook; readDirCallback : PPReadDirHook; @@ -237,17 +314,23 @@ 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; - { 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; - deleteDirAsyncCallback : PPDeleteDirAsyncHook; 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; + setLabelCallback : PPSetLabelHook; end; { Generic storage volume } @@ -260,23 +343,21 @@ type freeSectors : uint32; filesystem : PFilesystem; isBootDrive : boolean; + lifecycleFlags : uint32; + labelText : TVolumeLabel; + 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; + 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 26da5737..54a8682a 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. @@ -34,7 +35,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); @@ -89,7 +90,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); @@ -119,7 +120,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,12 +132,19 @@ 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. } +function VolumeHasOpenHandles(vol : PStorage_Volume) : boolean; +function InvalidateVolume(vol : PStorage_Volume) : boolean; + { VFS Functions } function newVirtualDirectory(Path : pchar) : TError; 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; @@ -145,6 +152,8 @@ implementation uses driver.storage.fs.mgr, + driver.storage.fs.fat32, + driver.storage.fs.ext2, proc.mgr, proc.types, io.stdio, @@ -164,18 +173,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) } @@ -185,6 +182,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; @@ -215,46 +249,56 @@ 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); +procedure vfs_write_complete(error : TError; userdata : pointer); var - ctx : PVFSOpenAsyncCtx; + ctx : PVFSWriteAsyncCtx; 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; + 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; - kfree(puint32(ctx^.DataBuf)); - kfree(puint32(ctx^.DataSize)); + 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; @@ -460,6 +504,131 @@ 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; + +function volumeHasHandles(vol : PStorage_Volume; asyncOnly : boolean) : boolean; +var + pCount : uint32; + pi : uint32; + fi : uint32; + pCtx : proc.types.PProcessContext; + tbl : PFDTable; +begin + volumeHasHandles := 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 ((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; + 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'); + InvalidateVolume := false; + if vol = nil then exit; + if volumeHasHandles(vol,true) then 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); + + { 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; + + 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'); +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; @@ -826,6 +995,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; @@ -993,6 +1163,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))); @@ -1004,12 +1183,67 @@ 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); 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. } @@ -1058,6 +1292,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 @@ -1112,9 +1352,6 @@ var vol : PStorage_Volume; dir : pchar; fname : pchar; - dataBuf : puint32; - dataSize : puint32; - readErr : uint32; absPath : pchar; vfsObj : PVFSObject; devInfo : PVFSDevice; @@ -1156,10 +1393,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 +1426,39 @@ 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; + fd^.FSPrivate := nil; + fd^.FSCloseHook := nil; - { If reading, load the file data now } - 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; + { Cache per-file metadata and populate DataSize via FS open hook } + 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 + Error^ := eInvalidFileName + else + Error^ := eFileDoesNotExist; end; - - if readErr = 0 then begin - fd^.DataBuffer := puint32(dataBuf^); - fd^.DataSize := dataSize^; - fd^.Loaded := true; + 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; + 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); 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 +1468,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; @@ -1247,156 +1480,54 @@ begin debug.tracer.push_trace('driver.storage.vfs.OpenFile.exit'); end; -function WriteFile(FileHandle : TFileHandle; Position : uint32; Buffer : puint8; Length : uint32) : uint32; -var - fd : PFileDescriptor; - vol : PStorage_Volume; - dirEntry : TDirectory_Entry; - status : puint32; - padBuf : puint32; - padSize : uint32; +{ 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; + TransferFileSync := 0; + memset(uint32(@wait), 0, sizeof(wait)); + wait.Error := eUnknown; + wait.Process := CurrentProcess; + 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); - 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 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; + asm pushf; cli 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; + 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; - { 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; - debug.tracer.push_trace('driver.storage.vfs.WriteFile.exit'); - exit; - end; - - vol := fd^.Volume; - if vol = nil then exit; - if vol^.filesystem = 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); - end; - - 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 - fd : PFileDescriptor; - copyLen : uint32; begin - debug.tracer.push_trace('driver.storage.vfs.ReadFile.enter'); - ReadFile := 0; - - fd := fd_get(currentFDTable, FileHandle); - if fd = nil then exit; - - { 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; - 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; - 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; - if Buffer = nil then exit; - - { Copy from loaded buffer at Position into caller's buffer } - if Position >= fd^.DataSize then exit; - - { Clamp copyLen without relying on (Position + copyLen) which can overflow } - 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 := copyLen; - debug.tracer.push_trace('driver.storage.vfs.ReadFile.exit'); + ReadFile := TransferFileSync(FileHandle, Position, Buffer, Length, false); 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; @@ -1427,17 +1558,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; - dirEntry : TDirectory_Entry; - padBuf : puint32; 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 @@ -1445,7 +1576,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,31 +1588,11 @@ 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 BytesWritten <> nil then BytesWritten^ := padSize; if Callback <> nil then Callback(eNone, CallbackData); debug.tracer.push_trace('driver.storage.vfs.WriteFileAsync.exit'); exit; @@ -1499,39 +1610,38 @@ 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^.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, 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); @@ -1540,9 +1650,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 +1661,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 +1706,53 @@ 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; + fd^.FSPrivate := nil; + fd^.FSCloseHook := nil; 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^.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 + 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; + 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; + 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; @@ -2098,138 +2197,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; @@ -2285,30 +2303,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; @@ -2339,6 +2333,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'); @@ -2358,11 +2353,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 +2363,12 @@ 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^.readOffsetAsyncCallback = nil) + and (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; @@ -2413,167 +2382,95 @@ begin if Length < copyLen then copyLen := Length; - if copyLen > 0 then - core.util.memcpy(uint32(fd^.DataBuffer) + Position, uint32(Buffer), copyLen); + 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, 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; { ===================== 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 - vol : PStorage_Volume; - dir : pchar; - fname : pchar; - fullPath : pchar; - tmpPath : pchar; - status : puint32; - errCode : TError; + 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; - - 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 + CompletePathOperation(Error, eOutOfMemory, Callback, 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)); + CompletePathOperation(Error, eOutOfMemory, Callback, 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); + ctx^.ErrorPtr := Error; + ctx^.IsDirectory := IsDirectory; + ctx^.UserCallback := Callback; + ctx^.UserData := CallbackData; + 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 + kfree(void(ctx^.Path)); + kfree(void(ctx)); + CompletePathOperation(Error, eOutOfMemory, Callback, CallbackData); end; +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; - - 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 - vol : PStorage_Volume; - dir : pchar; - dirName : pchar; - fullPath : pchar; - tmpPath : pchar; - status : puint32; - errCode : TError; 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; - - 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); - 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); - 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; - - debug.tracer.push_trace('driver.storage.vfs.DeleteDirectoryAsync.exit'); + DeletePathAsync(Path, true, Error, Callback, CallbackData); end; { ===================== Async Directory Listing ============================= } @@ -2606,10 +2503,35 @@ 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; absPath : pchar; + worker : proc.types.PProcessContext; begin debug.tracer.push_trace('driver.storage.vfs.GetDirectoryListingAsync.enter'); if ResultMap <> nil then ResultMap^ := nil; @@ -2622,12 +2544,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; @@ -2995,39 +2931,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; @@ -3389,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(); @@ -3396,11 +3315,6 @@ var i : uint32; vol : PStorage_Volume; volName : pchar; - volNumStr : pchar; - mountPath : pchar; - prefix : pchar; - dataBuf : puint32; - dataSize : puint32; readErr : uint32; mntPath : pchar; mntLen : uint32; @@ -3418,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'); @@ -3432,67 +3351,34 @@ 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); 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)); + { Stable across label changes; safe in the space-delimited mount script. } + volName := stringCopy('/boot'); - 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, nil); mntPath[mntLen] := char(0); + if readErr > 0 then begin + lineStart := 0; while lineStart < mntLen do begin { Find end of line } @@ -3561,12 +3447,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; @@ -3644,8 +3529,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; @@ -3700,6 +3620,89 @@ 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'); + { 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'); + 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); + { 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...'); @@ -4034,15 +4037,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 +4074,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'); - WriteFileAsync(devHandle, 0, devBuf, 16, nil, nil); + { /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, 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)); @@ -4107,4 +4110,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 deleted file mode 100644 index 3b805751..00000000 --- a/src/driver/storage/fs/driver.storage.fs.fat32.pas +++ /dev/null @@ -1,2436 +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; - -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; - -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; - - { 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 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 - -{ 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; // need write functions for boot record! -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 - -function readFat(volume : PStorage_volume; cluster : uint32; bootRecord : PBootRecord) : uint32; -var - buffer : puint32; - fatEntriesPerSector : uint32; - sectorLocation : uint32; - dataStart : 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'); -end; - -procedure writeFat(volume : PStorage_volume; cluster : uint32; value : uint32; bootRecord : PBootRecord); -var - buffer : puint32; - fatEntriesPerSector : uint32; - sectorLocation : uint32; - dataStart : 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); - - 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); - - kfree(buffer); - io.syslog.logln('FAT32', 'writeFat: done'); -end; - -function getFatChain(volume : PStorage_volume; cluster : uint32; bootRecord : PBootRecord) : PLinkedListBase; -var - currentCluster : uint32; - currentClusterValue : uint32; - 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; - - while true do 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); - dirElm^:= currentCluster; - end; - - currentCluster := currentClusterValue; - 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; - 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 } - if bootRecord^.sectorsize > 0 then - maxCluster := (bootRecord^.FATSize * bootRecord^.sectorsize) div 4 - else - maxCluster := 0; - - 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; - - { 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; - -//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'); - 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) ); - - dataStart:= volume^.sectorStart + 1 + bootRecord^.rsvSectors + bootRecord^.FATSize; - - push_trace('driver.storage.fs.fat32.getDirEntries.readSectors'); - for i:=0 to LL_size(clusters) - 1 do begin - sectorLocation:= bootRecord^.spc * (i + cluster); - 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) - 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'); - io.syslog.logln('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; - 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'); - io.syslog.logln('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); - io.syslog.logln('FAT32', 'readDirectory: component not found'); - break; - end; - 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); - 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'); - 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 -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; - clusters : PLinkedListBase; - 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:= volume^.sectorStart + 1 + bootRecord^.FATSize + bootRecord^.rsvSectors; - - 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:= uint32(parentDirectory^.clusterlow) or uint32(parentDirectory^.clusterhigh shl 16); - - clusters:= findFreeClusters(volume, 1, bootRecord); - if clusters = 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; - cluster:= uint32(LL_Get(clusters, 0)^); - io.syslog.logln('FAT32', 'writeDirectory: allocated cluster'); - LL_Free(clusters); - 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; - bufferPointer^.clusterLow:= cluster; - bufferPointer^.clusterHigh:= uint16((cluster shr 16) and $0000FFFF); - - bufferPointer:= @PDirectory(buffer)[1]; - bufferPointer^.fileName:= parentArray; //TODO implement time - bufferPointer^.attributes:= attributes; - bufferPointer^.clusterLow:= parentCluster; - bufferPointer^.clusterHigh:= uint16((parentCluster shr 16) and $0000FFFF); - - //write to disk - driver.storage.mgr.storage_write(volume^.device, dataStart + (cluster * bootRecord^.spc), 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; - { 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; - end; - kfree(void(namePart)); - kfree(void(extPart)); - bufferPointer^.clusterLow:= cluster; - bufferPointer^.clusterHigh:= uint16((cluster shr 16) and $0000FFFF); - - 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). } -procedure freeFatChain(volume : PStorage_volume; startCluster : uint32; bootRecord : PBootRecord); -var - currentCluster : uint32; - nextCluster : uint32; -begin - push_trace('driver.storage.fs.fat32.freeFatChain.enter'); - currentCluster := startCluster; - while true do begin - nextCluster := readFat(volume, currentCluster, bootRecord); - writeFat(volume, currentCluster, 0, bootRecord); - - 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; - -{ 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; - EntriesPerSector : uint32; - totalEntries : uint32; - dir : PDirectory; - found : boolean; - rawIdx : uint32; - cluster : uint32; - sectorOffset : 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); - - if (fileName = nil) or (stringSize(fileName) = 0) then begin - io.syslog.logln('FAT32', 'deleteFile: invalid filename'); - if statusOut <> nil then statusOut^ := ord(eInvalidFileName); - kfree(status); - 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; - { 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 } - splitFileNameParts(fileName, namePart, extPart); - - { Read raw sectors from parent cluster (handles $E5 gaps correctly) } - 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 } - 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; - end; - found := true; - cluster := uint32(dir^.clusterLow) or uint32(dir^.clusterHigh shl 16); - break; - end; - end; - - 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; - - 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)); - 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; - directories : PLinkedListBase; - childDirs : PLinkedListBase; - parentEntry : PDirectory; - parentCluster : uint32; - status : puint32; - dataStart : uint32; - spc : uint32; - sectorLocation : uint32; - buffer : puint32; - EntriesPerSector : uint32; - totalEntries : uint32; - dir : PDirectory; - found : boolean; - rawIdx : uint32; - cluster : uint32; - childCount : uint32; - sectorOffset : 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); - - if (dirName = nil) or (stringSize(dirName) = 0) then begin - io.syslog.logln('FAT32', 'deleteDir: invalid directory name'); - if statusOut <> nil then statusOut^ := ord(eInvalidFileName); - kfree(status); - 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 } - splitFileNameParts(dirName, namePart, extPart); - - { Read raw sectors from parent cluster } - 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 } - 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; - end; - found := true; - cluster := uint32(dir^.clusterLow) or uint32(dir^.clusterHigh shl 16); - break; - end; - end; - - 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; - - { 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)); - 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; - directories : PLinkedListBase; - 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; -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); - - if (fileName = nil) or (stringSize(fileName) = 0) then begin - io.syslog.logln('FAT32', 'renameFile: invalid filename'); - if statusOut <> nil then statusOut^ := ord(eInvalidFileName); - kfree(status); - 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); - kfree(status); - kfree(void(parentDir)); - 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', '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; - - { Split current filename for 8.3 matching } - splitFileNameParts(fileName, namePart, extPart); - - { Read raw sectors from parent cluster } - 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 } - 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; - end; - end; - - 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; - - { 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)); - kfree(void(fileName)); - push_trace('driver.storage.fs.fat32.renameFile.exit'); - 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; -// 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, - 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, - 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 := 1; - if spc = 0 then spc := 1; - ctx^.SPC := spc; - - ctx^.FATSize := ((sectors div spc) * 4) div 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 := 1; - 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 := 1; - if spc = 0 then spc := 1; - - //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:= ((sectors div spc) * 4) div disk^.sectorsize; - - bootRecord^.jmp2boot := $0; //TODO impliment boot jump - bootRecord^.OEMName := asuroArray; - bootRecord^.sectorSize := disk^.sectorsize; - bootRecord^.spc := 1; - 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, 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; - -{ 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; -var - bootRecord : PBootRecord; - dirs : PLinkedListBase; - dir : PDirectory; - statusOut : puint32; - i : uint32; - exists : boolean; - cluster : uint32; - clusters : PLinkedListBase; - noClusters : uint32; - dataStart : uint32; - cleanFileName : byteArray8; - otherCFN : byteArray8; - tempdir : PDirectory; - namePart : pchar; - extPart : pchar; - readbuffer : puint32; - bytesPerCluster : uint32; - startClusterIdx : uint32; - inClusterOffset : uint32; - startSecInClust : uint32; - secByteOff : uint32; - chainPos : uint32; - curCluster : uint32; - clusterLBA : uint32; - sectorIdx : uint32; - destPos : uint32; - remaining : uint32; - bytesToCopy : uint32; - fileSize : uint32; -begin - push_trace('driver.storage.fs.fat32.readFileAtOffset.enter'); - io.syslog.logln('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; - - 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; - statusOut^ := 0; - end; - end; - end; - - kfree(void(namePart)); - 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)); - push_trace('driver.storage.fs.fat32.readFileAtOffset.notFound'); - exit; - end; - - 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; - end; - - { Clamp read length to not exceed file size } - remaining := byteCount; - 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 } - 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; - end; - - readbuffer := puint32(kalloc(bootRecord^.sectorSize)); - destPos := 0; - chainPos := startClusterIdx; - - while (remaining > 0) and (chainPos < noClusters) do begin - curCluster := puint32(LL_Get(clusters, chainPos))^; - clusterLBA := dataStart + (curCluster * uint32(bootRecord^.spc)); - - { 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); - end else begin - startSecInClust := 0; - secByteOff := 0; - end; - - sectorIdx := startSecInClust; - while (sectorIdx < uint32(bootRecord^.spc)) and (remaining > 0) do begin - driver.storage.mgr.storage_read(volume^.device, clusterLBA + sectorIdx, 1, readbuffer); - - 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; - 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; - -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.createAsyncCallback := @create_volume_async; - filesystem.detectcallback:= @detect_volumes; - filesystem.writecallback:= @writeFile; - filesystem.readcallback := @readFile; - filesystem.readOffsetCallback := @readFileAtOffset; - filesystem.identifyCallback := @identify_volume; - filesystem.deleteFileCallback := @deleteFile; - filesystem.deleteDirCallback := @deleteDir; - filesystem.renameFileCallback := @renameFile; - { 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; - filesystem.deleteDirAsyncCallback := 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'); - -end. diff --git a/src/driver/storage/fs/driver.storage.fs.flatfs.pas b/src/driver/storage/fs/driver.storage.fs.flatfs.pas index bbd63078..73052ac0 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 @@ -62,7 +63,7 @@ var procedure init; -procedure create_volume(volume : PStorage_Volume; sectors : uint32; start : uint32; config : puint32); +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); @@ -71,84 +72,55 @@ procedure read_file(volume : PStorage_Volume; fileName : pchar; data : PuInt32; implementation -procedure create_volume(volume : PStorage_Volume; sectors : uint32; start : uint32; config : puint32); -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^ <> 0 then begin - info^.fileCount := config^; + 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); @@ -763,13 +735,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 +780,7 @@ begin end; kfree(buffer); + if freeMbr then kfree(void(mbr)); end; function readDirectoryEntries(volume : PStorage_Volume; directory : pchar; status : PuInt32) : PLinkedListBase; @@ -843,16 +841,113 @@ 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; + ctx : pointer) : 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.validateFormat := @validate_format; filesystem.detectcallback:= @detect_volumes; filesystem.identifyCallback:= @identify_volume; + 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 b456bba9..50ca0aa6 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; @@ -406,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 @@ -419,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; @@ -451,6 +447,7 @@ begin volume^.filesystem := @filesystem; volume^.isBootDrive := disk^.isBootDevice; + cacheVolumeLabel(volume,pvd); driver.storage.vol.mgr.register_volume(disk, volume); kfree(buf); @@ -584,109 +581,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 +616,136 @@ 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; + ctx : pointer) : 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 +754,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/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. 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..24742ac5 --- /dev/null +++ b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.core.pas @@ -0,0 +1,2558 @@ +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; + +{ 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; +function fatInitDirectoryCluster(volume : PStorage_Volume; info : PFATVolumeInfo; cluster : uint32; parentCluster : uint32) : boolean; 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 + fatMaxClusterCount := FAT32ClusterLimit(bootRecord); +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; + +{ 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 + { 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; +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); + 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; +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 + + 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)); + 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 + Inc(cache^.Device^.Stats.FatCacheMisses); + 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); + if ctx = nil then begin + readFat := 0; + exit; + end; + 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 + Inc(cache^.Device^.Stats.FatCacheHits); + dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); + readFat := dataPtr[entryOff] and $0FFFFFFF; + cacheTouch(cache,baseSlot,baseSlot+w); + 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 + Inc(cache^.Device^.Stats.FatCacheHits); + dataPtr := puint32(uint32(cache^.Data) + ((baseSlot + w) * 512)); + readFat := dataPtr[entryOff] and $0FFFFFFF; + cacheTouch(cache,baseSlot,baseSlot+w); + asm popf end; + exit; + end; + Inc(cache^.Device^.Stats.FatCacheMisses); + cache^.Busy := true; + 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 + 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; + cacheTouch(cache,baseSlot,victim); + 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); + if ctx = nil then exit; + 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 + 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; + cacheTouch(cache,baseSlot,baseSlot+w); + 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 + 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; + cacheTouch(cache,baseSlot,baseSlot+w); + asm popf end; + exit; + end; + Inc(cache^.Device^.Stats.FatCacheMisses); + cache^.Busy := true; + 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 + 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; + cacheTouch(cache,baseSlot,victim); + 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].FileClusterStart := runMap.TotalClusters; + 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 (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; + 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 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; + 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; + 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; +var + newEnd : uint32; + needClusters : uint32; + targetClusters : uint32; + reserve : 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 + FAT32EnsureCapacityForWrite := eInvalidArgument; + exit; + end; + + if byteCount = 0 then exit; + + FAT32EnsureCapacityForWrite := FAT32EnsureDirEntry(ofi); + 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; + 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 + { 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; + 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)); + 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; + 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) 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; + 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; + 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); + + { 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); + 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 and $08) <> 0 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; + +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; + 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); + fatInitDirectoryCluster := true; +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; + + 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)); + 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.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 new file mode 100644 index 00000000..449cd930 --- /dev/null +++ b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.pas @@ -0,0 +1,67 @@ +unit driver.storage.fs.fat32; + +interface + +uses + boot.mgr, + driver.storage.fs.mgr, + driver.storage.types; + +procedure init; +procedure FAT32ReleaseVolumeState(volume : PStorage_Volume); + +implementation + +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; + +var + filesystem : TFilesystem; + +procedure FAT32ReleaseVolumeState(volume : PStorage_Volume); +begin + 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; + filesystem.createDirCallback := @FAT32CreateDirectory; + filesystem.createCallback := @create_volume; + 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; + filesystem.readOffsetAsyncCallback := @FAT32ReadFileAtOffsetAsync; + filesystem.writeOffsetAsyncCallback := @FAT32WriteFileAtOffsetAsync; + filesystem.fileSizeCallback := @FAT32GetFileSize; + filesystem.identifyCallback := @identify_volume; + filesystem.deleteFileCallback := @FAT32DeleteFile; + filesystem.deleteDirCallback := @FAT32DeleteDir; + filesystem.renameFileCallback := @FAT32RenameFile; + filesystem.openFileCallback := @FAT32OpenFile; + filesystem.closeFileCallback := @FAT32CloseFile; + filesystem.createDirAsyncCallback := nil; + filesystem.readDirAsyncCallback := nil; + 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); +end; + +initialization + boot.mgr.registerBoot('driver.storage.fs.fat32', @init, 'FAT32 Filesystem', 'driver.storage.fs.mgr'); + +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..d82e6b88 --- /dev/null +++ b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.transfer.pas @@ -0,0 +1,519 @@ +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 + proc.mgr, proc.types, + 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; + err : TError; +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 mod bytesPerCluster; + sectorOffset := runByteOffset mod sectorSize; + sectorLBA := FAT32ClusterToLBA(ofi^.VolumeInfo, startCluster) + (runByteOffset div sectorSize); + + 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; + 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); + err := driver.storage.mgr.storage_write(ofi^.Volume^.device, sectorLBA, 1, scratch); + if err <> eNone then break; + 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 + err := driver.storage.mgr.storage_read(ofi^.Volume^.device, sectorLBA, chunkSectors, + puint32(uint32(buffer) + bytesDone)) + else + 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 + noteWriteProgress(ofi, offset + bytesDone); + end; + + transferSync := bytesDone; +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; + 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; + +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; + +{ 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; + 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 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) or + (((uint32(ctx^.Buffer)+ctx^.BytesDone) and 1) <> 0) 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; + + if ctx^.Mode = ftmRead then begin readBatch(ctx); 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; + +{ 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 + ofi : PFATOpenFile; + tempCtx : pointer; + fileSize : uint32; + readLimit : uint32; + wait : TReadWait; +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 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); +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..6096cffb --- /dev/null +++ b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.types.pas @@ -0,0 +1,263 @@ +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 = 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 = 8; + FAT_WRITE_PREALLOC_MAX_CLUSTERS = 2048; + FAT_WRITE_PREALLOC_MIN_CLUSTERS = 64; + FAT_MAX_IO_SECTORS = 2048 * 2; + FAT_OPEN_HANDLE_SLOTS = 65000; +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; + FileClusterStart : 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; + 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); + TFATTransferPhase = ( + ftpIdle, + ftpAwaitDirect, + ftpAwaitScratchRead, + ftpAwaitScratchWrite, + ftpComplete, + ftpError + ); + + PFATVolumeCtx = ^TFATVolumeCtx; + PFATTransferCtx = ^TFATTransferCtx; + + PFATReadSlot = ^TFATReadSlot; + TFATReadSlot = record + Owner : PFATTransferCtx; + Bytes : uint32; + Error : TError; + end; + + 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; + Pumping, Resume, Finished : boolean; + ReadSubmitting : boolean; + ReadPending, ReadCount : uint32; + ReadSlots : array[0..3] of TFATReadSlot; + 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; + 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 new file mode 100644 index 00000000..89e01cc1 --- /dev/null +++ b/src/driver/storage/fs/fat32/driver.storage.fs.fat32.vol.pas @@ -0,0 +1,299 @@ +unit driver.storage.fs.fat32.vol; + +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; +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; + +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); +end; + +function formatSPC(config : PFSFormatParams) : uint32; +begin + formatSPC := 8; + if (config <> nil) and ((config^.Flags and FS_FORMAT_PARAM_CLUSTER_SIZE) <> 0) then + formatSPC := config^.ClusterSize; +end; + +function formatFATSize(sectors, spc : uint32) : uint32; +begin + { 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; + 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 countFreeFATClusters(volume : PStorage_Volume; br : PBootRecord) : uint32; +var limit, sectorIdx, entryIdx, clusterNum, count, sectors, batch : uint32; buffer : puint32; +begin + countFreeFATClusters := $FFFFFFFF; + limit := FAT32ClusterLimit(br); + if limit <= 2 then exit; + 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; + 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 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, batch); + end; + kfree(buffer); + countFreeFATClusters := count; +end; + +procedure detect_volumes(disk : PStorage_Device); +var + buffer : puint32; + bufSize : uint32; + volume : PStorage_Volume; + br : PBootRecord; + freeClusters : uint32; +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; + + if storage_read(disk, 0, 1, buffer) <> eNone then begin kfree(buffer); exit; end; + + br := PBootRecord(buffer); + 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 + 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; + 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; + + kfree(buffer); +end; + +function identify_volume(volume : PStorage_Volume) : boolean; +var + buffer : puint32; + br : PBootRecord; + bufSize : uint32; + fatNameOk : boolean; + freeClusters : uint32; + 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); + + if storage_read(volume^.device, volume^.sectorStart, 1, buffer) <> eNone then begin kfree(buffer); exit; end; + 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) and (br^.rootCluster < FAT32ClusterLimit(br)) and + (br^.manySectors <= volume^.sectorCount) then begin + io.syslog.logln('FAT32', 'identify_volume: FAT32 signature matched'); + 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; + +{ Every write is checked; a valid boot signature is published last. } +function create_volume(volume : PStorage_Volume; sectors, start : uint32; config : PFSFormatParams) : TError; +var + 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 + 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(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; + 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 1d3cc800..737fcabd 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; @@ -57,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; @@ -74,14 +79,180 @@ 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; +{ 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 +uses + driver.storage.vfs, + proc.mgr, + proc.types; + +type + PLabelWork = ^TLabelWork; + TLabelWork = record + Volume : PStorage_Volume; + Value : TVolumeLabel; + Callback : TIOCallback; + Data : pointer; + end; + PStorageLifecycleWatch = ^TStorageLifecycleWatch; + TStorageLifecycleWatch = record + Active : boolean; + ID : uint32; + Callback : TStorageLifecycleCallback; + UserData : pointer; + end; + + PFormatVolumeAsyncCtx = ^TFormatVolumeAsyncCtx; + TFormatVolumeAsyncCtx = record + Volume : PStorage_Volume; + Filesystem : PFilesystem; + 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; + NextVolumeID : 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; + +function auto_mount_volume(volume : PStorage_Volume) : TError; +begin + 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; +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; + +function claim_metadata(device : PStorage_Device) : boolean; +var flags : uint32; +begin + 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 (flags and $200) <> 0 then asm sti end; +end; + +function update_partition_system_id(device : PStorage_Device; sectorStart : uint32; systemId : uint8) : TError; +var src, copyMbr : PMaster_Boot_Record; slot : sint32; +begin + 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; + +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 err <> eNone then begin + driver.storage.vfs.InvalidateVolume(vol); + vol^.filesystem := nil; + end; + perform_format := err; +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 := 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); +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 @@ -96,67 +267,37 @@ 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; -{ 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; -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 - kfree(puint32(found)); -end; - -{ Remove all volumes belonging to a device } -procedure remove_all_volumes_for_device(device : PStorage_Device); +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(volumes) do begin - v := PStorage_Volume(Void(DL_Get(volumes, i))^); - if v^.device = device then begin - DL_Delete(volumes, i); - kfree(puint32(v)); - end else - i := i + 1; + 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; - if (device <> nil) and (device^.volumes <> nil) then - DL_Clear(device^.volumes); +procedure detach_volume(volume : PStorage_Volume); +var idx : sint32; +begin + 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 ===================== } @@ -165,6 +306,77 @@ procedure init(); begin push_trace('driver.storage.vol.mgr.init'); 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; +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 ===================== } @@ -175,6 +387,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)); @@ -184,6 +397,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; @@ -223,71 +437,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. @@ -431,17 +702,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; @@ -456,6 +734,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 @@ -483,168 +762,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 — buffer is the persistent cache } - driver.storage.mgr.storage_write_async(device, 0, 1, puint32(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 } - driver.storage.mgr.storage_write_async(device, 0, 1, puint32(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 } - driver.storage.mgr.storage_write_async(device, 0, 1, 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 : puint32) : 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; - - vol^.filesystem := fs; - fs^.createCallback(vol, vol^.sectorCount, vol^.sectorStart, config); - format_volume := true; + 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; + 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 : puint32; callback : TIOCallback; callbackData : pointer) : boolean; -var - vol : PStorage_Volume; - fs : PFilesystem; + config : PFSFormatParams; callback : TIOCallback; callbackData : pointer) : boolean; +var vol : PStorage_Volume; fs : PFilesystem; ctx : PFormatVolumeAsyncCtx; begin - push_trace('driver.storage.vol.mgr.format_volume_async'); format_volume_async := false; + if not prepare_format(device, volIndex, filesystemName, config, vol, fs) then exit; + ctx := PFormatVolumeAsyncCtx(kalloc(sizeof(TFormatVolumeAsyncCtx))); + 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)); + end; + set_volume_flag(vol, STORAGE_VOLUME_FLAG_FORMATTING, false); + device^.metadataBusy := false; + notify_lifecycle(sleVolumeFormatCompleted, device, vol, eOutOfMemory); +end; - vol := get_volume(volIndex); - if vol = nil then exit; - if vol^.device <> device then exit; +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; - fs := driver.storage.fs.mgr.find_filesystem_by_name(filesystemName); - if fs = nil then exit; - if fs^.createAsyncCallback = nil then exit; - - vol^.filesystem := fs; - fs^.createAsyncCallback(vol, vol^.sectorCount, vol^.sectorStart, config, callback, callbackData); - format_volume_async := true; +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/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 7ef253d0..ad933bd0 100644 --- a/src/proc/proc.mgr.pas +++ b/src/proc/proc.mgr.pas @@ -1,3 +1,17 @@ +// 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. +// 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. @@ -107,10 +121,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,11 +133,17 @@ var prio : uint8; begin push_trace('proc.mgr.create'); + createInternal := 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 } @@ -134,7 +154,7 @@ begin ctx^.ParentID := CurrentProcess^.ProcessID; { State } - ctx^.State := psCreated; + ctx^.State := psSuspended; ctx^.ExitCode := 0; { Entry point } @@ -154,15 +174,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,17 +265,40 @@ 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 } - 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 ---- } @@ -492,12 +565,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/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 } diff --git a/src/test/test.pas b/src/test/test.pas index 5b7b787e..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 @@ -12,6 +29,7 @@ implementation uses core.strings, + app.diskutil, driver.bus.usb.types, driver.bus.usb.core, driver.bus.usb.uhci, @@ -32,6 +50,11 @@ uses core.ds.prio, driver.storage.vfs, driver.storage.test, + driver.storage.bench, + driver.storage.metatest, + driver.storage.perftest, + driver.storage.ctl.ahci, + driver.storage.ctl.usb, core.enc.fnv1a, core.enc.djb2, core.ds.bloom, @@ -46,6 +69,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 +90,11 @@ 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; + driver.storage.ctl.usb.UnitTest; core.enc.fnv1a.UnitTest; core.enc.djb2.UnitTest; core.ds.bloom.UnitTest;