forensic_vfs/locator.rs
1//! [`Locator`] — the recursive, self-describing access-route 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//! locators. Two text forms exist (see [`crate::uri`]): a lossless canonical URI
9//! and 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 file path — the only parentless layer (a real file: local disk,
49 /// USB, network share, FUSE mount).
50 File { path: PathBuf },
51 /// A byte window of the parent.
52 Range { start: u64, len: u64 },
53 /// Decode a container (Auto = sniffed).
54 Container { format: ContainerFormat },
55 /// A volume within a volume system.
56 Volume {
57 scheme: VolumeScheme,
58 index: usize,
59 guid: Option<Guid>,
60 },
61 /// A full-disk-encryption translation (credentials supplied out-of-band).
62 Encryption { scheme: EncryptionScheme },
63 /// A snapshot/shadow store.
64 Snapshot { store: SnapshotRef },
65 /// A mounted filesystem, addressing one node.
66 Fs { kind: FsKind, at: NodeAddr },
67 /// A named data stream (ADS / resource fork) of the addressed node.
68 Stream { id: StreamId },
69 /// A peeled archive/compression wrapper. `member: None` is a 1→1 stream peel
70 /// (a bare gzip/bzip2 wrapper re-entering resolution like a container decode);
71 /// `member: Some(i)` is the `i`-th member of a multi-member archive (tar/zip/7z).
72 Archive { member: Option<usize> },
73}
74
75/// The recursive locator. Constructed via [`Locator::file`] + [`Locator::push`],
76/// so a new `Layer` variant is an additive change and callers never build the
77/// chain by hand-nesting `Box`es.
78#[derive(Debug, Clone, PartialEq, Eq, Hash)]
79pub struct Locator {
80 pub layer: Layer,
81 pub parent: Option<Box<Locator>>,
82}
83
84impl Locator {
85 /// A base locator rooted at a real file path (local disk, USB, network
86 /// share, FUSE mount — medium-agnostic).
87 #[must_use]
88 pub fn file(path: impl Into<PathBuf>) -> Self {
89 Self {
90 layer: Layer::File { path: path.into() },
91 parent: None,
92 }
93 }
94
95 /// Deprecated alias for [`Locator::file`] (renamed in forensic-vfs 0.6).
96 #[deprecated(note = "renamed to file()")]
97 #[must_use]
98 pub fn os(path: impl Into<PathBuf>) -> Self {
99 Self::file(path)
100 }
101
102 /// A raw base locator for any layer (no parent). Prefer [`Locator::file`] for
103 /// the root; this exists for tests and re-parenting.
104 #[must_use]
105 pub fn root(layer: Layer) -> Self {
106 Self {
107 layer,
108 parent: None,
109 }
110 }
111
112 /// Extend the chain: `self` becomes the parent of a new node carrying `layer`.
113 #[must_use]
114 pub fn push(self, layer: Layer) -> Self {
115 Self {
116 layer,
117 parent: Some(Box::new(self)),
118 }
119 }
120
121 /// The number of layers in the chain (the root counts as one).
122 #[must_use]
123 pub fn depth(&self) -> usize {
124 1 + self.parent.as_ref().map_or(0, |p| p.depth())
125 }
126
127 /// The root (parentless) locator of this chain.
128 #[must_use]
129 pub fn base(&self) -> &Locator {
130 match &self.parent {
131 Some(p) => p.base(),
132 None => self,
133 }
134 }
135
136 /// The chain from root to this node, root first.
137 #[must_use]
138 pub fn layers(&self) -> Vec<&Layer> {
139 let mut out = match &self.parent {
140 Some(p) => p.layers(),
141 None => Vec::new(),
142 };
143 out.push(&self.layer);
144 out
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::{Layer, Locator};
151
152 #[test]
153 #[allow(deprecated)]
154 fn deprecated_os_alias_matches_file() {
155 // The one-release compat shim (ADR 0013): `Locator::os()` must behave
156 // identically to the renamed `Locator::file()`, including seeding the
157 // renamed `Layer::File` base variant.
158 let via_os = Locator::os("/evidence/DC01.E01");
159 assert_eq!(via_os, Locator::file("/evidence/DC01.E01"));
160 assert!(matches!(via_os.layer, Layer::File { .. }));
161 }
162}