Skip to main content

forensic_vfs/
fs.rs

1//! The filesystem navigation contract and its unified forensic metadata.
2//!
3//! One mounted, read-only filesystem is an [`Arc<dyn FileSystem>`]: all reads are
4//! `&self` so N workers share one handle (no per-thread MFT re-parse), and bulk
5//! enumerations are **owned, `Send`, `'static` streams** — never `&self`-borrowing
6//! iterators (which cannot cross a thread boundary) and never eager `Vec`s (which
7//! OOM on WinSxS-scale directories). [`FileId`] is a filesystem-specific identity,
8//! not a bare inode, because a reallocated NTFS record or a reused ext inode must
9//! never be confused with the original.
10
11use std::sync::Arc;
12
13use crate::error::VfsResult;
14
15/// Filesystem-specific stable identity. The address domain matches each FS's real
16/// identity primitive, so a reused slot is never confused with the original.
17#[non_exhaustive]
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum FileId {
20    /// NTFS MFT reference: record number + sequence.
21    NtfsRef { entry: u64, seq: u16 },
22    /// ext2/3/4 inode + generation.
23    ExtInode { ino: u64, gen: u32 },
24    /// APFS object id + transaction id.
25    ApfsOid { oid: u64, xid: u64 },
26    /// FAT/exFAT physical directory-entry address (no stable inode).
27    FatDirEntry { cluster: u32, index: u16 },
28    /// ISO 9660 path-table / extent address.
29    IsoExtent { block: u32 },
30    /// A filesystem with a plain inode and nothing finer.
31    Opaque(u64),
32}
33
34/// A named data stream on a node: the default `$DATA`, an NTFS ADS, an HFS+
35/// resource fork, an xattr, or synthetic slack. Metadata only — the actual runs
36/// come from [`FileSystem::extents`], lazily.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct StreamInfo {
39    pub id: StreamId,
40    pub name: Option<Vec<u8>>,
41    pub size: u64,
42    pub residency: ResidencyKind,
43    pub kind: StreamKind,
44}
45
46/// Which stream of a node an operation addresses.
47#[non_exhaustive]
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub enum StreamId {
50    Default,
51    Named(u16),
52    ResourceFork,
53    Xattr(u16),
54    Slack,
55}
56
57/// Stream taxonomy — not every named stream is an NTFS ADS; a consumer needs to
58/// know what it is reading rather than flattening all streams into one bucket.
59#[non_exhaustive]
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum StreamKind {
62    NtfsData,
63    NtfsAds,
64    HfsResourceFork,
65    ApfsNamed,
66    Xattr,
67    SyntheticSlack,
68}
69
70/// Whether a stream's bytes live inline in the metadata record or out in runs.
71#[non_exhaustive]
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum ResidencyKind {
74    Resident { inline_len: u32 },
75    NonResident,
76}
77
78/// Name/metadata-layer allocation status (TSK's name-vs-meta split). Run-level
79/// allocation is tracked separately on [`RunInfo`].
80#[non_exhaustive]
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum Allocation {
83    Allocated,
84    Deleted,
85    Orphan,
86}
87
88/// What kind of node this is.
89#[non_exhaustive]
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum NodeKind {
92    File,
93    Dir,
94    Symlink,
95    Device,
96    Other,
97}
98
99/// Where a timestamp came from — an NTFS `$STANDARD_INFORMATION` time and a
100/// `$FILE_NAME` time disagreeing is a tamper signal, so the source is preserved.
101#[non_exhaustive]
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum TimeSource {
104    /// NTFS `$STANDARD_INFORMATION`.
105    Si,
106    /// NTFS `$FILE_NAME`.
107    Fn,
108    /// A Unix inode table (ext/APFS).
109    InodeTable,
110    /// A directory entry (FAT/exFAT).
111    DirEntry,
112    /// Source not distinguished by the reader.
113    Unspecified,
114}
115
116/// Native granularity of a timestamp — a UTC/seconds assumption silently loses
117/// NTFS's 100 ns resolution (a tamper signal) or FAT's 2-second quantization.
118#[non_exhaustive]
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum TimeResolution {
121    /// Windows FILETIME, 100 ns ticks.
122    WinFileTime,
123    Nanos,
124    Micros,
125    Seconds,
126    /// FAT last-modified is quantized to 2 seconds.
127    TwoSeconds,
128}
129
130/// One timestamp with its provenance.
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub struct TimeStamp {
133    /// Nanoseconds since the Unix epoch (i128 spans FILETIME's range).
134    pub unix_nanos: i128,
135    pub source: TimeSource,
136    pub resolution: TimeResolution,
137}
138
139/// MAC(B) times. `None` for a field means "not present in this FS", which is
140/// forensically distinct from an epoch-zero value.
141#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
142pub struct MacbTimes {
143    pub modified: Option<TimeStamp>,
144    pub accessed: Option<TimeStamp>,
145    /// Metadata-change (ctime).
146    pub changed: Option<TimeStamp>,
147    /// Creation (crtime / born).
148    pub born: Option<TimeStamp>,
149}
150
151/// How a volume's timestamps are anchored. FAT/exFAT are volume-local, so a UTC
152/// assumption shifts every MAC time.
153#[non_exhaustive]
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum TimeZonePolicy {
156    Utc,
157    LocalUnknown,
158    Local { minutes_east: i16 },
159}
160
161/// Logical/physical sector and cluster/block sizes, with per-layer provenance.
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub struct SectorSizes {
164    pub logical: u32,
165    pub physical: u32,
166    pub cluster_or_block: u32,
167}
168
169/// Flags on one data run.
170#[allow(clippy::struct_excessive_bools)] // a domain flags record, not state
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
172pub struct RunFlags {
173    pub sparse: bool,
174    pub encrypted: bool,
175    pub compressed: bool,
176    /// A filler/placeholder run (e.g. a sparse hole rendered as zeros).
177    pub filler: bool,
178}
179
180/// A physical byte run in the underlying image.
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182pub struct ByteRun {
183    pub image_offset: u64,
184    pub len: u64,
185    pub flags: RunFlags,
186}
187
188/// Run-level allocation — a deleted file can have partly-reallocated clusters; an
189/// allocated file can have sparse holes. Independent of [`FsMeta::allocated`].
190#[non_exhaustive]
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
192pub enum RunAlloc {
193    Allocated,
194    Unallocated,
195    Overwritten,
196    Unknown,
197}
198
199/// One run plus its allocation provenance.
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201pub struct RunInfo {
202    pub run: ByteRun,
203    pub alloc: RunAlloc,
204}
205
206/// One directory entry.
207#[derive(Debug, Clone, PartialEq, Eq)]
208pub struct DirEntry {
209    pub name: Vec<u8>,
210    pub id: FileId,
211    pub kind: NodeKind,
212}
213
214/// A hardlink back-reference: a parent directory plus the name under it.
215#[derive(Debug, Clone, PartialEq, Eq)]
216pub struct HardLink {
217    pub parent: FileId,
218    pub name: Vec<u8>,
219}
220
221/// The unified forensic metadata record — TSK's name-layer vs meta-layer split,
222/// ADS/residency, and per-timestamp provenance, **without** the eager run-list
223/// (runs come from [`FileSystem::extents`] on demand).
224#[derive(Debug, Clone, PartialEq, Eq)]
225pub struct FsMeta {
226    /// Metadata address (MFT reference / inode number).
227    pub ino: u64,
228    pub kind: NodeKind,
229    /// Name/metadata-layer status.
230    pub allocated: Allocation,
231    pub size: u64,
232    pub nlink: u32,
233    pub uid: Option<u32>,
234    pub gid: Option<u32>,
235    pub mode: Option<u32>,
236    pub times: MacbTimes,
237    /// Default `$DATA` plus ADS / resource forks (metadata only).
238    pub streams: Vec<StreamInfo>,
239    pub residency: ResidencyKind,
240    pub link_target: Option<Vec<u8>>,
241}
242
243/// An owned, `Send`, `'static` stream of directory entries — holds no borrow of
244/// `&self` and no lock across `next()`, so it moves to a worker thread freely.
245pub struct DirStream(Box<dyn Iterator<Item = VfsResult<DirEntry>> + Send>);
246/// An owned stream of allocation-tagged runs.
247pub struct ExtentStream(Box<dyn Iterator<Item = VfsResult<RunInfo>> + Send>);
248/// An owned stream of nodes (for deleted/orphan enumeration).
249pub struct NodeStream(Box<dyn Iterator<Item = VfsResult<FsMeta>> + Send>);
250
251impl DirStream {
252    /// Wrap any `Send + 'static` iterator of entries.
253    pub fn new(it: impl Iterator<Item = VfsResult<DirEntry>> + Send + 'static) -> Self {
254        Self(Box::new(it))
255    }
256    /// An empty stream.
257    #[must_use]
258    pub fn empty() -> Self {
259        Self(Box::new(std::iter::empty()))
260    }
261}
262impl ExtentStream {
263    pub fn new(it: impl Iterator<Item = VfsResult<RunInfo>> + Send + 'static) -> Self {
264        Self(Box::new(it))
265    }
266    #[must_use]
267    pub fn empty() -> Self {
268        Self(Box::new(std::iter::empty()))
269    }
270}
271impl NodeStream {
272    pub fn new(it: impl Iterator<Item = VfsResult<FsMeta>> + Send + 'static) -> Self {
273        Self(Box::new(it))
274    }
275    #[must_use]
276    pub fn empty() -> Self {
277        Self(Box::new(std::iter::empty()))
278    }
279}
280impl Iterator for DirStream {
281    type Item = VfsResult<DirEntry>;
282    fn next(&mut self) -> Option<Self::Item> {
283        self.0.next()
284    }
285}
286impl Iterator for ExtentStream {
287    type Item = VfsResult<RunInfo>;
288    fn next(&mut self) -> Option<Self::Item> {
289        self.0.next()
290    }
291}
292impl Iterator for NodeStream {
293    type Item = VfsResult<FsMeta>;
294    fn next(&mut self) -> Option<Self::Item> {
295        self.0.next()
296    }
297}
298
299/// The filesystem family — the canonical identity newtype from
300/// forensicnomicon-core (`FsKind::NTFS`, `FsKind::EXT`, …).
301pub use forensicnomicon_core::filesystems::FsKind;
302
303/// One mounted, read-only filesystem. Inode-addressed; `&self` reads share one
304/// handle across workers; internal caches use interior mutability, never
305/// `&mut self`.
306pub trait FileSystem: Send + Sync {
307    fn kind(&self) -> FsKind;
308    fn root(&self) -> FileId;
309    fn sector_sizes(&self) -> SectorSizes;
310    fn timestamp_zone(&self) -> TimeZonePolicy;
311
312    /// Stream the children of a directory (owned, `Send`).
313    fn read_dir(&self, ino: FileId) -> VfsResult<DirStream>;
314    /// Stream the runs of one data stream of a node (owned, `Send`).
315    fn extents(&self, ino: FileId, stream: StreamId) -> VfsResult<ExtentStream>;
316
317    fn lookup(&self, parent: FileId, name: &[u8]) -> VfsResult<Option<FileId>>;
318    fn meta(&self, ino: FileId) -> VfsResult<FsMeta>;
319    fn read_at(&self, ino: FileId, stream: StreamId, off: u64, buf: &mut [u8]) -> VfsResult<usize>;
320    /// Read a symlink target, capped so a hostile symlink cannot allocate without
321    /// bound.
322    fn read_link(&self, ino: FileId, cap: usize) -> VfsResult<Vec<u8>>;
323
324    // --- Forensic surface (default-empty / streamed) ---
325
326    fn data_streams(&self, ino: FileId) -> VfsResult<Vec<StreamInfo>> {
327        let _ = ino;
328        Ok(Vec::new())
329    }
330    /// Hardlink back-references (capped by the implementation).
331    fn hardlinks(&self, ino: FileId) -> VfsResult<Vec<HardLink>> {
332        let _ = ino;
333        Ok(Vec::new())
334    }
335    /// Deleted/orphan nodes, streamed (never an eager `Vec`).
336    fn deleted(&self) -> VfsResult<NodeStream>;
337    /// Unallocated runs, streamed.
338    fn unallocated(&self) -> VfsResult<ExtentStream>;
339    /// File slack for a stream, if the FS exposes it.
340    fn slack(&self, ino: FileId, stream: StreamId) -> VfsResult<Option<ByteRun>> {
341        let _ = (ino, stream);
342        Ok(None)
343    }
344
345    /// Findings raised while mounting/navigating this filesystem. Behind the
346    /// `findings` feature so a bare reader does not inherit forensicnomicon.
347    #[cfg(feature = "findings")]
348    fn findings(&self) -> VfsResult<Vec<forensicnomicon::report::Finding>> {
349        Ok(Vec::new())
350    }
351}
352
353/// The object-safe shared filesystem handle.
354pub type DynFs = Arc<dyn FileSystem>;