1use std::ops::ControlFlow;
2
3use md5::{Digest, Md5};
4use sha1::Sha1;
5use sha2::Sha256;
6
7use crate::index::logical_chunk_count;
8use crate::{EwfError, Image, Result, VerifyResult};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12#[must_use]
13pub struct VerifyOptions {
14 expected_md5: Option<[u8; 16]>,
15 expected_sha1: Option<[u8; 20]>,
16 expected_sha256: Option<[u8; 32]>,
17 parallelism: usize,
18 chunk_buffer_size_bytes: usize,
19 maximum_findings: usize,
20}
21
22impl Default for VerifyOptions {
23 fn default() -> Self {
24 Self {
25 expected_md5: None,
26 expected_sha1: None,
27 expected_sha256: None,
28 parallelism: 1,
29 chunk_buffer_size_bytes: 8 * 1024 * 1024,
30 maximum_findings: 1024,
31 }
32 }
33}
34
35impl VerifyOptions {
36 pub fn with_expected_md5(mut self, expected: [u8; 16]) -> Self {
38 self.expected_md5 = Some(expected);
39 self
40 }
41
42 pub fn with_expected_sha1(mut self, expected: [u8; 20]) -> Self {
44 self.expected_sha1 = Some(expected);
45 self
46 }
47
48 pub fn with_expected_sha256(mut self, expected: [u8; 32]) -> Self {
50 self.expected_sha256 = Some(expected);
51 self
52 }
53
54 pub fn with_parallelism(mut self, workers: usize) -> Self {
57 self.parallelism = workers;
58 self
59 }
60
61 pub fn with_chunk_buffer_size_bytes(mut self, bytes: usize) -> Self {
65 self.chunk_buffer_size_bytes = bytes;
66 self
67 }
68
69 pub fn with_maximum_findings(mut self, count: usize) -> Self {
72 self.maximum_findings = count;
73 self
74 }
75
76 pub fn parallelism(&self) -> usize {
78 self.parallelism
79 }
80
81 pub fn chunk_buffer_size_bytes(&self) -> usize {
83 self.chunk_buffer_size_bytes
84 }
85
86 pub fn maximum_findings(&self) -> usize {
88 self.maximum_findings
89 }
90
91 pub(crate) fn validate(&self) -> Result<()> {
92 if !(1..=64).contains(&self.parallelism) || self.chunk_buffer_size_bytes == 0 {
93 return Err(std::io::Error::new(
94 std::io::ErrorKind::InvalidInput,
95 "verification requires 1..=64 workers and a nonzero chunk buffer budget",
96 )
97 .into());
98 }
99 if self.parallelism > 1 && !cfg!(feature = "parallel") {
100 return Err(EwfError::Unsupported("parallel feature is disabled".into()));
101 }
102 Ok(())
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
108#[cfg_attr(feature = "serde", derive(serde::Serialize))]
109#[non_exhaustive]
110pub struct ComputedHashes {
111 pub md5: [u8; 16],
113 pub sha1: [u8; 20],
115 pub sha256: [u8; 32],
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121#[cfg_attr(feature = "serde", derive(serde::Serialize))]
122pub enum HashAlgorithm {
123 Md5,
125 Sha1,
127 Sha256,
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133#[cfg_attr(feature = "serde", derive(serde::Serialize))]
134pub enum HashReference {
135 Stored,
137 External,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
143#[cfg_attr(feature = "serde", derive(serde::Serialize))]
144#[non_exhaustive]
145pub struct HashComparison {
146 pub algorithm: HashAlgorithm,
148 pub reference: HashReference,
150 pub expected: Vec<u8>,
152 pub computed: Vec<u8>,
154 pub matches: bool,
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160#[cfg_attr(feature = "serde", derive(serde::Serialize))]
161#[non_exhaustive]
162pub struct VerifyProgress {
163 pub bytes_processed: u64,
165 pub bytes_verified: u64,
167 pub bytes_total: u64,
169 pub chunks_processed: u64,
171 pub chunks_total: u64,
173}
174
175#[derive(Debug, Clone, PartialEq, Eq)]
177#[cfg_attr(feature = "serde", derive(serde::Serialize))]
178#[non_exhaustive]
179pub struct VerificationReport {
180 pub hashes: ComputedHashes,
182 pub comparisons: Vec<HashComparison>,
184 pub bytes_verified: u64,
186}
187
188impl VerificationReport {
189 pub fn references_match(&self) -> Option<bool> {
191 (!self.comparisons.is_empty()).then(|| self.comparisons.iter().all(|item| item.matches))
192 }
193}
194
195pub(crate) struct MediaScan {
196 pub(crate) hashes: Option<ComputedHashes>,
197 pub(crate) progress: VerifyProgress,
198}
199
200impl Image {
201 pub fn verify(&self) -> Result<VerifyResult> {
205 let report = self.verify_with_options(&VerifyOptions::default())?;
206 Ok(VerifyResult {
207 computed_md5: Some(report.hashes.md5),
208 computed_sha1: Some(report.hashes.sha1),
209 computed_sha256: Some(report.hashes.sha256),
210 md5_match: self.md5_hash().map(|hash| hash == report.hashes.md5),
211 sha1_match: self.sha1_hash().map(|hash| hash == report.hashes.sha1),
212 sha256_match: report
213 .comparisons
214 .iter()
215 .find(|item| {
216 item.algorithm == HashAlgorithm::Sha256
217 && item.reference == HashReference::Stored
218 })
219 .map(|item| item.matches),
220 })
221 }
222
223 pub fn verify_with_options(&self, options: &VerifyOptions) -> Result<VerificationReport> {
226 self.verify_with_progress(options, |_| ControlFlow::Continue(()))
227 }
228
229 pub fn verify_with_progress(
234 &self,
235 options: &VerifyOptions,
236 progress: impl FnMut(VerifyProgress) -> ControlFlow<()>,
237 ) -> Result<VerificationReport> {
238 let scan = self.scan_media(options, progress, |_, error| Err(error))?;
239 let hashes = scan
240 .hashes
241 .expect("successful verification has complete hashes");
242 let comparisons = self.compare_hashes(&hashes, options);
243 Ok(VerificationReport {
244 hashes,
245 comparisons,
246 bytes_verified: scan.progress.bytes_verified,
247 })
248 }
249
250 pub(crate) fn compare_hashes(
251 &self,
252 hashes: &ComputedHashes,
253 options: &VerifyOptions,
254 ) -> Vec<HashComparison> {
255 let references = [
256 (
257 HashAlgorithm::Sha256,
258 HashReference::Stored,
259 self.info()
260 .stored_hashes
261 .hash_value("SHA256")
262 .and_then(crate::types::parse_hex_array::<32>)
263 .map(|v| v.to_vec()),
264 hashes.sha256.as_slice(),
265 ),
266 (
267 HashAlgorithm::Md5,
268 HashReference::Stored,
269 self.md5_hash().map(|v| v.to_vec()),
270 hashes.md5.as_slice(),
271 ),
272 (
273 HashAlgorithm::Sha1,
274 HashReference::Stored,
275 self.sha1_hash().map(|v| v.to_vec()),
276 hashes.sha1.as_slice(),
277 ),
278 (
279 HashAlgorithm::Md5,
280 HashReference::External,
281 options.expected_md5.map(|v| v.to_vec()),
282 hashes.md5.as_slice(),
283 ),
284 (
285 HashAlgorithm::Sha1,
286 HashReference::External,
287 options.expected_sha1.map(|v| v.to_vec()),
288 hashes.sha1.as_slice(),
289 ),
290 (
291 HashAlgorithm::Sha256,
292 HashReference::External,
293 options.expected_sha256.map(|v| v.to_vec()),
294 hashes.sha256.as_slice(),
295 ),
296 ];
297 references
298 .into_iter()
299 .filter_map(|(algorithm, reference, expected, computed)| {
300 expected.map(|expected| HashComparison {
301 algorithm,
302 reference,
303 matches: expected == computed,
304 expected,
305 computed: computed.to_vec(),
306 })
307 })
308 .collect()
309 }
310
311 pub(crate) fn scan_media(
312 &self,
313 options: &VerifyOptions,
314 mut on_progress: impl FnMut(VerifyProgress) -> ControlFlow<()>,
315 mut on_error: impl FnMut(u64, EwfError) -> Result<()>,
316 ) -> Result<MediaScan> {
317 options.validate()?;
318 self.ensure_not_aborted()?;
319 let chunk_size = self.chunk_size();
320 let count = logical_chunk_count(self.media_size(), chunk_size)?;
321 let mut progress = VerifyProgress {
322 bytes_processed: 0,
323 bytes_verified: 0,
324 bytes_total: self.media_size(),
325 chunks_processed: 0,
326 chunks_total: count,
327 };
328 report_progress(self, &mut on_progress, progress)?;
329 let budget_chunks = (options.chunk_buffer_size_bytes as u64 / chunk_size).max(1);
330 let batch_size = budget_chunks.min(options.parallelism as u64 * 2).max(1);
331 #[cfg(feature = "parallel")]
332 let pool = if options.parallelism > 1 && count > 1 && batch_size > 1 {
333 Some(
334 rayon::ThreadPoolBuilder::new()
335 .num_threads(options.parallelism.min(batch_size as usize))
336 .build()
337 .map_err(std::io::Error::other)?,
338 )
339 } else {
340 None
341 };
342 let mut md5 = Md5::new();
343 let mut sha1 = Sha1::new();
344 let mut sha256 = Sha256::new();
345 let mut failed = false;
346 let mut start = 0;
347 while start < count {
348 self.ensure_not_aborted()?;
349 let end = start.saturating_add(batch_size).min(count);
350 let indices: Vec<u64> = (start..end).collect();
351 #[cfg(feature = "parallel")]
352 let chunks: Vec<Result<Vec<u8>>> = if let Some(pool) = &pool {
353 use rayon::prelude::*;
354 pool.install(|| {
355 indices
356 .par_iter()
357 .map(|&id| self.verification_chunk(id))
358 .collect()
359 })
360 } else {
361 indices
362 .iter()
363 .map(|&id| self.verification_chunk(id))
364 .collect()
365 };
366 #[cfg(not(feature = "parallel"))]
367 let chunks: Vec<Result<Vec<u8>>> = indices
368 .iter()
369 .map(|&id| self.verification_chunk(id))
370 .collect();
371 for (id, chunk) in indices.into_iter().zip(chunks) {
372 self.ensure_not_aborted()?;
373 let size = chunk_size.min(self.media_size() - progress.bytes_processed);
374 match chunk {
375 Ok(bytes) if bytes.len() as u64 == size => {
376 md5.update(&bytes);
377 sha1.update(&bytes);
378 sha256.update(&bytes);
379 progress.bytes_verified += size;
380 }
381 Ok(_) => {
382 failed = true;
383 on_error(
384 id,
385 EwfError::Malformed(
386 "decoded chunk length differs from logical media range".into(),
387 ),
388 )?;
389 }
390 Err(EwfError::Aborted) => return Err(EwfError::Aborted),
391 Err(error) => {
392 failed = true;
393 on_error(id, error)?;
394 }
395 }
396 progress.chunks_processed += 1;
397 progress.bytes_processed += size;
398 report_progress(self, &mut on_progress, progress)?;
399 }
400 start = end;
401 }
402 Ok(MediaScan {
403 hashes: (!failed).then(|| ComputedHashes {
404 md5: md5.finalize().into(),
405 sha1: sha1.finalize().into(),
406 sha256: sha256.finalize().into(),
407 }),
408 progress,
409 })
410 }
411}
412
413fn report_progress(
414 image: &Image,
415 callback: &mut impl FnMut(VerifyProgress) -> ControlFlow<()>,
416 progress: VerifyProgress,
417) -> Result<()> {
418 if callback(progress).is_break() {
419 return Err(EwfError::Aborted);
420 }
421 image.ensure_not_aborted()
422}