1use std::ops::ControlFlow;
2use std::path::Path;
3
4use crate::{
5 ComputedHashes, EwfError, EwfPassword, HashAlgorithm, HashComparison, HashReference, Image,
6 OpenStrictness, Result, SectionInfo, SectionKind, VerifyOptions, VerifyProgress,
7};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize))]
12pub enum IntegritySeverity {
13 Warning,
15 Error,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize))]
22#[non_exhaustive]
23pub enum IntegrityFindingKind {
24 InvalidStructure,
26 InvalidSignature,
28 UnsupportedFeature,
30 PasswordRequired,
32 DecryptionFailed,
34 ChunkUnreadable,
36 RedundantTableMismatch,
38 TableCheckUnavailable,
40 MissingStoredHashes,
42 AcquisitionErrors,
44 IncompleteAcquisition,
46 LenientOpen,
48 HashMismatch {
50 algorithm: HashAlgorithm,
52 reference: HashReference,
54 },
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
59#[cfg_attr(feature = "serde", derive(serde::Serialize))]
60#[non_exhaustive]
61pub struct IntegrityFinding {
62 pub kind: IntegrityFindingKind,
64 pub severity: IntegritySeverity,
66 pub message: String,
68 pub segment_index: Option<usize>,
70 pub segment_offset: Option<u64>,
72 pub chunk_index: Option<u64>,
74 pub logical_offset: Option<u64>,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80#[cfg_attr(feature = "serde", derive(serde::Serialize))]
81pub enum MediaScanStatus {
82 Complete,
84 Incomplete,
86 Unavailable,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
92#[cfg_attr(feature = "serde", derive(serde::Serialize))]
93#[non_exhaustive]
94pub struct IntegrityReport {
95 pub media_status: MediaScanStatus,
97 pub bytes_verified: u64,
99 pub bytes_total: Option<u64>,
101 pub hashes: Option<ComputedHashes>,
103 pub comparisons: Vec<HashComparison>,
105 pub findings: Vec<IntegrityFinding>,
107 pub suppressed_findings: u64,
109 pub error_count: u64,
111 pub warning_count: u64,
113}
114
115impl IntegrityReport {
116 fn new() -> Self {
117 Self {
118 media_status: MediaScanStatus::Unavailable,
119 bytes_verified: 0,
120 bytes_total: None,
121 hashes: None,
122 comparisons: Vec::new(),
123 findings: Vec::new(),
124 suppressed_findings: 0,
125 error_count: 0,
126 warning_count: 0,
127 }
128 }
129
130 fn push(&mut self, finding: IntegrityFinding, limit: usize) {
131 match finding.severity {
132 IntegritySeverity::Error => self.error_count += 1,
133 IntegritySeverity::Warning => self.warning_count += 1,
134 }
135 if self.findings.len() < limit {
136 self.findings.push(finding);
137 } else {
138 self.suppressed_findings += 1;
139 }
140 }
141}
142
143fn finding(
144 kind: IntegrityFindingKind,
145 severity: IntegritySeverity,
146 message: impl Into<String>,
147) -> IntegrityFinding {
148 IntegrityFinding {
149 kind,
150 severity,
151 message: message.into(),
152 segment_index: None,
153 segment_offset: None,
154 chunk_index: None,
155 logical_offset: None,
156 }
157}
158
159pub fn analyze_path(path: impl AsRef<Path>, options: &VerifyOptions) -> Result<IntegrityReport> {
166 options.validate()?;
167 analyze_open_result(Image::open(path), options)
168}
169
170pub fn analyze_path_with_progress(
176 path: impl AsRef<Path>,
177 options: &VerifyOptions,
178 progress: impl FnMut(VerifyProgress) -> ControlFlow<()>,
179) -> Result<IntegrityReport> {
180 options.validate()?;
181 let opened = Image::open_with_options(
182 path,
183 crate::OpenOptions::default()
184 .with_chunk_cache_size(0)
185 .with_maximum_open_handles(Some(16)),
186 );
187 analyze_open_with_progress(opened, options, progress)
188}
189
190pub fn analyze_path_with_password(
193 path: impl AsRef<Path>,
194 options: &VerifyOptions,
195 password: &EwfPassword,
196) -> Result<IntegrityReport> {
197 options.validate()?;
198 analyze_open_result(Image::open_with_password(path, password), options)
199}
200
201pub fn analyze_path_with_progress_and_password(
203 path: impl AsRef<Path>,
204 options: &VerifyOptions,
205 password: &EwfPassword,
206 progress: impl FnMut(VerifyProgress) -> ControlFlow<()>,
207) -> Result<IntegrityReport> {
208 options.validate()?;
209 let opened = Image::open_with_options_and_password(
210 path,
211 crate::OpenOptions::default()
212 .with_chunk_cache_size(0)
213 .with_maximum_open_handles(Some(16)),
214 password,
215 );
216 analyze_open_with_progress(opened, options, progress)
217}
218
219fn analyze_open_result(opened: Result<Image>, options: &VerifyOptions) -> Result<IntegrityReport> {
220 analyze_open_with_progress(opened, options, |_| ControlFlow::Continue(()))
221}
222
223fn analyze_open_with_progress(
224 opened: Result<Image>,
225 options: &VerifyOptions,
226 progress: impl FnMut(VerifyProgress) -> ControlFlow<()>,
227) -> Result<IntegrityReport> {
228 match opened {
229 Ok(image) => image.analyze_with_progress(options, progress),
230 Err(error) => {
231 let kind = match &error {
232 EwfError::Malformed(_) | EwfError::BufferTooShort { .. } => {
233 IntegrityFindingKind::InvalidStructure
234 }
235 EwfError::InvalidSignature => IntegrityFindingKind::InvalidSignature,
236 EwfError::Unsupported(_) => IntegrityFindingKind::UnsupportedFeature,
237 EwfError::PasswordRequired => IntegrityFindingKind::PasswordRequired,
238 EwfError::PasswordRejected | EwfError::DecryptionValidationFailed => {
239 IntegrityFindingKind::DecryptionFailed
240 }
241 EwfError::Io(io) if io.kind() == std::io::ErrorKind::UnexpectedEof => {
242 IntegrityFindingKind::InvalidStructure
243 }
244 _ => return Err(error),
245 };
246 let mut report = IntegrityReport::new();
247 report.push(
248 finding(kind, IntegritySeverity::Error, error.to_string()),
249 options.maximum_findings(),
250 );
251 Ok(report)
252 }
253 }
254}
255
256impl Image {
257 pub fn analyze(&self, options: &VerifyOptions) -> Result<IntegrityReport> {
261 self.analyze_with_progress(options, |_| ControlFlow::Continue(()))
262 }
263
264 pub fn analyze_with_progress(
268 &self,
269 options: &VerifyOptions,
270 progress: impl FnMut(VerifyProgress) -> ControlFlow<()>,
271 ) -> Result<IntegrityReport> {
272 options.validate()?;
273 self.ensure_not_aborted()?;
274 let limit = options.maximum_findings();
275 let mut report = IntegrityReport::new();
276 report.bytes_total = Some(self.media_size());
277 if self.open_strictness() == OpenStrictness::Lenient {
278 report.push(
279 finding(
280 IntegrityFindingKind::LenientOpen,
281 IntegritySeverity::Warning,
282 "image was opened with lenient structural validation",
283 ),
284 limit,
285 );
286 }
287 if !self.info().acquisition_complete {
288 report.push(
289 finding(
290 IntegrityFindingKind::IncompleteAcquisition,
291 IntegritySeverity::Warning,
292 "acquisition is incomplete",
293 ),
294 limit,
295 );
296 }
297 if !self.acquisition_errors().is_empty() {
298 report.push(
299 finding(
300 IntegrityFindingKind::AcquisitionErrors,
301 IntegritySeverity::Warning,
302 format!(
303 "{} acquisition error ranges are recorded",
304 self.acquisition_errors().len()
305 ),
306 ),
307 limit,
308 );
309 }
310 if self.md5_hash().is_none()
311 && self.sha1_hash().is_none()
312 && self.hash_value("SHA256").is_none()
313 {
314 report.push(
315 finding(
316 IntegrityFindingKind::MissingStoredHashes,
317 IntegritySeverity::Warning,
318 "no embedded MD5, SHA1, or SHA256 reference is available",
319 ),
320 limit,
321 );
322 }
323 self.analyze_redundant_tables(&mut report, limit)?;
324 let scan = self.scan_media(options, progress, |id, error| {
325 let mut item = finding(
326 IntegrityFindingKind::ChunkUnreadable,
327 IntegritySeverity::Error,
328 error.to_string(),
329 );
330 item.chunk_index = Some(id);
331 item.logical_offset = Some(id * self.chunk_size());
332 item.segment_index = self.segment_filename_for_chunk(id).ok().and_then(|path| {
334 self.segment_filenames()
335 .iter()
336 .position(|candidate| candidate == path)
337 });
338 report.push(item, limit);
339 Ok(())
340 })?;
341 report.bytes_verified = scan.progress.bytes_verified;
342 report.media_status = if scan.hashes.is_some() {
343 MediaScanStatus::Complete
344 } else {
345 MediaScanStatus::Incomplete
346 };
347 if let Some(hashes) = &scan.hashes {
348 report.comparisons = self.compare_hashes(hashes, options);
349 let mismatches: Vec<_> = report
350 .comparisons
351 .iter()
352 .filter(|item| !item.matches)
353 .map(|item| {
354 finding(
355 IntegrityFindingKind::HashMismatch {
356 algorithm: item.algorithm,
357 reference: item.reference,
358 },
359 IntegritySeverity::Error,
360 format!(
361 "{:?} differs from {:?} reference",
362 item.algorithm, item.reference
363 ),
364 )
365 })
366 .collect();
367 for item in mismatches {
368 report.push(item, limit);
369 }
370 }
371 report.hashes = scan.hashes;
372 Ok(report)
373 }
374
375 fn analyze_redundant_tables(&self, report: &mut IntegrityReport, limit: usize) -> Result<()> {
376 let mut primary: Option<&SectionInfo> = None;
377 for section in self.sections() {
378 self.ensure_not_aborted()?;
379 match §ion.kind {
380 SectionKind::Ewf1(name) if name == "table" => primary = Some(section),
381 SectionKind::Ewf1(name) if name == "table2" => {
382 if let Some(table) = primary
383 .take()
384 .filter(|table| table.segment_index == section.segment_index)
385 {
386 match self.compare_table_entries(table, section) {
387 Ok(Some(offset)) => {
388 let mut item = finding(
389 IntegrityFindingKind::RedundantTableMismatch,
390 IntegritySeverity::Error,
391 "primary and redundant chunk table entries differ",
392 );
393 item.segment_index = Some(section.segment_index);
394 item.segment_offset = Some(offset);
395 report.push(item, limit);
396 }
397 Err(EwfError::Aborted) => return Err(EwfError::Aborted),
398 Err(error) => {
399 let mut item = finding(
400 IntegrityFindingKind::TableCheckUnavailable,
401 IntegritySeverity::Error,
402 error.to_string(),
403 );
404 item.segment_index = Some(section.segment_index);
405 item.segment_offset = Some(section.descriptor_offset);
406 report.push(item, limit);
407 }
408 Ok(None) => {}
409 }
410 }
411 }
412 _ => {}
413 }
414 }
415 Ok(())
416 }
417
418 fn compare_table_entries(
419 &self,
420 primary: &SectionInfo,
421 redundant: &SectionInfo,
422 ) -> Result<Option<u64>> {
423 let left =
424 self.read_segment_range(primary.segment_index, primary.data_offset, 24, false)?;
425 let right =
426 self.read_segment_range(redundant.segment_index, redundant.data_offset, 24, false)?;
427 if left[..4] != right[..4] || left[8..16] != right[8..16] {
429 return Ok(None);
430 }
431 let size = u64::from(u32::from_le_bytes(
432 left[..4].try_into().expect("header read"),
433 )) * 4;
434 if size > primary.data_size.saturating_sub(24)
435 || size > redundant.data_size.saturating_sub(24)
436 {
437 return Err(EwfError::Malformed(
438 "redundant table entries exceed section payload".into(),
439 ));
440 }
441 let mut offset = 0;
442 while offset < size {
443 self.ensure_not_aborted()?;
444 let take = (size - offset).min(64 * 1024);
445 let a = self.read_segment_range(
446 primary.segment_index,
447 primary.data_offset + 24 + offset,
448 take,
449 false,
450 )?;
451 let b = self.read_segment_range(
452 redundant.segment_index,
453 redundant.data_offset + 24 + offset,
454 take,
455 false,
456 )?;
457 if let Some(difference) = a.iter().zip(&b).position(|(a, b)| a != b) {
458 return Ok(Some(
459 redundant.data_offset + 24 + offset + difference as u64,
460 ));
461 }
462 offset += take;
463 }
464 Ok(None)
465 }
466}