Files
Asuro/plan.md
T

16 KiB

FAT32 Rewrite Plan

Implement this entire plan without stopping.

Goal

Replace the current FAT32 runtime I/O architecture with a simpler, faster design where open-time metadata work builds a cached runtime view, and steady-state file reads and writes consume that cached view with direct storage I/O.

The rewrite should make the common path thin enough that contiguous, preopened reads are near raw block I/O performance, while preserving correctness for fragmented files, append/growth, and metadata updates.

Immediate Direction

  1. Rename the current FAT32 implementation file to driver.storage.fs.fat32.old.pas.
  2. Create a new driver.storage.fs.fat32.pas as the public registration/entry unit.
  3. Move the new implementation into a dedicated fat32/ folder with multiple focused units instead of another monolithic source file.
  4. Rebuild the internals from a simpler architecture instead of continuing to patch the old design.

The old file remains as a reference during migration and can be deleted after parity is reached.

Core Problems To Fix

  1. The current runtime path is overcomplicated.
  2. Ordinary file I/O pays too much control-plane overhead.
  3. Fragmented files are treated as a special slow path instead of a normal case.
  4. The async API still hides synchronous behavior in some cases.
  5. FAT and directory metadata work leaks into the steady-state transfer loop.
  6. The current source layout is too large to maintain comfortably as one unit.

Design Principles

  1. Fragmentation is normal, not a special case.
  2. File I/O should be run-map driven.
  3. FAT is metadata, not the runtime transfer engine.
  4. The common path must avoid kalloc/kfree.
  5. Async APIs must actually be async.
  6. The same architecture must handle 1 run or 100 runs.
  7. Metadata work should be deferred or isolated from the hot path.
  8. Ownership and lifetime rules must be explicit at the file, volume, and transfer levels.

Architectural Layers

The new design should keep three layers separate so FAT policy does not leak back into the transfer engine.

A. FAT Metadata Layer

Responsible for:

  • boot record and FAT geometry interpretation
  • FAT entry reads/writes
  • FAT chain traversal
  • directory entry lookup
  • raw directory entry locator resolution
  • directory entry update/commit
  • cluster allocation/free
  • run-map build/rebuild
  • corruption detection and validation

B. Open-File State Layer

Responsible for:

  • pointer/reference to the mounted volume
  • pointer/reference to shared immutable TFATVolumeInfo
  • first cluster
  • committed byte size
  • allocated cluster count
  • cached run map
  • logical dirty state
  • reusable scratch sector buffer
  • sequential cursor hints
  • cached raw directory entry location
  • open-handle lifetime and teardown bookkeeping

C. Transfer Layer

Responsible for:

  • offset/length to run slicing
  • direct storage I/O submission
  • partial-sector read-modify-write handling
  • async continuation/chaining
  • completed-byte accounting
  • EOF/capacity bounds enforcement

The transfer layer must not perform FAT scans, directory searches, or cluster allocation from IRQ/completion context.

Runtime View: Runs, Not FAT Walks

Define the runtime view explicitly:

  • run = contiguous range of file clusters on disk
  • run map = ordered in-memory array of all file runs for one open handle

The run map is the authoritative transfer view for that handle. FAT is consulted only during open, growth, explicit rebuild/validation, or metadata commit.

Fragmentation only changes how many runs are walked. It must not select a different architecture.

Volume Geometry And Shared State

Do not copy boot/FAT geometry into every file handle.

Use a shared per-volume immutable record such as TFATVolumeInfo containing:

  • sector size
  • sectors per cluster
  • FAT start
  • FAT size
  • data start
  • root cluster
  • max cluster
  • other immutable geometry/cache references needed for fast translation

Each open file should hold:

  • Volume
  • pointer/reference to TFATVolumeInfo

Open File State

Each open file should own a compact cached state record such as TFATOpenFile containing:

  • Volume
  • VolumeInfo
  • FirstCluster
  • ByteSize
  • AllocClusters
  • run map
  • sequential cursor hint
  • reusable scratch sector buffer
  • dirty allocation / dirty size / dirty dir-entry flags
  • raw directory entry locator

This state replaces repeated rediscovery and repeated FAT walking during normal I/O.

Raw Directory Entry Locator

Every open file must cache enough information to update the on-disk directory entry without rescanning the parent directory. The cached form should include either:

  • parent directory first cluster plus raw entry location data

or a fully resolved raw location such as:

  • raw directory entry cluster
  • raw sector LBA
  • entry index within sector

This is required to avoid regressions in first-cluster and size updates.

File Size And Allocation State

The plan must keep these rules explicit:

  • ByteSize = committed logical EOF
  • AllocClusters = current allocated chain length
  • allocated capacity = AllocClusters * ClusterSize
  • writes may use existing allocated capacity without allocation growth
  • ByteSize advances only from actual completed writes, never intended request length

This directly protects against partial-growth and short-write corruption.

Empty File Creation Model

For a newly created empty file:

  • FirstCluster may be 0
  • run map is initially empty
  • ByteSize = 0
  • AllocClusters = 0
  • raw directory entry locator is known immediately
  • first allocation happens only when a write requires storage

