ewf_forensic/
integrity_path.rs1use crate::integrity::{AnalysisProgress, ComputedHashes, EwfIntegrity, EwfIntegrityAnomaly};
2use memmap2::Mmap;
3use std::fs::File;
4use std::io;
5use std::path::{Path, PathBuf};
6
7pub 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 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 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 pub fn with_expected_md5(mut self, hash: [u8; 16]) -> Self {
56 self.expected_md5 = Some(hash);
57 self
58 }
59
60 pub fn with_expected_sha1(mut self, hash: [u8; 20]) -> Self {
62 self.expected_sha1 = Some(hash);
63 self
64 }
65
66 pub fn with_expected_sha256(mut self, hash: [u8; 32]) -> Self {
69 self.expected_sha256 = Some(hash);
70 self
71 }
72
73 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 unsafe { Mmap::map(&file) }
84 })
85 .collect::<io::Result<Vec<Mmap>>>()?;
86 let seg_refs: Vec<&[u8]> = mmaps.iter().map(|m| m.as_ref()).collect();
87 Ok(EwfIntegrity::from_segments(&seg_refs).compute_hashes())
88 }
89
90 pub fn analyse(&self) -> io::Result<Vec<EwfIntegrityAnomaly>> {
94 let mmaps = self
95 .segment_paths
96 .iter()
97 .map(|p| {
98 let file = File::open(p)?;
99 unsafe { Mmap::map(&file) }
102 })
103 .collect::<io::Result<Vec<Mmap>>>()?;
104
105 let seg_refs: Vec<&[u8]> = mmaps.iter().map(|m| m.as_ref()).collect();
106
107 let mut checker = EwfIntegrity::from_segments(&seg_refs);
108 if let Some(h) = self.expected_md5 {
109 checker = checker.with_expected_md5(h);
110 }
111 if let Some(h) = self.expected_sha1 {
112 checker = checker.with_expected_sha1(h);
113 }
114 if let Some(h) = self.expected_sha256 {
115 checker = checker.with_expected_sha256(h);
116 }
117
118 Ok(checker.analyse())
119 }
120
121 pub fn analyse_and_compute_hashes(
129 &self,
130 ) -> io::Result<(Vec<EwfIntegrityAnomaly>, ComputedHashes)> {
131 let mmaps = self
132 .segment_paths
133 .iter()
134 .map(|p| {
135 let file = File::open(p)?;
136 unsafe { Mmap::map(&file) }
138 })
139 .collect::<io::Result<Vec<Mmap>>>()?;
140
141 let seg_refs: Vec<&[u8]> = mmaps.iter().map(|m| m.as_ref()).collect();
142
143 let mut checker = EwfIntegrity::from_segments(&seg_refs);
144 if let Some(h) = self.expected_md5 {
145 checker = checker.with_expected_md5(h);
146 }
147 if let Some(h) = self.expected_sha1 {
148 checker = checker.with_expected_sha1(h);
149 }
150 if let Some(h) = self.expected_sha256 {
151 checker = checker.with_expected_sha256(h);
152 }
153
154 let anomalies = checker.analyse();
155 let hashes = EwfIntegrity::from_segments(&seg_refs)
156 .compute_hashes()
157 .unwrap_or(ComputedHashes { md5: [0u8; 16], sha1: [0u8; 20], sha256: [0u8; 32] });
158
159 Ok((anomalies, hashes))
160 }
161
162 pub fn analyse_with_progress(
170 &self,
171 mut progress: impl FnMut(AnalysisProgress),
172 ) -> io::Result<(Vec<EwfIntegrityAnomaly>, ())> {
173 let mmaps = self
174 .segment_paths
175 .iter()
176 .map(|p| {
177 let file = File::open(p)?;
178 unsafe { Mmap::map(&file) }
180 })
181 .collect::<io::Result<Vec<Mmap>>>()?;
182
183 let seg_refs: Vec<&[u8]> = mmaps.iter().map(|m| m.as_ref()).collect();
184
185 let mut checker = EwfIntegrity::from_segments(&seg_refs);
186 if let Some(h) = self.expected_md5 {
187 checker = checker.with_expected_md5(h);
188 }
189 if let Some(h) = self.expected_sha1 {
190 checker = checker.with_expected_sha1(h);
191 }
192 if let Some(h) = self.expected_sha256 {
193 checker = checker.with_expected_sha256(h);
194 }
195
196 let anomalies = checker.analyse_with_progress(&mut progress);
197 Ok((anomalies, ()))
198 }
199}
200
201fn discover_segments(base: &Path) -> Vec<PathBuf> {
209 let ext = match base.extension().and_then(|e| e.to_str()) {
210 Some(e) => e,
211 None => return vec![base.to_path_buf()],
212 };
213
214 let (prefix_char, has_x, digits) = match parse_ewf_extension(ext) {
216 Some(v) => v,
217 None => return vec![base.to_path_buf()],
218 };
219
220 let stem = match base.file_stem().and_then(|s| s.to_str()) {
221 Some(s) => s,
222 None => return vec![base.to_path_buf()],
223 };
224 let dir = base.parent().unwrap_or(Path::new("."));
225
226 let mut segments = Vec::new();
227 for n in 1u32.. {
228 let ext_str = make_ewf_extension(prefix_char, has_x, digits, n);
229 let candidate = dir.join(format!("{stem}.{ext_str}"));
230 if candidate.exists() {
231 segments.push(candidate);
232 } else {
233 break;
234 }
235 if n >= 999 {
236 break;
237 }
238 }
239
240 if segments.is_empty() {
241 vec![base.to_path_buf()]
242 } else {
243 segments
244 }
245}
246
247fn parse_ewf_extension(ext: &str) -> Option<(char, bool, usize)> {
250 let mut chars = ext.chars();
251 let prefix = chars.next()?;
252 if !prefix.is_ascii_alphabetic() {
253 return None;
254 }
255 let rest: String = chars.collect();
256 let has_x = rest.starts_with('x') || rest.starts_with('X');
257 let rest = rest.trim_start_matches(|c| c == 'x' || c == 'X');
258 if rest.chars().all(|c| c.is_ascii_digit()) && !rest.is_empty() {
259 Some((prefix, has_x, rest.len()))
260 } else {
261 None
262 }
263}
264
265fn make_ewf_extension(prefix: char, has_x: bool, digit_count: usize, n: u32) -> String {
268 let width = digit_count.max(2);
269 let x = if has_x { "x" } else { "" };
270 format!("{}{}{:0width$}", prefix, x, n, width = width)
271}