Architecture
forensic-vfs defines five layer-open traits, each a two-step probe()
(recognize) then open() (peel), plus one orchestrator, SourceOpen (in
forensic-vfs-resolver). The diagram lays the layers out in their normal nesting
order — an archive wraps a container, which holds a partition table, which may hold an
encrypted volume, which holds a filesystem. But SourceOpen is a graph, not a fixed
pipeline: every non-terminal peel yields an ImageSource that re-enters resolution,
so unusual real-evidence stacks (raw → LUKS → LVM → ext4, an archive inside a filesystem,
encryption that is container metadata) resolve by re-entry, in any order.
flowchart TD
SRC(["Evidence — a path or bytes"])
SRC --> IS
IS["<b>ImageSource</b> — read-only positioned bytes<br/>the one edge every layer consumes, and each peel re-produces"]
IS --> A
A["<b>1 · ArchiveOpen</b> — peel packaging<br/>gz · bz2 · tar · zip · clbx · 7z · AD1 · DAR"]
C["<b>2 · ContainerOpen</b> — decode the acquisition image<br/>E01 · VMDK · VHD · VHDX · QCOW2 · DMG · AFF4 · raw"]
V["<b>3 · VolumeSystemOpen</b> — walk the partition table<br/>MBR · GPT · APM"]
E["<b>4 · EncryptionOpen</b> — unlock the volume<br/>BitLocker · LUKS · FileVault · VeraCrypt"]
F["<b>5 · FileSystemOpen</b> — mount the filesystem<br/>NTFS · FAT · ext4 · XFS · btrfs · APFS · HFS+ · UFS · ISO9660 · UDF"]
A --> C --> V --> E --> F
F --> FN["<b>FsNode tree</b> — files · data streams · MACB metadata"]
A -. "Members → N members re-enter" .-> IS
C -. "peeled source re-enters" .-> IS
E -. "decrypted source re-enters" .-> IS
Example nestings a graph handles that a fixed lane would not:
E01 → GPT → BitLocker → NTFS (encryption after volume), raw → LUKS → LVM → ext4
(encryption before volume), E01 → APFS-container(encrypted volume) → APFS (encryption
is container metadata, not a separate step).
The byte source
ImageSource is a positioned-read, Send + Sync trait with read_at(&self, …)
and no write method. This delivers three properties at once:
- Parallel reads —
&selfshares oneArc<dyn ImageSource>across workers; aRead + Seekcursor's&mut selfcannot. - Read-only by construction — no write API exists to misuse.
- Clean
dyncomposition —Send + Syncare auto traits, soArc<dyn ImageSource>is itselfSend + Sync.
Adapters bridge the existing world: FileSource (positioned OS reads, no
Mutex<File>), SubRange (a byte window that is itself an ImageSource), and
SourceCursor (a Read + Seek view for legacy call sites).
Identity and metadata
FileIdis filesystem-specific (NtfsRef{entry,seq},ExtInode{ino,gen},ApfsOid{oid,xid},FatDirEntry,IsoExtent,Opaque), so a reused slot is never confused with the original.FsMetacarries the name/metadata allocation split (Allocated | Deleted | Orphan), per-timestampTimeSource+TimeResolution(including NTFS's 100 nsWinFileTime), ADS/resource-fork stream info, and residency — without the eager run-list (runs come fromextents()lazily).
Locator
A recursive chain of Layer nodes. Identity is the structured enum, so raw path
bytes containing a delimiter can never collide two specs. Two text forms: a
lossless canonical URI (round-trip is a test- and fuzz-enforced invariant) and
a lossy human Display. Credentials are supplied out-of-band at resolve time,
never stored in the address.
Crate structure
Three roles across three crates (ADR 0007):
| Crate | Role | Status |
|---|---|---|
forensic-vfs |
byte source (ImageSource), the five *Open traits, Openers dispatch table, Locator, FsMeta, FsKind |
published (0.4) — this crate |
forensic-vfs-resolver |
the SourceOpen orchestrator — impl SourceOpen for Openers, recursive graph descent, walk, snapshot_view |
published (0.1) — this workspace |
forensic-vfs-engine |
default_openers() wiring the ~17 concrete readers + Vfs::open(path) host bootstrap + concurrent block cache |
a separate published repo |
disk-forensic / disk4n6 |
thin CLI over the engine | evolving |
The leaf carries the contracts and nothing that names a concrete format; the resolver carries the reader-independent descent policy; the engine (its own repo, so 17 reader trees never pollute the leaf's CI or audit surface) carries the concrete wiring. The division and its rationale — including why the resolver was extracted from the leaf once the archive layer forced richer selection policy — are recorded in ADR 0007; the layer model in ADR 0003; first-class archives in ADR 0008.
Frontier
The horizontal layers are strong — container and filesystem/archive readers implement
their contracts in production. The two vertical layers are the remaining work: the
VolumeSystemOpen (MBR/GPT/APM/VSS) and EncryptionOpen (BitLocker/LUKS/FileVault)
readers exist as crates but are not yet wired to the contract. See PRD §7 for
the ranked coverage matrix and remaining work.