Skip to main content

forensic_vfs/
pathspec.rs

1//! [`PathSpec`] — the recursive, self-describing locator.
2//!
3//! A chain of [`Layer`] nodes, each naming one layer, its location within that
4//! layer, and its parent. It is the cache key, the reproducibility record, and
5//! what a report cites. It carries **no credentials** — an address, not a
6//! keychain. Identity is the structured enum (derived `Hash`/`Eq`), never a
7//! stringification, so raw path bytes containing a delimiter cannot collide two
8//! specs. Two text forms exist (see [`crate::uri`]): a lossless canonical URI and
9//! a lossy human `Display`.
10
11use std::path::PathBuf;
12
13use crate::encryption::EncryptionScheme;
14use crate::fs::{FileId, FsKind, StreamId};
15use crate::registry::ContainerFormat;
16use crate::volume::VolumeScheme;
17
18/// A 16-byte GUID (GPT partition type/id, volume identifier).
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub struct Guid(pub [u8; 16]);
21
22/// A reference to one snapshot within a snapshot set.
23#[non_exhaustive]
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub enum SnapshotRef {
26    /// VSS store index.
27    VssStore(usize),
28    /// APFS snapshot transaction id.
29    ApfsXid(u64),
30}
31
32/// How a node is addressed within a filesystem.
33#[non_exhaustive]
34#[derive(Debug, Clone, PartialEq, Eq, Hash)]
35pub enum NodeAddr {
36    /// Raw path components — filesystem names are bytes, not guaranteed UTF-8.
37    Path(Vec<Vec<u8>>),
38    /// The filesystem-specific stable id (survives a reallocated slot).
39    File(FileId),
40    /// Both: resolve by id, keep the observed path for context.
41    Both { path: Vec<Vec<u8>>, id: FileId },
42}
43
44/// One layer in the locator chain.
45#[non_exhaustive]
46#[derive(Debug, Clone, PartialEq, Eq, Hash)]
47pub enum Layer {
48    /// The base OS path — the only parentless layer.
49    Os { path: PathBuf },
50    /// A byte window of the parent.
51    Range { start: u64, len: u64 },
52    /// Decode a container (Auto = sniffed).
53    Container { format: ContainerFormat },
54    /// A volume within a volume system.
55    Volume {
56        scheme: VolumeScheme,
57        index: usize,
58        guid: Option<Guid>,
59    },
60    /// A full-disk-encryption translation (credentials supplied out-of-band).
61    Encryption { scheme: EncryptionScheme },
62    /// A snapshot/shadow store.
63    Snapshot { store: SnapshotRef },
64    /// A mounted filesystem, addressing one node.
65    Fs { kind: FsKind, at: NodeAddr },
66    /// A named data stream (ADS / resource fork) of the addressed node.
67    Stream { id: StreamId },
68    /// A peeled archive/compression wrapper. `member: None` is a 1→1 stream peel
69    /// (a bare gzip/bzip2 wrapper re-entering resolution like a container decode);
70    /// `member: Some(i)` is the `i`-th member of a multi-member archive (tar/zip/7z).
71    Archive { member: Option<usize> },
72}
73
74/// The recursive locator. Constructed via [`PathSpec::os`] + [`PathSpec::push`],
75/// so a new `Layer` variant is an additive change and callers never build the
76/// chain by hand-nesting `Box`es.
77#[derive(Debug, Clone, PartialEq, Eq, Hash)]
78pub struct PathSpec {
79    pub layer: Layer,
80    pub parent: Option<Box<PathSpec>>,
81}
82
83impl PathSpec {
84    /// A base spec rooted at an OS path.
85    #[must_use]
86    pub fn os(path: impl Into<PathBuf>) -> Self {
87        Self {
88            layer: Layer::Os { path: path.into() },
89            parent: None,
90        }
91    }
92
93    /// A raw base spec for any layer (no parent). Prefer [`PathSpec::os`] for the
94    /// root; this exists for tests and re-parenting.
95    #[must_use]
96    pub fn root(layer: Layer) -> Self {
97        Self {
98            layer,
99            parent: None,
100        }
101    }
102
103    /// Extend the chain: `self` becomes the parent of a new node carrying `layer`.
104    #[must_use]
105    pub fn push(self, layer: Layer) -> Self {
106        Self {
107            layer,
108            parent: Some(Box::new(self)),
109        }
110    }
111
112    /// The number of layers in the chain (the root counts as one).
113    #[must_use]
114    pub fn depth(&self) -> usize {
115        1 + self.parent.as_ref().map_or(0, |p| p.depth())
116    }
117
118    /// The root (parentless) spec of this chain.
119    #[must_use]
120    pub fn base(&self) -> &PathSpec {
121        match &self.parent {
122            Some(p) => p.base(),
123            None => self,
124        }
125    }
126
127    /// The chain from root to this node, root first.
128    #[must_use]
129    pub fn layers(&self) -> Vec<&Layer> {
130        let mut out = match &self.parent {
131            Some(p) => p.layers(),
132            None => Vec::new(),
133        };
134        out.push(&self.layer);
135        out
136    }
137}