Run-Map Lifetime Rules

This needs to be defined up front:

  1. Each open file owns its own run map.
  2. The run map is built once at open and then trusted for that handle unless the same handle grows or explicitly rebuilds it.
  3. Writes through that handle update that handle's run map incrementally in memory.
  4. Other open handles are not kept coherent in the first rewrite. Their run maps and sizes are treated as open-time snapshots until reopen.
  5. Concurrent write opens for the same file should be disallowed for the first rewrite, or explicitly treated as unsupported/undefined. Prefer rejecting the second writable open.
  6. Flush/close commits directory entry and FAT changes from the handle's current state; optional full rebuild-and-validate before commit is a later hardening step, not part of the hot path.

Unified Transfer Engine

Implement one transfer engine for both reads and writes.

It should:

  • accept open-file state, buffer, offset, length, mode, callback
  • translate byte ranges into run slices
  • issue direct storage I/O chunk by chunk
  • handle completion by advancing precomputed transfer state
  • use scratch-sector read-modify-write only for unaligned edges
  • complete without separate aligned/fragmented architectures

The old special-case direct path should become the normal path.

Async Execution Model

For the first rewrite, define one concrete model:

  • open/build-run-map may remain synchronous
  • read/write entrypoints submit async storage I/O and return immediately
  • transfer progression uses a small pooled TFATTransferCtx
  • IRQ/completion callbacks only advance precomputed transfer state and schedule the next I/O or final completion
  • IRQ/completion callbacks must not call helpers that can perform blocking FAT or directory I/O
  • close/flush may remain synchronous initially if that keeps the contract honest and simple

This keeps async behavior explicit and avoids hidden blocking inside async hooks.

Rare Heavy Work Only

Only these operations should do heavier FAT or metadata work:

  • opening a file
  • building or rebuilding a run map
  • allocating/extending clusters
  • updating directory entries
  • flushing FAT or metadata
  • partial-sector read-modify-write preparation
  • corruption validation/failure handling

These must not dominate steady-state transfer throughput.

Corruption And Validation Rules

When FAT traversal or run-map build finds:

  • out-of-range cluster
  • cycle
  • bad-cluster marker
  • premature EOC relative to stored size

the first rewrite should fail the open or mark the file invalid for transfer. It should not attempt automatic repair.

All corruption-handling loops must be bounded by hard limits derived from FAT size / max cluster count.

Required Runtime Invariants

Invariant 1

For any open file:

sum(run.length_clusters) = AllocClusters

Invariant 2

Committed logical size must satisfy:

ByteSize <= AllocClusters * ClusterSize

Invariant 3

On successful flush/close:

  • directory entry first cluster matches open state
  • directory entry size matches committed bytes
  • FAT chain matches the run map if dirty allocation occurred

Invariant 4

No async transfer may outlive:

  • file handle
  • volume object
  • transfer context backing storage

Invariant 5

The run map is the authoritative transfer view for the handle; FAT is not re-walked in the steady-state transfer path.

What To Remove From The New Design

The new implementation should avoid carrying forward these ideas:

  • worker-centric per-chunk file transfer orchestration
  • separate fast path and general path for aligned vs fragmented I/O
  • synchronous fallbacks hidden inside async hooks
  • repeated FAT-chain walking during ordinary sequential transfer
  • hot-path temporary allocations
  • parent-directory rescans just to commit file size or first cluster

Planned Source Layout

The rewrite should move FAT32 internals into a dedicated folder so the code remains readable and reviewable.

Planned shape:

  • src/driver/storage/fs/driver.storage.fs.fat32.pas for public registration and top-level hook wiring
  • src/driver/storage/fs/driver.storage.fs.fat32.old.pas for the temporary legacy reference
  • src/driver/storage/fs/fat32/ for focused internal units

Likely internal unit split:

  • volume / geometry
  • FAT metadata helpers
  • directory lookup and raw entry location
  • open-file state and handle lifetime
  • run-map construction
  • transfer engine
  • allocation/growth
  • flush/sync

Exact file names can be finalized during Phase 0, but the rewrite should not collapse back into one large source file.

Migration Plan

Phase 0: File Split And Scaffolding

  1. Rename existing FAT32 unit source to driver.storage.fs.fat32.old.pas.
  2. Create a new driver.storage.fs.fat32.pas that preserves the public filesystem registration contract.
  3. Create the src/driver/storage/fs/fat32/ folder and split new internals into focused units from the start.

Phase 1A: Volume Geometry And FAT Primitives

Build and validate:

  • TFATVolumeInfo
  • shared geometry decode
  • FAT entry read helper
  • bounded FAT chain traversal helper
  • run-map builder
  • raw directory entry locator helper

At the end of this phase, the core metadata primitives are usable without the old runtime architecture.

Phase 1B: Minimal Read-Only Mount/Open

Build the new unit with only:

  • type definitions needed by the filesystem manager
  • boot record and directory entry structures
  • volume identify/detect hooks
  • basic open/close hooks
  • read-only run-map construction

At the end of this phase, the new unit should compile, mount a FAT32 volume, open files, and build read-only run maps.

