Skip to main content

forensic_vfs/
registry.rs

1//! The plugin contracts and the compiled-in dispatch [`Openers`].
2//!
3//! A reader implements one of the five `*Open` traits; the engine
4//! (`forensic-vfs-engine`) fills an [`Openers`] with every reader and drives the
5//! resolver. The table is explicit and greppable — not a link-time `inventory`
6//! registration — so the dependency graph stays auditable and detection order is
7//! deterministic.
8
9use crate::archive::ArchiveContents;
10use crate::encryption::EncryptionLayer;
11use crate::error::VfsResult;
12use crate::fs::{DynFs, FsKind};
13use crate::source::DynSource;
14use crate::volume::{VolumeScheme, VolumeSystem};
15
16/// The outer container/image format.
17#[non_exhaustive]
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum ContainerFormat {
20    Ewf,
21    Vmdk,
22    Vhdx,
23    Vhd,
24    Qcow2,
25    Dmg,
26    Aff4,
27    Ad1,
28    Dar,
29    /// A flat raw/dd image (no wrapper).
30    Raw,
31    /// Sniff the format at resolve time.
32    Auto,
33}
34
35/// A prober's verdict. `Yes` carries *how* it matched, for the ambiguity report.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum Confidence {
38    No,
39    Maybe,
40    Yes { how: &'static str },
41}
42
43impl Confidence {
44    /// True for `Yes`/`Maybe` — worth attempting `open`.
45    #[must_use]
46    pub fn is_candidate(self) -> bool {
47        !matches!(self, Confidence::No)
48    }
49
50    /// True only for a definite `Yes`.
51    #[must_use]
52    pub fn is_yes(self) -> bool {
53        matches!(self, Confidence::Yes { .. })
54    }
55}
56
57/// A bounded window of bytes handed to a prober. Holds a prefix of the source
58/// (the *head*) plus the absolute base offset that prefix starts at, so a prober
59/// reads magic without an unbounded scan and without touching the source
60/// directly. It also carries the source's `total_len` and a *tail* window (the
61/// last N bytes), so a prober can match a trailer signature — e.g. the DMG
62/// `koly` footer at `total_len - 512` — that the head window never reaches.
63pub struct SniffWindow<'a> {
64    base: u64,
65    bytes: &'a [u8],
66    total_len: u64,
67    tail: &'a [u8],
68}
69
70impl<'a> SniffWindow<'a> {
71    /// A head-only window of `bytes` that begins at absolute `base` in the
72    /// source. `total_len` is inferred as `base + bytes.len()` and the tail is
73    /// empty — existing head-magic probers are unaffected.
74    #[must_use]
75    pub fn new(base: u64, bytes: &'a [u8]) -> Self {
76        Self {
77            base,
78            bytes,
79            total_len: base.saturating_add(bytes.len() as u64),
80            tail: &[],
81        }
82    }
83
84    /// A window carrying both a head (`bytes` at `base`) and a `tail` (the last
85    /// bytes of the source), plus the source's `total_len`. `tail` must end at
86    /// `total_len` for the from-end probes to be correct.
87    #[must_use]
88    pub fn with_tail(base: u64, bytes: &'a [u8], total_len: u64, tail: &'a [u8]) -> Self {
89        Self {
90            base,
91            bytes,
92            total_len,
93            tail,
94        }
95    }
96
97    /// The absolute offset the window starts at.
98    #[must_use]
99    pub fn base(&self) -> u64 {
100        self.base
101    }
102
103    /// The window (head) bytes.
104    #[must_use]
105    pub fn bytes(&self) -> &[u8] {
106        self.bytes
107    }
108
109    /// The total length of the source this window was sniffed from.
110    #[must_use]
111    pub fn total_len(&self) -> u64 {
112        self.total_len
113    }
114
115    /// The `n` bytes at window-relative `off`, or `None` if out of range. Never
116    /// panics — the panic-free way to test a magic.
117    #[must_use]
118    pub fn at(&self, off: usize, n: usize) -> Option<&[u8]> {
119        let end = off.checked_add(n)?;
120        self.bytes.get(off..end)
121    }
122
123    /// True when the window has `magic` at window-relative `off`.
124    #[must_use]
125    pub fn has_magic(&self, off: usize, magic: &[u8]) -> bool {
126        self.at(off, magic.len()) == Some(magic)
127    }
128
129    /// The `n` bytes of the tail beginning `from_end` bytes before the source's
130    /// end, or `None` when the tail is too short. Never panics.
131    #[must_use]
132    pub fn tail_at(&self, from_end: usize, n: usize) -> Option<&[u8]> {
133        let start = self.tail.len().checked_sub(from_end)?;
134        let end = start.checked_add(n)?;
135        self.tail.get(start..end)
136    }
137
138    /// True when the tail carries `magic` starting `from_end` bytes before the
139    /// source's end (e.g. `has_magic_from_end(512, b"koly")` for a DMG footer).
140    /// False if the tail is too short — the panic-free trailer probe.
141    #[must_use]
142    pub fn has_magic_from_end(&self, from_end: usize, magic: &[u8]) -> bool {
143        self.tail_at(from_end, magic.len()) == Some(magic)
144    }
145}
146
147/// Decodes an outer container to a raw byte stream.
148pub trait ContainerOpen: Send + Sync {
149    fn format(&self) -> ContainerFormat;
150    fn probe(&self, w: &SniffWindow) -> Confidence;
151    fn open(&self, src: DynSource) -> VfsResult<DynSource>;
152}
153
154/// Recognizes and opens a partitioning/volume scheme.
155pub trait VolumeSystemOpen: Send + Sync {
156    fn scheme(&self) -> VolumeScheme;
157    fn probe(&self, w: &SniffWindow) -> Confidence;
158    fn open(&self, src: DynSource) -> VfsResult<Box<dyn VolumeSystem>>;
159}
160
161/// Recognizes and opens a full-disk-encryption layer.
162pub trait EncryptionOpen: Send + Sync {
163    fn scheme(&self) -> crate::encryption::EncryptionScheme;
164    fn probe(&self, w: &SniffWindow) -> Confidence;
165    fn open(&self, src: DynSource) -> VfsResult<Box<dyn EncryptionLayer>>;
166}
167
168/// Recognizes and mounts a filesystem.
169pub trait FileSystemOpen: Send + Sync {
170    fn kind(&self) -> FsKind;
171    fn probe(&self, w: &SniffWindow) -> Confidence;
172    fn open(&self, src: DynSource) -> VfsResult<DynFs>;
173}
174
175/// Recognizes and peels an archive/compression wrapper: a bare gzip/bzip2 stream
176/// decodes to a single inner [`DynSource`] ([`ArchiveContents::Stream`], 1→1),
177/// while a multi-member archive (tar/zip/7z) yields its member table
178/// ([`ArchiveContents::Members`], 1→N). Each result re-enters resolution exactly
179/// like a container decode. See ADR 0008 (archives resolve as a first-class layer);
180/// the concrete decoder lives in the `archive-core` adapter, never in this leaf.
181pub trait ArchiveOpen: Send + Sync {
182    fn probe(&self, w: &SniffWindow) -> Confidence;
183    fn open(&self, src: DynSource) -> VfsResult<ArchiveContents>;
184}
185
186/// The compiled-in dispatch table. Populated by the engine's `default_openers()`;
187/// held here so any tool/test can build one without a circular dep through a
188/// binary crate.
189#[derive(Default)]
190pub struct Openers {
191    containers: Vec<Box<dyn ContainerOpen>>,
192    volume_systems: Vec<Box<dyn VolumeSystemOpen>>,
193    encryption: Vec<Box<dyn EncryptionOpen>>,
194    filesystems: Vec<Box<dyn FileSystemOpen>>,
195    archives: Vec<Box<dyn ArchiveOpen>>,
196}
197
198impl Openers {
199    /// An empty registry.
200    #[must_use]
201    pub fn new() -> Self {
202        Self::default()
203    }
204
205    /// Register a container decoder (builder style).
206    #[must_use]
207    pub fn container(mut self, d: impl ContainerOpen + 'static) -> Self {
208        self.containers.push(Box::new(d));
209        self
210    }
211
212    /// Register a volume-system prober.
213    #[must_use]
214    pub fn volume_system(mut self, p: impl VolumeSystemOpen + 'static) -> Self {
215        self.volume_systems.push(Box::new(p));
216        self
217    }
218
219    /// Register a encryption prober.
220    #[must_use]
221    pub fn encryption(mut self, p: impl EncryptionOpen + 'static) -> Self {
222        self.encryption.push(Box::new(p));
223        self
224    }
225
226    /// Register a filesystem prober.
227    #[must_use]
228    pub fn filesystem(mut self, p: impl FileSystemOpen + 'static) -> Self {
229        self.filesystems.push(Box::new(p));
230        self
231    }
232
233    /// Register an archive prober (gzip/bzip2/tar/zip/7z peel).
234    #[must_use]
235    pub fn archive(mut self, a: impl ArchiveOpen + 'static) -> Self {
236        self.archives.push(Box::new(a));
237        self
238    }
239
240    /// The registered container decoders, in registration order.
241    #[must_use]
242    pub fn containers(&self) -> &[Box<dyn ContainerOpen>] {
243        &self.containers
244    }
245    /// The registered volume-system probers.
246    #[must_use]
247    pub fn volume_systems(&self) -> &[Box<dyn VolumeSystemOpen>] {
248        &self.volume_systems
249    }
250    /// The registered encryption probers.
251    #[must_use]
252    pub fn encryption_layers(&self) -> &[Box<dyn EncryptionOpen>] {
253        &self.encryption
254    }
255    /// The registered filesystem probers.
256    #[must_use]
257    pub fn filesystems(&self) -> &[Box<dyn FileSystemOpen>] {
258        &self.filesystems
259    }
260    /// The registered archive probers.
261    #[must_use]
262    pub fn archives(&self) -> &[Box<dyn ArchiveOpen>] {
263        &self.archives
264    }
265}