Skip to main content

ewf_forensic/
integrity_path.rs

1use crate::integrity::{AnalysisProgress, ComputedHashes, EwfIntegrity, EwfIntegrityAnomaly};
2use memmap2::Mmap;
3use std::fs::File;
4use std::io;
5use std::path::{Path, PathBuf};
6
7/// Path-based, mmap-backed EWF integrity analyser.
8///
9/// Unlike [`EwfIntegrity`] (which takes `&[u8]` slices already in memory),
10/// `EwfIntegrityPath` opens segment files and memory-maps them read-only.
11/// The OS pages data on demand, so 500 GB evidence files are handled without
12/// loading them into RAM.
13///
14/// # Segment auto-discovery
15///
16/// [`from_path`][EwfIntegrityPath::from_path] accepts the first segment
17/// (`evidence.E01` / `evidence.e01`) and automatically discovers consecutive
18/// siblings (`E02`, `E03`, … up to `EZZ`) in the same directory. Pass
19/// [`from_paths`][EwfIntegrityPath::from_paths] to supply the segment list
20/// explicitly.
21pub struct EwfIntegrityPath {
22    segment_paths: Vec<PathBuf>,
23    expected_md5: Option<[u8; 16]>,
24    expected_sha1: Option<[u8; 20]>,
25    expected_sha256: Option<[u8; 32]>,
26}
27
28impl EwfIntegrityPath {
29    /// Analyse a single segment or auto-discover a multi-segment image.
30    ///
31    /// If `path` has an extension matching the EWF numbered-segment pattern
32    /// (`E01`/`e01`, `E02`/`e02`, …) this will look for consecutive siblings
33    /// in the same directory and include them automatically.
34    pub fn from_path(path: impl AsRef<Path>) -> Self {
35        let base = path.as_ref();
36        Self {
37            segment_paths: discover_segments(base),
38            expected_md5: None,
39            expected_sha1: None,
40            expected_sha256: None,
41        }
42    }
43
44    /// Analyse an explicit ordered list of segment paths.
45    pub fn from_paths(paths: &[impl AsRef<Path>]) -> Self {
46        Self {
47            segment_paths: paths.iter().map(|p| p.as_ref().to_path_buf()).collect(),
48            expected_md5: None,
49            expected_sha1: None,
50            expected_sha256: None,
51        }
52    }
53
54    /// Supply an external chain-of-custody MD5 to compare against.
55    pub fn with_expected_md5(mut self, hash: [u8; 16]) -> Self {
56        self.expected_md5 = Some(hash);
57        self
58    }
59
60    /// Supply an external chain-of-custody SHA-1 to compare against.
61    pub fn with_expected_sha1(mut self, hash: [u8; 20]) -> Self {
62        self.expected_sha1 = Some(hash);
63        self
64    }
65
66    /// Supply an external chain-of-custody SHA-256 to compare against.
67    /// Mismatch → `ExternalSha256Mismatch` (Critical).
68    pub fn with_expected_sha256(mut self, hash: [u8; 32]) -> Self {
69        self.expected_sha256 = Some(hash);
70        self
71    }
72
73    /// Memory-map every segment and compute sector data hashes.
74    ///
75    /// Returns `Err` if any segment cannot be opened or mapped.
76    /// Returns `Ok(None)` if the image is unparseable or is EWF v2.
77    pub fn compute_hashes(&self) -> io::Result<Option<ComputedHashes>> {
78        let mmaps = self
79            .segment_paths
80            .iter()
81            .map(|p| {
82                let file = File::open(p)?;
83                // SAFETY: read-only mmap of an immutable evidence file.
84                #[allow(unsafe_code)]
85                unsafe {
86                    Mmap::map(&file)
87                }
88            })
89            .collect::<io::Result<Vec<Mmap>>>()?;
90        let seg_refs: Vec<&[u8]> = mmaps.iter().map(std::convert::AsRef::as_ref).collect();
91        Ok(EwfIntegrity::from_segments(&seg_refs).compute_hashes())
92    }
93
94    /// Memory-map every segment and run the full integrity analyser.
95    ///
96    /// Returns `Err` if any segment file cannot be opened or mapped.
97    pub fn analyse(&self) -> io::Result<Vec<EwfIntegrityAnomaly>> {
98        let mmaps = self
99            .segment_paths
100            .iter()
101            .map(|p| {
102                let file = File::open(p)?;
103                // SAFETY: we open the file read-only and do not modify it.
104                // Concurrent truncation is not a concern for immutable evidence files.
105                #[allow(unsafe_code)]
106                unsafe {
107                    Mmap::map(&file)
108                }
109            })
110            .collect::<io::Result<Vec<Mmap>>>()?;
111
112        let seg_refs: Vec<&[u8]> = mmaps.iter().map(std::convert::AsRef::as_ref).collect();
113
114        let mut checker = EwfIntegrity::from_segments(&seg_refs);
115        if let Some(h) = self.expected_md5 {
116            checker = checker.with_expected_md5(h);
117        }
118        if let Some(h) = self.expected_sha1 {
119            checker = checker.with_expected_sha1(h);
120        }
121        if let Some(h) = self.expected_sha256 {
122            checker = checker.with_expected_sha256(h);
123        }
124
125        Ok(checker.analyse())
126    }
127
128    /// Memory-map every segment once and run analysis + hash computation in a
129    /// single pass, avoiding duplicate I/O compared to calling [`analyse`](Self::analyse) and
130    /// [`compute_hashes`](Self::compute_hashes) separately.
131    ///
132    /// Returns `Err` if any segment file cannot be opened or mapped.
133    /// If the image is too corrupted to compute hashes, a zeroed
134    /// [`ComputedHashes`] is returned alongside the anomalies.
135    pub fn analyse_and_compute_hashes(
136        &self,
137    ) -> io::Result<(Vec<EwfIntegrityAnomaly>, ComputedHashes)> {
138        let mmaps = self
139            .segment_paths
140            .iter()
141            .map(|p| {
142                let file = File::open(p)?;
143                // SAFETY: read-only mmap of an immutable evidence file.
144                #[allow(unsafe_code)]
145                unsafe {
146                    Mmap::map(&file)
147                }
148            })
149            .collect::<io::Result<Vec<Mmap>>>()?;
150
151        let seg_refs: Vec<&[u8]> = mmaps.iter().map(std::convert::AsRef::as_ref).collect();
152
153        let mut checker = EwfIntegrity::from_segments(&seg_refs);
154        if let Some(h) = self.expected_md5 {
155            checker = checker.with_expected_md5(h);
156        }
157        if let Some(h) = self.expected_sha1 {
158            checker = checker.with_expected_sha1(h);
159        }
160        if let Some(h) = self.expected_sha256 {
161            checker = checker.with_expected_sha256(h);
162        }
163
164        let anomalies = checker.analyse();
165        let hashes = EwfIntegrity::from_segments(&seg_refs)
166            .compute_hashes()
167            .unwrap_or(ComputedHashes {
168                md5: [0u8; 16],
169                sha1: [0u8; 20],
170                sha256: [0u8; 32],
171            });
172
173        Ok((anomalies, hashes))
174    }
175
176    /// Run integrity analysis while reporting progress to a callback.
177    ///
178    /// Identical to [`analyse`](Self::analyse) but invokes `progress` after each chunk is
179    /// processed so callers can display a progress bar for large images.
180    ///
181    /// Returns `(anomalies, ())` on success, or `Err` if a segment cannot be
182    /// opened or memory-mapped.
183    pub fn analyse_with_progress(
184        &self,
185        mut progress: impl FnMut(AnalysisProgress),
186    ) -> io::Result<(Vec<EwfIntegrityAnomaly>, ())> {
187        let mmaps = self
188            .segment_paths
189            .iter()
190            .map(|p| {
191                let file = File::open(p)?;
192                // SAFETY: read-only mmap of an immutable evidence file.
193                #[allow(unsafe_code)]
194                unsafe {
195                    Mmap::map(&file)
196                }
197            })
198            .collect::<io::Result<Vec<Mmap>>>()?;
199
200        let seg_refs: Vec<&[u8]> = mmaps.iter().map(std::convert::AsRef::as_ref).collect();
201
202        let mut checker = EwfIntegrity::from_segments(&seg_refs);
203        if let Some(h) = self.expected_md5 {
204            checker = checker.with_expected_md5(h);
205        }
206        if let Some(h) = self.expected_sha1 {
207            checker = checker.with_expected_sha1(h);
208        }
209        if let Some(h) = self.expected_sha256 {
210            checker = checker.with_expected_sha256(h);
211        }
212
213        let anomalies = checker.analyse_with_progress(&mut progress);
214        Ok((anomalies, ()))
215    }
216}
217
218// ── Segment auto-discovery ────────────────────────────────────────────────────
219
220/// Given the path to an E01 segment, return an ordered list of all discovered
221/// sibling segments (E01, E02, … E09, E10, … EZZ).
222///
223/// If the path does not have a recognised numbered-extension, returns a
224/// single-element vec containing the given path.
225fn discover_segments(base: &Path) -> Vec<PathBuf> {
226    let ext = match base.extension().and_then(|e| e.to_str()) {
227        Some(e) => e,
228        None => return vec![base.to_path_buf()],
229    };
230
231    // Match E01 / e01 / Ex01 style (first segment is always *01)
232    let (prefix_char, has_x, digits) = match parse_ewf_extension(ext) {
233        Some(v) => v,
234        None => return vec![base.to_path_buf()],
235    };
236
237    let stem = match base.file_stem().and_then(|s| s.to_str()) {
238        Some(s) => s,
239        None => return vec![base.to_path_buf()],
240    };
241    let dir = base.parent().unwrap_or(Path::new("."));
242
243    let mut segments = Vec::new();
244    for n in 1u32.. {
245        let ext_str = make_ewf_extension(prefix_char, has_x, digits, n);
246        let candidate = dir.join(format!("{stem}.{ext_str}"));
247        if candidate.exists() {
248            segments.push(candidate);
249        } else {
250            break;
251        }
252        if n >= 999 {
253            break;
254        }
255    }
256
257    if segments.is_empty() {
258        vec![base.to_path_buf()]
259    } else {
260        segments
261    }
262}
263
264/// Parse an EWF extension like `E01`, `e01`, `Ex01`, `L01` into
265/// `(prefix_char, has_x, digit_count)`.
266fn parse_ewf_extension(ext: &str) -> Option<(char, bool, usize)> {
267    let mut chars = ext.chars();
268    let prefix = chars.next()?;
269    if !prefix.is_ascii_alphabetic() {
270        return None;
271    }
272    let rest: String = chars.collect();
273    let has_x = rest.starts_with('x') || rest.starts_with('X');
274    let rest = rest.trim_start_matches(['x', 'X']);
275    if rest.chars().all(|c| c.is_ascii_digit()) && !rest.is_empty() {
276        Some((prefix, has_x, rest.len()))
277    } else {
278        None
279    }
280}
281
282/// Reconstruct an EWF extension for segment number `n` (1-based).
283/// `prefix_char` = 'E' or 'e', `has_x` = true for Ex01/Lx01 style.
284fn make_ewf_extension(prefix: char, has_x: bool, digit_count: usize, n: u32) -> String {
285    let width = digit_count.max(2);
286    let x = if has_x { "x" } else { "" };
287    format!("{prefix}{x}{n:0width$}")
288}