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 #[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 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 #[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 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 #[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 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 #[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
218fn 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 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
264fn 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
282fn 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}