Phase 2: Read Path First

Implement a unified run-driven read engine:

  • build run map at open
  • read from runs directly
  • support multi-run files without architectural fallback
  • use a pooled transfer context
  • use a reusable scratch sector buffer only for unaligned reads

Acceptance target:

  • aligned sequential reads on contiguous preopened files should be near raw STORBENCH read speed
  • fragmented reads should scale mainly with run count, not collapse architecturally

Phase 3: Writes Within Existing Allocation

Implement overwrite and in-capacity write support on the same transfer model:

  • direct writes over existing runs
  • partial-sector handling through reusable scratch buffer
  • metadata dirtied in memory, not rewritten every chunk
  • logical EOF advanced only from actual completed bytes

Acceptance target:

  • aligned writes over preallocated files use the same thin transfer engine as reads
  • no per-chunk metadata rewrite

Phase 4: Allocation And Growth

Implement append and extension logic:

  • allocate clusters when writes exceed current capacity
  • extend the open-file run map incrementally
  • prefer contiguous growth when possible
  • keep allocation outside the steady-state transfer loop
  • handle first-cluster update for empty files

Acceptance target:

  • append-heavy writes remain much closer to preallocated write performance than today
  • fragmented growth still remains run-driven

Phase 5: Metadata Commit Rules

Implement clean flush/close behavior:

  • FAT dirty tracking
  • directory entry update on close or explicit sync
  • first-cluster update for newly created files
  • byte-size commit from actual completed bytes only
  • handle/volume teardown rules for in-flight transfers

Acceptance target:

  • no regression in crash-safety compared with current design assumptions
  • no hot-path metadata thrash

Phase 6: Async Contract Cleanup

Make the async API honest and documented:

  • async hooks always return immediately
  • no synchronous disk I/O hidden inside async entrypoints
  • callback context is consistent and documented
  • FD lifetime and teardown rules are enforced
  • close/unmount/process-exit races are explicitly handled

Acceptance target:

  • ReadFileAsync and WriteFileAsync are genuinely async for all supported cases
  • no blocking FAT work is re-entered from IRQ/completion context

Required Supporting Structures

The new implementation should include:

  • TFATVolumeInfo for shared immutable geometry
  • TFATOpenFile with run map, logical state, raw directory entry locator, and reusable scratch buffer
  • pooled TFATTransferCtx
  • compact run representation
  • read/write helpers that work from run maps, not repeated FAT scans
  • explicit flush/sync helpers

It should not require a per-volume worker just to progress ordinary file transfers.

Compatibility Requirements

The new implementation should preserve compatibility with:

  • driver.storage.fs.mgr
  • driver.storage.vfs
  • current filesystem hook signatures in driver.storage.types
  • current FAT32 on-disk format

The rewrite should change the internal architecture, not the external filesystem contract, unless a later cleanup intentionally revises that contract.

Performance Targets

Track both open/close cost and steady-state transfer throughput.

Initial realistic targets:

  • reads on contiguous preopened files should be near raw block I/O throughput
  • writes on preallocated files should be much closer to raw than today
  • append/growth will remain measurably slower due to FAT and directory updates
  • the remaining gap to raw should be mostly metadata/allocation cost, not control-plane overhead

Longer-term target:

  • once FAT32 overhead is reduced, remaining limits should mainly be in storage/AHCI

Correctness Requirements

Must explicitly preserve:

  • correct handling of fragmented files
  • correct file size updates
  • correct first-cluster updates
  • correct directory entry location handling
  • no shared DMA-buffer hazards
  • no blocking FAT work from IRQ callbacks
  • no use-after-free during close/unmount/process teardown
  • bounded handling of bad or cyclic FAT chains
  • correct behavior for empty files with FirstCluster = 0

Validation Plan

  1. Compile after each phase.
  2. Smoke-test mount/open/read on a known FAT32 volume.
  3. Measure open/close cost separately from transfer throughput.
  4. Run IOTEST on a fresh 4 KB cluster FAT32 volume.
  5. Run STORBENCH on the same disk and compare.
  6. Test fragmented-file read/write behavior.
  7. Test create, append, close, reopen, and verify.
  8. Test writable-open rejection or unsupported-path behavior for concurrent writers.
  9. Test close-during-async and volume invalidation edge cases.
  10. Test corruption detection on invalid or cyclic cluster chains.

Non-Goals For The First Rewrite

These are explicitly not required for the first clean rewrite:

  • perfect crash-consistency semantics
  • journaled metadata behavior
  • aggressive readahead/writeback cache
  • cross-handle cache coherency for concurrent file writers
  • automatic FAT repair

Those can come after the architecture is clean.

Definition Of Success

The rewrite is successful when:

  1. FAT32 ordinary file I/O is run-driven by default.
  2. The old direct path no longer exists as a separate idea because it became the main path.
  3. Async hooks are genuinely async.
  4. Hot-path allocator churn is removed.
  5. Directory entry updates no longer require parent-directory rescans during ordinary file writes.
  6. FAT32 throughput is no longer dominated by internal orchestration overhead.