1use flate2::read::ZlibDecoder;
2use md5::{Digest as _, Md5};
3use sha1::Sha1;
4use sha2::Sha256;
5use std::fmt;
6use std::io::Read as _;
7
8const EVF_SIGNATURE: [u8; 8] = [0x45, 0x56, 0x46, 0x09, 0x0d, 0x0a, 0xff, 0x00];
11const DVF_SIGNATURE: [u8; 8] = [0x64, 0x76, 0x66, 0x09, 0x0d, 0x0a, 0xff, 0x00];
13const LVF_SIGNATURE: [u8; 8] = [0x4c, 0x56, 0x46, 0x09, 0x0d, 0x0a, 0xff, 0x00];
15
16const FILE_HEADER_SIZE: usize = 13;
17pub(crate) const SECTION_DESCRIPTOR_SIZE: usize = 76;
18const VOLUME_DATA_MIN: usize = 24;
19const VOLUME_DATA_FULL: usize = 1052;
21const VALID_MEDIA_TYPES: &[u8] = &[0x00, 0x01, 0x03, 0x0e, 0x10];
23
24const KNOWN_TYPES: &[&str] = &[
25 "header", "header2", "volume", "disk", "table", "table2", "sectors", "hash", "digest",
26 "error2", "session", "done", "next", "data", "ltree", "ltreedata",
27];
28
29const EVF2_SIGNATURE: [u8; 8] = [0x45, 0x56, 0x46, 0x32, 0x0d, 0x0a, 0x81, 0x00];
32const LEF2_SIGNATURE: [u8; 8] = [0x4c, 0x45, 0x46, 0x32, 0x0d, 0x0a, 0x81, 0x00];
33const EVF2_FILE_HEADER_SIZE: usize = 32;
34const EVF2_SECTION_DESCRIPTOR_SIZE: usize = 64;
35const EVF2_DATA_FLAG_ENCRYPTED: u32 = 0x0000_0002;
36const EVF2_CHUNK_FLAG_COMPRESSED: u32 = 0x0000_0001;
37const EVF2_TYPE_MEDIA_INFO: u32 = 0x02;
38const EVF2_TYPE_CHUNK_TABLE: u32 = 0x04;
39const EVF2_TYPE_MD5_HASH: u32 = 0x08;
40const EVF2_TYPE_SHA1_HASH: u32 = 0x09;
41const EVF2_TYPE_SHA256_HASH: u32 = 0x0A;
42const EVF2_CHUNK_TABLE_HEADER_SIZE: usize = 32;
43const EVF2_CHUNK_TABLE_ENTRY_SIZE: usize = 16;
44
45pub use forensicnomicon::report::Severity;
50
51#[derive(Debug, Clone, PartialEq, Eq)]
52#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
53pub enum EwfIntegrityAnomaly {
54 InvalidSignature,
56 SegmentNumberZero,
57 SectionDescriptorCrcMismatch {
58 offset: u64,
59 section_type: String,
60 computed: u32,
61 stored: u32,
62 },
63 SectionChainBroken {
64 at_offset: u64,
65 next_offset: u64,
66 },
67 SectionGapNonZero {
68 gap_offset: u64,
69 gap_size: u64,
70 },
71 VolumeSectionMissing,
72 UnknownSectionType {
73 offset: u64,
74 type_name: String,
75 },
76 DoneSectionMissing,
77 SectorsSectionMissing,
79 TableSectionMissing,
81 ChunkSizeInvalid {
82 sectors_per_chunk: u32,
83 bytes_per_sector: u32,
84 },
85 SectorCountMismatch {
86 declared: u64,
87 expected: u64,
88 },
89 BytesPerSectorInvalid {
90 bytes_per_sector: u32,
91 },
92 TableChunkCountMismatch {
93 in_volume: u32,
94 in_table: u32,
95 },
96 TableHeaderAdler32Mismatch {
97 computed: u32,
98 stored: u32,
99 },
100 TableEntryOutOfBounds {
101 chunk_index: u32,
102 entry_offset: u64,
103 file_size: u64,
104 },
105 TableEntryOutsideSectorsRange {
106 chunk_index: u32,
107 entry_offset: u64,
108 sectors_start: u64,
109 sectors_end: u64,
110 },
111 SectionGapZero {
112 gap_offset: u64,
113 gap_size: u64,
114 },
115 HashMismatch {
116 computed: [u8; 16],
117 stored: [u8; 16],
118 },
119 HashSectionMissing,
120 Table2Mismatch {
122 offset: usize,
124 },
125 BadSectorsPresent {
127 count: u32,
129 },
130 SegmentOutOfOrder {
133 segment_number: u16,
134 expected: u16,
135 },
136 DigestSha1Mismatch {
139 computed: [u8; 20],
140 stored: [u8; 20],
141 },
142 DigestSha256Mismatch {
144 computed: [u8; 32],
145 stored: [u8; 32],
146 },
147 ExternalMd5Mismatch {
150 computed: [u8; 16],
151 expected: [u8; 16],
152 },
153 ExternalSha1Mismatch {
155 computed: [u8; 20],
156 expected: [u8; 20],
157 },
158 Ewf2SectionDataHashMismatch {
161 offset: u64,
162 section_type_id: u32,
163 computed: [u8; 16],
164 stored: [u8; 16],
165 },
166 Ewf2EncryptedSection {
168 offset: u64,
169 },
170 Ewf2HashSectionMissing,
172 VolumeBodyCrcMismatch { computed: u32, stored: u32 },
175 MediaTypeUnknown { media_type: u8 },
178 SetIdentifierMismatch { segment: usize },
181 Ewf2MediaInfoMissing,
183 Ewf2ChunkTableChecksumMismatch { computed: u32, stored: u32 },
186 ChunkChecksumMismatch {
189 chunk_index: usize,
190 computed: u32,
191 stored: u32,
192 },
193 ChunkDecompressionError {
196 chunk_index: usize,
197 },
198 UnsupportedCompressionAlgorithm {
200 method_id: u16,
202 },
203 ExternalSha256Mismatch {
205 computed: [u8; 32],
206 expected: [u8; 32],
207 },
208 Ewf2MediaInfoParseFailed,
212}
213
214impl EwfIntegrityAnomaly {
215 pub fn severity(&self) -> Severity {
216 match self {
217 Self::InvalidSignature => Severity::Critical,
218 Self::SegmentNumberZero => Severity::High,
219 Self::SectionDescriptorCrcMismatch { .. } => Severity::High,
220 Self::SectionChainBroken { .. } => Severity::Critical,
221 Self::SectionGapNonZero { .. } => Severity::Medium,
222 Self::VolumeSectionMissing => Severity::Critical,
223 Self::UnknownSectionType { .. } => Severity::Medium,
224 Self::DoneSectionMissing => Severity::Medium,
225 Self::SectorsSectionMissing => Severity::High,
226 Self::TableSectionMissing => Severity::High,
227 Self::ChunkSizeInvalid { .. } => Severity::High,
228 Self::SectorCountMismatch { .. } => Severity::High,
229 Self::BytesPerSectorInvalid { .. } => Severity::High,
230 Self::TableChunkCountMismatch { .. } => Severity::High,
231 Self::TableHeaderAdler32Mismatch { .. } => Severity::High,
232 Self::TableEntryOutOfBounds { .. } => Severity::High,
233 Self::TableEntryOutsideSectorsRange { .. } => Severity::High,
234 Self::SectionGapZero { .. } => Severity::Info,
235 Self::HashMismatch { .. } => Severity::High,
236 Self::HashSectionMissing => Severity::Medium,
237 Self::Table2Mismatch { .. } => Severity::High,
238 Self::BadSectorsPresent { .. } => Severity::Medium,
239 Self::SegmentOutOfOrder { .. } => Severity::High,
240 Self::DigestSha1Mismatch { .. } => Severity::High,
241 Self::DigestSha256Mismatch { .. } => Severity::High,
242 Self::ExternalMd5Mismatch { .. } => Severity::Critical,
243 Self::ExternalSha1Mismatch { .. } => Severity::Critical,
244 Self::VolumeBodyCrcMismatch { .. } => Severity::High,
245 Self::MediaTypeUnknown { .. } => Severity::Medium,
246 Self::SetIdentifierMismatch { .. } => Severity::High,
247 Self::Ewf2SectionDataHashMismatch { .. } => Severity::High,
248 Self::Ewf2EncryptedSection { .. } => Severity::Medium,
249 Self::Ewf2HashSectionMissing => Severity::Medium,
250 Self::Ewf2MediaInfoMissing => Severity::Medium,
251 Self::Ewf2ChunkTableChecksumMismatch { .. } => Severity::High,
252 Self::ChunkChecksumMismatch { .. } => Severity::High,
253 Self::ChunkDecompressionError { .. } => Severity::High,
254 Self::UnsupportedCompressionAlgorithm { .. } => Severity::High,
255 Self::ExternalSha256Mismatch { .. } => Severity::Critical,
256 Self::Ewf2MediaInfoParseFailed => Severity::High,
257 }
258 }
259}
260
261impl fmt::Display for EwfIntegrityAnomaly {
262 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263 match self {
264 Self::InvalidSignature =>
265 write!(f, "invalid EWF signature — not a valid E01/Ex01 file"),
266 Self::SegmentNumberZero =>
267 write!(f, "segment number is zero (expected ≥ 1)"),
268 Self::SectionDescriptorCrcMismatch { offset, section_type, computed, stored } =>
269 write!(f, "section '{section_type}' at 0x{offset:x}: descriptor CRC mismatch (computed 0x{computed:08x}, stored 0x{stored:08x})"),
270 Self::SectionChainBroken { at_offset, next_offset } =>
271 write!(f, "section chain broken at 0x{at_offset:x}: next pointer 0x{next_offset:x} is invalid"),
272 Self::SectionGapNonZero { gap_offset, gap_size } =>
273 write!(f, "non-zero data in {gap_size}-byte gap at 0x{gap_offset:x} — possible hidden data"),
274 Self::VolumeSectionMissing =>
275 write!(f, "volume/disk section missing in segment 1"),
276 Self::UnknownSectionType { offset, type_name } =>
277 write!(f, "unknown section type '{type_name}' at 0x{offset:x}"),
278 Self::DoneSectionMissing =>
279 write!(f, "done section missing from final segment"),
280 Self::SectorsSectionMissing =>
281 write!(f, "sectors section missing — chunk data not found in segment"),
282 Self::TableSectionMissing =>
283 write!(f, "table section missing — chunk offset table not found in segment"),
284 Self::ChunkSizeInvalid { sectors_per_chunk, bytes_per_sector } =>
285 write!(f, "invalid chunk size: {sectors_per_chunk} sectors × {bytes_per_sector} bytes/sector"),
286 Self::SectorCountMismatch { declared, expected } =>
287 write!(f, "sector count mismatch: declared {declared}, expected {expected}"),
288 Self::BytesPerSectorInvalid { bytes_per_sector } =>
289 write!(f, "invalid bytes_per_sector: {bytes_per_sector} (expected 512 or 4096)"),
290 Self::TableChunkCountMismatch { in_volume, in_table } =>
291 write!(f, "chunk count mismatch: volume declares {in_volume}, table has {in_table}"),
292 Self::TableHeaderAdler32Mismatch { computed, stored } =>
293 write!(f, "table header Adler-32 mismatch: computed 0x{computed:08x}, stored 0x{stored:08x}"),
294 Self::TableEntryOutOfBounds { chunk_index, entry_offset, file_size } =>
295 write!(f, "table entry for chunk {chunk_index} points outside file: 0x{entry_offset:x} ≥ 0x{file_size:x}"),
296 Self::TableEntryOutsideSectorsRange { chunk_index, entry_offset, sectors_start, sectors_end } =>
297 write!(f, "table entry for chunk {chunk_index} at 0x{entry_offset:x} is outside sectors section [0x{sectors_start:x}..0x{sectors_end:x}]"),
298 Self::SectionGapZero { gap_offset, gap_size } =>
299 write!(f, "zero-padded {gap_size}-byte gap at 0x{gap_offset:x}"),
300 Self::HashMismatch { computed, stored } =>
301 write!(f, "MD5 mismatch: computed {}, stored {}", hex(computed), hex(stored)),
302 Self::HashSectionMissing =>
303 write!(f, "hash section missing — cannot verify MD5"),
304 Self::Table2Mismatch { offset } =>
305 write!(f, "table2 body differs from table at byte offset {offset} — one redundant copy is corrupt"),
306 Self::BadSectorsPresent { count } =>
307 write!(f, "error2 section reports {count} unreadable sector range(s) from acquisition"),
308 Self::SegmentOutOfOrder { segment_number, expected } =>
309 write!(f, "segment {segment_number} found where segment {expected} was expected"),
310 Self::DigestSha1Mismatch { computed, stored } =>
311 write!(f, "SHA-1 mismatch: computed {}, stored {}", hex(computed), hex(stored)),
312 Self::DigestSha256Mismatch { computed, stored } =>
313 write!(f, "SHA-256 mismatch: computed {}, stored {}", hex(computed), hex(stored)),
314 Self::ExternalMd5Mismatch { computed, expected } =>
315 write!(f, "MD5 does not match chain-of-custody reference: computed {}, expected {}", hex(computed), hex(expected)),
316 Self::ExternalSha1Mismatch { computed, expected } =>
317 write!(f, "SHA-1 does not match chain-of-custody reference: computed {}, expected {}", hex(computed), hex(expected)),
318 Self::ExternalSha256Mismatch { computed, expected } =>
319 write!(f, "SHA-256 does not match chain-of-custody reference: computed {}, expected {}", hex(computed), hex(expected)),
320 Self::Ewf2SectionDataHashMismatch { offset, section_type_id, computed, stored } =>
321 write!(f, "EWF v2 section (type 0x{section_type_id:02x}) at 0x{offset:x}: data integrity hash mismatch (computed {}, stored {})", hex(computed), hex(stored)),
322 Self::Ewf2EncryptedSection { offset } =>
323 write!(f, "EWF v2 encrypted section at 0x{offset:x} — content not verifiable"),
324 Self::Ewf2HashSectionMissing =>
325 write!(f, "EWF v2 hash section missing from final segment"),
326 Self::VolumeBodyCrcMismatch { computed, stored } =>
327 write!(f, "volume section body CRC mismatch (computed 0x{computed:08x}, stored 0x{stored:08x})"),
328 Self::MediaTypeUnknown { media_type } =>
329 write!(f, "unknown media_type 0x{media_type:02x}"),
330 Self::SetIdentifierMismatch { segment } =>
331 write!(f, "set_identifier GUID mismatch in segment {segment} — segments may be from different acquisitions"),
332 Self::Ewf2MediaInfoMissing =>
333 write!(f, "EWF v2 media information section missing"),
334 Self::Ewf2ChunkTableChecksumMismatch { computed, stored } =>
335 write!(f, "EWF v2 chunk table checksum mismatch (computed 0x{computed:08x}, stored 0x{stored:08x})"),
336 Self::ChunkChecksumMismatch { chunk_index, computed, stored } =>
337 write!(f, "chunk {chunk_index}: Adler-32 mismatch (computed 0x{computed:08x}, stored 0x{stored:08x})"),
338 Self::ChunkDecompressionError { chunk_index } =>
339 write!(f, "chunk {chunk_index}: zlib decompression failed — chunk data is corrupt"),
340 Self::UnsupportedCompressionAlgorithm { method_id } =>
341 write!(f, "EWF v2 file header specifies unsupported compression algorithm 0x{method_id:04x} — only deflate (0/1) is supported"),
342 Self::Ewf2MediaInfoParseFailed =>
343 write!(f, "EWF v2 media information section body could not be decompressed or decoded"),
344 }
345 }
346}
347
348fn hex(bytes: &[u8]) -> String {
349 bytes.iter().map(|b| format!("{b:02x}")).collect()
350}
351
352#[derive(Debug, Clone, PartialEq, Eq)]
355#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
356pub struct AnalysisProgress {
357 pub chunks_done: usize,
359 pub chunks_total: Option<usize>,
361 pub bytes_done: u64,
363}
364
365#[derive(Debug, Clone, PartialEq, Eq)]
367#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
368pub struct ComputedHashes {
369 pub md5: [u8; 16],
370 pub sha1: [u8; 20],
371 pub sha256: [u8; 32],
372}
373
374#[derive(Debug, Clone, PartialEq, Eq)]
376#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
377pub struct EwfHeaderMetadata {
378 pub description: String,
379 pub case_number: String,
380 pub evidence_number: String,
381 pub examiner_name: String,
382 pub acquisition_date: String,
383 pub system_date: String,
384 pub password_hash: String,
385 pub acquisition_software: String,
386}
387
388pub struct EwfIntegrity<'a> {
391 segments: Vec<&'a [u8]>,
392 expected_md5: Option<[u8; 16]>,
393 expected_sha1: Option<[u8; 20]>,
394 expected_sha256: Option<[u8; 32]>,
395}
396
397impl<'a> EwfIntegrity<'a> {
398 pub fn new(data: &'a [u8]) -> Self {
400 Self {
401 segments: vec![data],
402 expected_md5: None,
403 expected_sha1: None,
404 expected_sha256: None,
405 }
406 }
407
408 pub fn from_segments(segs: &[&'a [u8]]) -> Self {
410 Self {
411 segments: segs.to_vec(),
412 expected_md5: None,
413 expected_sha1: None,
414 expected_sha256: None,
415 }
416 }
417
418 pub fn with_expected_md5(mut self, hash: [u8; 16]) -> Self {
421 self.expected_md5 = Some(hash);
422 self
423 }
424
425 pub fn with_expected_sha1(mut self, hash: [u8; 20]) -> Self {
428 self.expected_sha1 = Some(hash);
429 self
430 }
431
432 pub fn with_expected_sha256(mut self, hash: [u8; 32]) -> Self {
435 self.expected_sha256 = Some(hash);
436 self
437 }
438
439 pub fn header_metadata(&self) -> Option<EwfHeaderMetadata> {
445 for &data in &self.segments {
446 if let Some(meta) = parse_header_section(data) {
447 return Some(meta);
448 }
449 }
450 None
451 }
452
453 pub fn compute_hashes(&self) -> Option<ComputedHashes> {
458 let first = self.segments.first().copied().unwrap_or(&[]);
459 if first.len() >= 8
460 && (first[0..8] == EVF2_SIGNATURE || first[0..8] == LEF2_SIGNATURE)
461 {
462 return compute_hashes_ewf2(&self.segments);
463 }
464 compute_hashes_ewf1(&self.segments)
465 }
466
467 pub fn analyse(&self) -> Vec<EwfIntegrityAnomaly> {
468 let first = self.segments.first().copied().unwrap_or(&[]);
469 if first.len() >= 8
470 && (first[0..8] == EVF2_SIGNATURE || first[0..8] == LEF2_SIGNATURE)
471 {
472 return self.analyse_all_ewf2();
473 }
474 self.analyse_all_ewf1()
475 }
476
477 pub fn analyse_with_progress(
485 &self,
486 progress: impl FnMut(AnalysisProgress),
487 ) -> Vec<EwfIntegrityAnomaly> {
488 let first = self.segments.first().copied().unwrap_or(&[]);
489 if first.len() >= 8
490 && (first[0..8] == EVF2_SIGNATURE || first[0..8] == LEF2_SIGNATURE)
491 {
492 return self.analyse_all_ewf2_with_progress(progress);
493 }
494 self.analyse_all_ewf1_with_progress(progress)
495 }
496
497 fn analyse_all_ewf1(&self) -> Vec<EwfIntegrityAnomaly> {
500 let mut issues = Vec::new();
501 let n = self.segments.len();
502 let multi = n > 1;
503 let mut geometry: Option<VolumeGeometry> = None;
504 let mut all_sections: Vec<Vec<Section>> = Vec::with_capacity(n);
505 let mut total_table_entries: u32 = 0;
506
507 for (idx, &data) in self.segments.iter().enumerate() {
508 let expected_seg_num = (idx + 1) as u16;
509 let is_last = idx == n - 1;
510 let file_size = data.len() as u64;
511
512 if data.len() < FILE_HEADER_SIZE {
513 issues.push(EwfIntegrityAnomaly::SectionChainBroken {
514 at_offset: 0,
515 next_offset: 0,
516 });
517 all_sections.push(Vec::new());
518 continue;
519 }
520
521 if data[0..8] != EVF_SIGNATURE
522 && data[0..8] != DVF_SIGNATURE
523 && data[0..8] != LVF_SIGNATURE
524 {
525 issues.push(EwfIntegrityAnomaly::InvalidSignature);
526 }
527
528 let seg_num = u16::from_le_bytes(data[9..11].try_into().unwrap());
529 if seg_num == 0 {
530 issues.push(EwfIntegrityAnomaly::SegmentNumberZero);
531 } else if seg_num != expected_seg_num {
532 issues.push(EwfIntegrityAnomaly::SegmentOutOfOrder {
533 segment_number: seg_num,
534 expected: expected_seg_num,
535 });
536 }
537
538 let sections = walk_sections_v1(data, &mut issues);
539
540 if let Some(vol_sec) = sections
542 .iter()
543 .find(|s| s.type_name == "volume" || s.type_name == "disk")
544 {
545 if idx == 0 {
546 geometry = check_volume_v1(data, vol_sec.offset, vol_sec.size, &mut issues);
547 } else {
548 let later = check_volume_v1(data, vol_sec.offset, vol_sec.size, &mut issues);
550 if let (Some(ref base), Some(ref later_geom)) = (&geometry, &later) {
551 let base_guid = base.set_identifier;
552 let later_guid = later_geom.set_identifier;
553 let neither_zero =
554 base_guid != [0u8; 16] && later_guid != [0u8; 16];
555 if neither_zero && base_guid != later_guid {
556 issues.push(EwfIntegrityAnomaly::SetIdentifierMismatch {
557 segment: idx + 1,
558 });
559 }
560 }
561 }
562 } else if idx == 0 {
563 issues.push(EwfIntegrityAnomaly::VolumeSectionMissing);
564 }
565
566 let vol_count = if !multi && idx == 0 {
569 geometry.as_ref().map(|g| g.chunk_count)
570 } else {
571 None
572 };
573 let sectors_section = sections.iter().find(|s| s.type_name == "sectors");
574 let sectors_range = sectors_section
575 .map(|s| (s.offset + SECTION_DESCRIPTOR_SIZE as u64, s.offset + s.size));
576 if sectors_section.is_none() {
577 issues.push(EwfIntegrityAnomaly::SectorsSectionMissing);
578 }
579 if let Some(table) = sections.iter().find(|s| s.type_name == "table") {
580 let data_start = (table.offset as usize) + SECTION_DESCRIPTOR_SIZE;
581 if data.len() >= data_start + 4 {
582 let count = u32::from_le_bytes(data[data_start..data_start + 4].try_into().unwrap());
583 total_table_entries = total_table_entries.saturating_add(count);
584 }
585 check_table_v1(
586 data,
587 table.offset,
588 vol_count,
589 file_size,
590 sectors_range,
591 &mut issues,
592 );
593 } else {
594 issues.push(EwfIntegrityAnomaly::TableSectionMissing);
595 }
596
597 if let (Some(t1), Some(t2)) = (
599 sections.iter().find(|s| s.type_name == "table"),
600 sections.iter().find(|s| s.type_name == "table2"),
601 ) {
602 let b1_start = (t1.offset + SECTION_DESCRIPTOR_SIZE as u64) as usize;
603 let b1_end = (t1.offset + t1.size) as usize;
604 let b2_start = (t2.offset + SECTION_DESCRIPTOR_SIZE as u64) as usize;
605 let b2_end = (t2.offset + t2.size) as usize;
606 if let (Some(body1), Some(body2)) = (data.get(b1_start..b1_end), data.get(b2_start..b2_end)) {
607 if body1.len() == body2.len() {
608 if let Some(offset) = body1.iter().zip(body2).position(|(a, b)| a != b) {
609 issues.push(EwfIntegrityAnomaly::Table2Mismatch { offset });
610 }
611 } else {
612 issues.push(EwfIntegrityAnomaly::Table2Mismatch { offset: 0 });
613 }
614 }
615 }
616
617 if let Some(e2) = sections.iter().find(|s| s.type_name == "error2") {
619 let body_start = (e2.offset + SECTION_DESCRIPTOR_SIZE as u64) as usize;
620 if body_start + 4 <= data.len() {
621 let count = u32::from_le_bytes(data[body_start..body_start + 4].try_into().unwrap());
622 if count > 0 {
623 issues.push(EwfIntegrityAnomaly::BadSectorsPresent { count });
624 }
625 }
626 }
627
628 if is_last && !sections.iter().any(|s| s.type_name == "done") {
630 issues.push(EwfIntegrityAnomaly::DoneSectionMissing);
631 }
632
633 all_sections.push(sections);
634 }
635
636 if multi {
638 if let Some(geom) = &geometry {
639 if total_table_entries != geom.chunk_count {
640 issues.push(EwfIntegrityAnomaly::TableChunkCountMismatch {
641 in_volume: geom.chunk_count,
642 in_table: total_table_entries,
643 });
644 }
645 }
646 }
647
648 if let Some(geom) = &geometry {
650 check_hash_all_segments(
651 &self.segments,
652 &all_sections,
653 geom,
654 self.expected_md5,
655 self.expected_sha1,
656 self.expected_sha256,
657 &mut issues,
658 &mut |_| {},
659 );
660 }
661
662 issues
663 }
664
665 fn analyse_all_ewf2(&self) -> Vec<EwfIntegrityAnomaly> {
668 self.analyse_all_ewf2_with_progress(|_| {})
669 }
670
671 fn analyse_all_ewf2_impl(&self, progress: &mut dyn FnMut(AnalysisProgress)) -> Vec<EwfIntegrityAnomaly> {
672 let mut issues = Vec::new();
673 let n = self.segments.len();
674
675 let mut final_stored_md5: Option<[u8; 16]> = None;
677 let mut final_stored_sha1: Option<[u8; 20]> = None;
678 let mut final_stored_sha256: Option<[u8; 32]> = None;
679
680 for (idx, &data) in self.segments.iter().enumerate() {
681 let expected_seg_num = (idx + 1) as u32;
682
683 if data.len() < EVF2_FILE_HEADER_SIZE + EVF2_SECTION_DESCRIPTOR_SIZE {
684 issues.push(EwfIntegrityAnomaly::SectionChainBroken {
685 at_offset: 0,
686 next_offset: 0,
687 });
688 continue;
689 }
690
691 if data[0..8] != EVF2_SIGNATURE && data[0..8] != LEF2_SIGNATURE {
692 issues.push(EwfIntegrityAnomaly::InvalidSignature);
693 }
694
695 let seg_num = u32::from_le_bytes(data[12..16].try_into().unwrap());
696 if seg_num == 0 {
697 issues.push(EwfIntegrityAnomaly::SegmentNumberZero);
698 } else if seg_num != expected_seg_num {
699 issues.push(EwfIntegrityAnomaly::SegmentOutOfOrder {
700 segment_number: seg_num as u16,
701 expected: expected_seg_num as u16,
702 });
703 }
704
705 let compression_method = u16::from_le_bytes(data[10..12].try_into().unwrap());
708 if compression_method > 1 {
709 issues.push(EwfIntegrityAnomaly::UnsupportedCompressionAlgorithm {
710 method_id: compression_method,
711 });
712 }
713
714 let mut has_hash = false;
717 let mut has_media_info = false;
718 let mut chunk_table_body: Option<(usize, usize)> = None;
719 let mut stored_sector_md5: Option<[u8; 16]> = None;
720 let mut stored_sector_sha1: Option<[u8; 20]> = None;
721 let mut stored_sector_sha256: Option<[u8; 32]> = None;
722 let mut desc_offset = data.len().saturating_sub(EVF2_SECTION_DESCRIPTOR_SIZE);
723
724 loop {
725 if desc_offset + EVF2_SECTION_DESCRIPTOR_SIZE > data.len()
726 || desc_offset < EVF2_FILE_HEADER_SIZE
727 {
728 break;
729 }
730 let desc = &data[desc_offset..desc_offset + EVF2_SECTION_DESCRIPTOR_SIZE];
731 let section_type = u32::from_le_bytes(desc[0..4].try_into().unwrap());
732 let data_flags = u32::from_le_bytes(desc[4..8].try_into().unwrap());
733 let prev_offset = u64::from_le_bytes(desc[8..16].try_into().unwrap()) as usize;
734 let data_size = u64::from_le_bytes(desc[16..24].try_into().unwrap()) as usize;
735 let stored_hash: [u8; 16] = desc[32..48].try_into().unwrap();
736
737 let body_end = desc_offset;
739 let body_start = desc_offset.saturating_sub(data_size);
740
741 if data_flags & EVF2_DATA_FLAG_ENCRYPTED != 0 {
742 issues.push(EwfIntegrityAnomaly::Ewf2EncryptedSection {
743 offset: desc_offset as u64,
744 });
745 } else {
746 if stored_hash != [0u8; 16] {
747 if let Some(body) = data.get(body_start..body_end) {
748 let computed: [u8; 16] = Md5::digest(body).into();
749 if computed != stored_hash {
750 issues.push(EwfIntegrityAnomaly::Ewf2SectionDataHashMismatch {
751 offset: desc_offset as u64,
752 section_type_id: section_type,
753 computed,
754 stored: stored_hash,
755 });
756 }
757 }
758 }
759
760 match section_type {
761 EVF2_TYPE_MEDIA_INFO => {
762 has_media_info = true;
763 if let Some(body) = data.get(body_start..body_end) {
764 if !parse_media_info_body(body) {
765 issues.push(EwfIntegrityAnomaly::Ewf2MediaInfoParseFailed);
766 }
767 } else {
768 issues.push(EwfIntegrityAnomaly::Ewf2MediaInfoParseFailed);
769 }
770 }
771 EVF2_TYPE_CHUNK_TABLE => {
772 chunk_table_body = Some((body_start, body_end));
773 }
774 EVF2_TYPE_MD5_HASH => {
775 has_hash = true;
776 if data_size >= 16 {
778 if let Some(body) = data.get(body_start..body_end) {
779 let mut h = [0u8; 16];
780 h.copy_from_slice(&body[..16]);
781 stored_sector_md5 = Some(h);
782 }
783 }
784 }
785 EVF2_TYPE_SHA1_HASH => {
786 has_hash = true;
787 if data_size >= 20 {
788 if let Some(body) = data.get(body_start..body_end) {
789 let mut h = [0u8; 20];
790 h.copy_from_slice(&body[..20]);
791 stored_sector_sha1 = Some(h);
792 }
793 }
794 }
795 EVF2_TYPE_SHA256_HASH => {
796 has_hash = true;
797 if data_size >= 32 {
798 if let Some(body) = data.get(body_start..body_end) {
799 let mut h = [0u8; 32];
800 h.copy_from_slice(&body[..32]);
801 stored_sector_sha256 = Some(h);
802 }
803 }
804 }
805 _ => {}
806 }
807 }
808
809 if prev_offset == 0 {
810 break;
811 }
812 desc_offset = prev_offset;
813 }
814
815 if idx == n - 1 && !has_hash {
816 issues.push(EwfIntegrityAnomaly::Ewf2HashSectionMissing);
817 }
818 if idx == 0 && !has_media_info {
819 issues.push(EwfIntegrityAnomaly::Ewf2MediaInfoMissing);
820 }
821
822 if idx == n - 1 {
824 final_stored_md5 = stored_sector_md5;
825 final_stored_sha1 = stored_sector_sha1;
826 final_stored_sha256 = stored_sector_sha256;
827 }
828
829 if let Some((ct_start, ct_end)) = chunk_table_body {
832 verify_ewf2_sector_data(data, ct_start, ct_end, None, None, None, &mut issues, progress);
833 }
834 }
835
836 if let Some(computed) = compute_hashes_ewf2(&self.segments) {
839 if let Some(stored) = final_stored_md5 {
840 if computed.md5 != stored {
841 issues.push(EwfIntegrityAnomaly::HashMismatch { computed: computed.md5, stored });
842 }
843 }
844 if let Some(stored) = final_stored_sha1 {
845 if computed.sha1 != stored {
846 issues.push(EwfIntegrityAnomaly::DigestSha1Mismatch { computed: computed.sha1, stored });
847 }
848 }
849 if let Some(stored) = final_stored_sha256 {
850 if computed.sha256 != stored {
851 issues.push(EwfIntegrityAnomaly::DigestSha256Mismatch { computed: computed.sha256, stored });
852 }
853 }
854 if let Some(expected) = self.expected_md5 {
855 if computed.md5 != expected {
856 issues.push(EwfIntegrityAnomaly::ExternalMd5Mismatch { computed: computed.md5, expected });
857 }
858 }
859 if let Some(expected) = self.expected_sha1 {
860 if computed.sha1 != expected {
861 issues.push(EwfIntegrityAnomaly::ExternalSha1Mismatch { computed: computed.sha1, expected });
862 }
863 }
864 if let Some(expected) = self.expected_sha256 {
865 if computed.sha256 != expected {
866 issues.push(EwfIntegrityAnomaly::ExternalSha256Mismatch { computed: computed.sha256, expected });
867 }
868 }
869 }
870
871 issues
872 }
873
874 fn analyse_all_ewf1_with_progress(
875 &self,
876 mut progress: impl FnMut(AnalysisProgress),
877 ) -> Vec<EwfIntegrityAnomaly> {
878 let mut issues = Vec::new();
879 let n = self.segments.len();
880 let multi = n > 1;
881 let mut geometry: Option<VolumeGeometry> = None;
882 let mut all_sections: Vec<Vec<Section>> = Vec::with_capacity(n);
883 let mut total_table_entries: u32 = 0;
884
885 for (idx, &data) in self.segments.iter().enumerate() {
886 let expected_seg_num = (idx + 1) as u16;
887 let is_last = idx == n - 1;
888 let file_size = data.len() as u64;
889
890 if data.len() < FILE_HEADER_SIZE {
891 issues.push(EwfIntegrityAnomaly::SectionChainBroken { at_offset: 0, next_offset: 0 });
892 all_sections.push(Vec::new());
893 continue;
894 }
895 if data[0..8] != EVF_SIGNATURE && data[0..8] != DVF_SIGNATURE && data[0..8] != LVF_SIGNATURE {
896 issues.push(EwfIntegrityAnomaly::InvalidSignature);
897 }
898 let seg_num = u16::from_le_bytes(data[9..11].try_into().unwrap());
899 if seg_num == 0 {
900 issues.push(EwfIntegrityAnomaly::SegmentNumberZero);
901 } else if seg_num != expected_seg_num {
902 issues.push(EwfIntegrityAnomaly::SegmentOutOfOrder { segment_number: seg_num, expected: expected_seg_num });
903 }
904 let sections = walk_sections_v1(data, &mut issues);
905 if let Some(vol_sec) = sections.iter().find(|s| s.type_name == "volume" || s.type_name == "disk") {
906 if idx == 0 {
907 geometry = check_volume_v1(data, vol_sec.offset, vol_sec.size, &mut issues);
908 } else {
909 let later = check_volume_v1(data, vol_sec.offset, vol_sec.size, &mut issues);
910 if let (Some(ref base), Some(ref later_geom)) = (&geometry, &later) {
911 let base_guid = base.set_identifier;
912 let later_guid = later_geom.set_identifier;
913 if base_guid != [0u8; 16] && later_guid != [0u8; 16] && base_guid != later_guid {
914 issues.push(EwfIntegrityAnomaly::SetIdentifierMismatch { segment: idx + 1 });
915 }
916 }
917 }
918 } else if idx == 0 {
919 issues.push(EwfIntegrityAnomaly::VolumeSectionMissing);
920 }
921 let vol_count = if !multi && idx == 0 { geometry.as_ref().map(|g| g.chunk_count) } else { None };
922 let sectors_section = sections.iter().find(|s| s.type_name == "sectors");
923 let sectors_range = sectors_section
924 .map(|s| (s.offset + SECTION_DESCRIPTOR_SIZE as u64, s.offset + s.size));
925 if sectors_section.is_none() {
926 issues.push(EwfIntegrityAnomaly::SectorsSectionMissing);
927 }
928 if let Some(table) = sections.iter().find(|s| s.type_name == "table") {
929 let data_start = (table.offset as usize) + SECTION_DESCRIPTOR_SIZE;
930 if data.len() >= data_start + 4 {
931 let count = u32::from_le_bytes(data[data_start..data_start + 4].try_into().unwrap());
932 total_table_entries = total_table_entries.saturating_add(count);
933 }
934 check_table_v1(data, table.offset, vol_count, file_size, sectors_range, &mut issues);
935 } else {
936 issues.push(EwfIntegrityAnomaly::TableSectionMissing);
937 }
938 if let (Some(t1), Some(t2)) = (
939 sections.iter().find(|s| s.type_name == "table"),
940 sections.iter().find(|s| s.type_name == "table2"),
941 ) {
942 let b1_start = (t1.offset + SECTION_DESCRIPTOR_SIZE as u64) as usize;
943 let b1_end = (t1.offset + t1.size) as usize;
944 let b2_start = (t2.offset + SECTION_DESCRIPTOR_SIZE as u64) as usize;
945 let b2_end = (t2.offset + t2.size) as usize;
946 if let (Some(body1), Some(body2)) = (data.get(b1_start..b1_end), data.get(b2_start..b2_end)) {
947 if body1.len() == body2.len() {
948 if let Some(offset) = body1.iter().zip(body2).position(|(a, b)| a != b) {
949 issues.push(EwfIntegrityAnomaly::Table2Mismatch { offset });
950 }
951 } else {
952 issues.push(EwfIntegrityAnomaly::Table2Mismatch { offset: 0 });
953 }
954 }
955 }
956 if let Some(e2) = sections.iter().find(|s| s.type_name == "error2") {
957 let body_start = (e2.offset + SECTION_DESCRIPTOR_SIZE as u64) as usize;
958 if body_start + 4 <= data.len() {
959 let count = u32::from_le_bytes(data[body_start..body_start + 4].try_into().unwrap());
960 if count > 0 {
961 issues.push(EwfIntegrityAnomaly::BadSectorsPresent { count });
962 }
963 }
964 }
965 if is_last && !sections.iter().any(|s| s.type_name == "done") {
966 issues.push(EwfIntegrityAnomaly::DoneSectionMissing);
967 }
968 all_sections.push(sections);
969 }
970
971 if multi {
972 if let Some(geom) = &geometry {
973 if total_table_entries != geom.chunk_count {
974 issues.push(EwfIntegrityAnomaly::TableChunkCountMismatch {
975 in_volume: geom.chunk_count,
976 in_table: total_table_entries,
977 });
978 }
979 }
980 }
981
982 if let Some(geom) = &geometry {
983 check_hash_all_segments(
984 &self.segments, &all_sections, geom,
985 self.expected_md5, self.expected_sha1, self.expected_sha256,
986 &mut issues, &mut progress,
987 );
988 }
989 issues
990 }
991
992 fn analyse_all_ewf2_with_progress(
993 &self,
994 mut progress: impl FnMut(AnalysisProgress),
995 ) -> Vec<EwfIntegrityAnomaly> {
996 self.analyse_all_ewf2_impl(&mut progress)
997 }
998}
999
1000fn parse_header_section(data: &[u8]) -> Option<EwfHeaderMetadata> {
1003 if data.len() < FILE_HEADER_SIZE + SECTION_DESCRIPTOR_SIZE {
1004 return None;
1005 }
1006 let desc_off = FILE_HEADER_SIZE;
1007 let desc = &data[desc_off..desc_off + SECTION_DESCRIPTOR_SIZE];
1008 let type_end = desc[..16].iter().position(|&b| b == 0).unwrap_or(16);
1009 if &desc[..type_end] != b"header" {
1010 return None;
1011 }
1012 let section_size = u64::from_le_bytes(desc[24..32].try_into().unwrap()) as usize;
1013 let body_start = desc_off + SECTION_DESCRIPTOR_SIZE;
1014 let body_end = (desc_off + section_size).min(data.len());
1015 if body_start >= body_end {
1016 return None;
1017 }
1018 let compressed = &data[body_start..body_end];
1019
1020 let mut decoder = ZlibDecoder::new(compressed);
1021 let mut text = String::new();
1022 decoder.read_to_string(&mut text).ok()?;
1023
1024 parse_header_text(&text)
1025}
1026
1027fn parse_header_text(text: &str) -> Option<EwfHeaderMetadata> {
1028 let lines: Vec<&str> = text
1034 .lines()
1035 .map(|l| l.trim_end_matches('\r'))
1036 .filter(|l| !l.is_empty())
1037 .collect();
1038 if lines.len() < 4 {
1039 return None;
1040 }
1041 let keys: Vec<&str> = lines[2].split('\t').collect();
1042 let vals: Vec<&str> = lines[3].split('\t').collect();
1043
1044 let mut meta = EwfHeaderMetadata {
1045 description: String::new(),
1046 case_number: String::new(),
1047 evidence_number: String::new(),
1048 examiner_name: String::new(),
1049 acquisition_date: String::new(),
1050 system_date: String::new(),
1051 password_hash: String::new(),
1052 acquisition_software: String::new(),
1053 };
1054
1055 for (i, &key) in keys.iter().enumerate() {
1056 let val = vals.get(i).copied().unwrap_or("").to_owned();
1057 match key {
1058 "a" => meta.description = val,
1059 "c" => meta.case_number = val,
1060 "e" => meta.evidence_number = val,
1061 "t" => meta.examiner_name = val,
1062 "m" => meta.acquisition_date = val,
1063 "u" => meta.system_date = val,
1064 "p" => meta.password_hash = val,
1065 "r" => meta.acquisition_software = val,
1066 _ => {}
1067 }
1068 }
1069
1070 Some(meta)
1071}
1072
1073struct Section {
1074 type_name: String,
1075 offset: u64,
1076 size: u64,
1077}
1078
1079struct VolumeGeometry {
1080 chunk_count: u32,
1081 sectors_per_chunk: u32,
1082 bytes_per_sector: u32,
1083 sector_count: u64,
1084 set_identifier: [u8; 16],
1086}
1087
1088fn walk_sections_v1(data: &[u8], issues: &mut Vec<EwfIntegrityAnomaly>) -> Vec<Section> {
1089 let file_size = data.len() as u64;
1090 let mut sections = Vec::new();
1091 let mut pos = FILE_HEADER_SIZE as u64;
1092
1093 loop {
1094 let off = pos as usize;
1095 if off + SECTION_DESCRIPTOR_SIZE > data.len() {
1096 break;
1097 }
1098 let desc = &data[off..off + SECTION_DESCRIPTOR_SIZE];
1099
1100 let type_end = desc[..16].iter().position(|&b| b == 0).unwrap_or(16);
1101 let type_name = String::from_utf8_lossy(&desc[..type_end]).into_owned();
1102
1103 let stored_crc = u32::from_le_bytes(desc[72..76].try_into().unwrap());
1104 let computed_crc = adler32(&desc[..72]);
1105 if computed_crc != stored_crc {
1106 issues.push(EwfIntegrityAnomaly::SectionDescriptorCrcMismatch {
1107 offset: pos,
1108 section_type: type_name.clone(),
1109 computed: computed_crc,
1110 stored: stored_crc,
1111 });
1112 }
1113
1114 if !KNOWN_TYPES.contains(&type_name.as_str()) {
1115 issues.push(EwfIntegrityAnomaly::UnknownSectionType {
1116 offset: pos,
1117 type_name: type_name.clone(),
1118 });
1119 }
1120
1121 let next = u64::from_le_bytes(desc[16..24].try_into().unwrap());
1122 let section_size = u64::from_le_bytes(desc[24..32].try_into().unwrap());
1123 let section_end = pos.saturating_add(section_size);
1124
1125 sections.push(Section {
1126 type_name: type_name.clone(),
1127 offset: pos,
1128 size: section_size,
1129 });
1130
1131 if type_name == "done" || type_name == "next" {
1133 break;
1134 }
1135
1136 if next == 0 || next > file_size || next <= pos {
1137 issues.push(EwfIntegrityAnomaly::SectionChainBroken {
1138 at_offset: pos,
1139 next_offset: next,
1140 });
1141 break;
1142 }
1143
1144 if next > section_end {
1145 let gap_offset = section_end;
1146 let gap_size = next - section_end;
1147 let non_zero = data
1148 .get(section_end as usize..next as usize)
1149 .map(|s| s.iter().any(|&b| b != 0))
1150 .unwrap_or(false);
1151 if non_zero {
1152 issues.push(EwfIntegrityAnomaly::SectionGapNonZero { gap_offset, gap_size });
1153 } else {
1154 issues.push(EwfIntegrityAnomaly::SectionGapZero { gap_offset, gap_size });
1155 }
1156 }
1157
1158 pos = next;
1159 }
1160
1161 sections
1162}
1163
1164fn check_volume_v1(
1165 data: &[u8],
1166 desc_offset: u64,
1167 section_size: u64,
1168 issues: &mut Vec<EwfIntegrityAnomaly>,
1169) -> Option<VolumeGeometry> {
1170 let data_start = (desc_offset as usize) + SECTION_DESCRIPTOR_SIZE;
1171 if data.len() < data_start + VOLUME_DATA_MIN {
1172 return None;
1173 }
1174 let body_len = (section_size as usize).saturating_sub(SECTION_DESCRIPTOR_SIZE);
1175 let vol_end = (data_start + body_len).min(data.len());
1176 let vol = &data[data_start..vol_end];
1177
1178 let media_type = vol[0];
1180 if !VALID_MEDIA_TYPES.contains(&media_type) {
1181 issues.push(EwfIntegrityAnomaly::MediaTypeUnknown { media_type });
1182 }
1183
1184 let chunk_count = u32::from_le_bytes(vol[4..8].try_into().unwrap());
1185 let sectors_per_chunk = u32::from_le_bytes(vol[8..12].try_into().unwrap());
1186 let bytes_per_sector = u32::from_le_bytes(vol[12..16].try_into().unwrap());
1187 let sector_count = u64::from_le_bytes(vol[16..24].try_into().unwrap());
1188
1189 if bytes_per_sector != 512 && bytes_per_sector != 4096 {
1190 issues.push(EwfIntegrityAnomaly::BytesPerSectorInvalid { bytes_per_sector });
1191 }
1192 if sectors_per_chunk == 0 || !sectors_per_chunk.is_power_of_two() {
1193 issues.push(EwfIntegrityAnomaly::ChunkSizeInvalid {
1194 sectors_per_chunk,
1195 bytes_per_sector,
1196 });
1197 }
1198
1199 let max_sectors = u64::from(chunk_count) * u64::from(sectors_per_chunk);
1200 let min_sectors = max_sectors.saturating_sub(u64::from(sectors_per_chunk));
1201 if sectors_per_chunk.is_power_of_two() {
1202 let out_of_range =
1203 sector_count > max_sectors || (chunk_count > 0 && sector_count <= min_sectors);
1204 if out_of_range {
1205 issues.push(EwfIntegrityAnomaly::SectorCountMismatch {
1206 declared: sector_count,
1207 expected: max_sectors,
1208 });
1209 }
1210 }
1211
1212 let set_identifier: [u8; 16] = if vol.len() >= 80 {
1214 vol[64..80].try_into().unwrap()
1215 } else {
1216 [0u8; 16]
1217 };
1218
1219 if vol.len() >= VOLUME_DATA_FULL {
1222 let stored_crc = u32::from_le_bytes(vol[1048..1052].try_into().unwrap());
1223 let computed_crc = adler32(&vol[..1048]);
1224 if computed_crc != stored_crc {
1225 issues.push(EwfIntegrityAnomaly::VolumeBodyCrcMismatch {
1226 computed: computed_crc,
1227 stored: stored_crc,
1228 });
1229 }
1230 }
1231
1232 Some(VolumeGeometry {
1233 chunk_count,
1234 sectors_per_chunk,
1235 bytes_per_sector,
1236 sector_count,
1237 set_identifier,
1238 })
1239}
1240
1241fn check_table_v1(
1242 data: &[u8],
1243 desc_offset: u64,
1244 volume_chunk_count: Option<u32>,
1245 file_size: u64,
1246 sectors_range: Option<(u64, u64)>,
1247 issues: &mut Vec<EwfIntegrityAnomaly>,
1248) {
1249 let data_start = (desc_offset as usize) + SECTION_DESCRIPTOR_SIZE;
1250 if data.len() < data_start + 24 {
1251 return;
1252 }
1253 let tbl = &data[data_start..];
1254 let entry_count = u32::from_le_bytes(tbl[0..4].try_into().unwrap());
1255 let base_offset = u64::from_le_bytes(tbl[8..16].try_into().unwrap());
1256
1257 let stored_crc = u32::from_le_bytes(tbl[16..20].try_into().unwrap());
1260 if stored_crc != 0 {
1261 let computed_crc = adler32(&tbl[..16]);
1262 if computed_crc != stored_crc {
1263 issues.push(EwfIntegrityAnomaly::TableHeaderAdler32Mismatch {
1264 computed: computed_crc,
1265 stored: stored_crc,
1266 });
1267 }
1268 }
1269
1270 if let Some(vol_count) = volume_chunk_count {
1271 if entry_count != vol_count {
1272 issues.push(EwfIntegrityAnomaly::TableChunkCountMismatch {
1273 in_volume: vol_count,
1274 in_table: entry_count,
1275 });
1276 }
1277 }
1278
1279 let entries_start = data_start + 24;
1280 for i in 0..entry_count {
1281 let entry_off = entries_start + (i as usize) * 4;
1282 if entry_off + 4 > data.len() {
1283 break;
1284 }
1285 let raw = u32::from_le_bytes(data[entry_off..entry_off + 4].try_into().unwrap());
1286 let chunk_rel = u64::from(raw & 0x7FFF_FFFF);
1287 let absolute = base_offset.saturating_add(chunk_rel);
1288 if absolute >= file_size {
1289 issues.push(EwfIntegrityAnomaly::TableEntryOutOfBounds {
1290 chunk_index: i,
1291 entry_offset: absolute,
1292 file_size,
1293 });
1294 } else if let Some((sec_start, sec_end)) = sectors_range {
1295 if absolute < sec_start || absolute >= sec_end {
1296 issues.push(EwfIntegrityAnomaly::TableEntryOutsideSectorsRange {
1297 chunk_index: i,
1298 entry_offset: absolute,
1299 sectors_start: sec_start,
1300 sectors_end: sec_end,
1301 });
1302 }
1303 }
1304 }
1305}
1306
1307fn iter_segment_chunks(data: &[u8], sections: &[Section]) -> Vec<(usize, usize, bool)> {
1309 let table = match sections.iter().find(|s| s.type_name == "table") {
1310 Some(s) => s,
1311 None => return Vec::new(),
1312 };
1313 let sectors = match sections.iter().find(|s| s.type_name == "sectors") {
1314 Some(s) => s,
1315 None => return Vec::new(),
1316 };
1317
1318 let tbl_data_start = (table.offset as usize) + SECTION_DESCRIPTOR_SIZE;
1319 if data.len() < tbl_data_start + 24 {
1320 return Vec::new();
1321 }
1322 let tbl = &data[tbl_data_start..];
1323 let entry_count = u32::from_le_bytes(tbl[0..4].try_into().unwrap()) as usize;
1324 let base_offset = u64::from_le_bytes(tbl[8..16].try_into().unwrap()) as usize;
1325 let entries_start = tbl_data_start + 24;
1326 let sectors_body_end = (sectors.offset + sectors.size) as usize;
1327
1328 let mut chunks = Vec::with_capacity(entry_count);
1329 for i in 0..entry_count {
1330 let entry_off = entries_start + i * 4;
1331 if entry_off + 4 > data.len() {
1332 break;
1333 }
1334 let raw = u32::from_le_bytes(data[entry_off..entry_off + 4].try_into().unwrap());
1335 let compressed = raw & 0x8000_0000 != 0;
1336 let rel = (raw & 0x7FFF_FFFF) as usize;
1337 let start = base_offset + rel;
1338
1339 let end = if i + 1 < entry_count {
1340 let next_off = entries_start + (i + 1) * 4;
1341 if next_off + 4 > data.len() {
1342 break;
1343 }
1344 let next_raw = u32::from_le_bytes(data[next_off..next_off + 4].try_into().unwrap());
1345 let next_rel = (next_raw & 0x7FFF_FFFF) as usize;
1346 base_offset + next_rel
1347 } else {
1348 sectors_body_end.min(data.len())
1349 };
1350
1351 if start >= end || end > data.len() {
1352 break;
1353 }
1354 chunks.push((start, end, compressed));
1355 }
1356 chunks
1357}
1358
1359fn check_hash_all_segments(
1361 segments: &[&[u8]],
1362 all_sections: &[Vec<Section>],
1363 geom: &VolumeGeometry,
1364 expected_md5: Option<[u8; 16]>,
1365 expected_sha1: Option<[u8; 20]>,
1366 expected_sha256: Option<[u8; 32]>,
1367 issues: &mut Vec<EwfIntegrityAnomaly>,
1368 progress: &mut dyn FnMut(AnalysisProgress),
1369) {
1370 let chunk_size = u64::from(geom.sectors_per_chunk) * u64::from(geom.bytes_per_sector);
1371 let total_bytes = geom.sector_count * u64::from(geom.bytes_per_sector);
1372 let mut bytes_remaining = total_bytes;
1373
1374 let mut md5_h = Md5::new();
1375 let mut sha1_h = Sha1::new();
1376 let mut sha256_h = Sha256::new();
1377
1378 let chunk_size_usize = chunk_size as usize;
1379 let mut global_chunk_idx: usize = 0;
1380
1381 'outer: for (&seg_data, sections) in segments.iter().zip(all_sections.iter()) {
1382 for (start, end, compressed) in iter_segment_chunks(seg_data, sections) {
1383 if bytes_remaining == 0 {
1384 break 'outer;
1385 }
1386 let to_hash = bytes_remaining.min(chunk_size) as usize;
1387 let raw = &seg_data[start..end];
1388
1389 let this_chunk_idx = global_chunk_idx;
1399 global_chunk_idx += 1;
1400
1401 let has_uncompressed_checksum = !compressed && (raw.len() > chunk_size_usize);
1402 if has_uncompressed_checksum && raw.len() >= chunk_size_usize + 4 {
1403 let crc_end = chunk_size_usize;
1404 let stored = u32::from_le_bytes(raw[crc_end..crc_end + 4].try_into().unwrap());
1405 let computed = adler32(&raw[..crc_end]);
1406 if computed != stored {
1407 issues.push(EwfIntegrityAnomaly::ChunkChecksumMismatch {
1408 chunk_index: this_chunk_idx,
1409 computed,
1410 stored,
1411 });
1412 }
1413 }
1414
1415 if compressed {
1416 let limit = (to_hash as u64).saturating_add(1);
1417 let mut decompressed = Vec::with_capacity(to_hash);
1418 if ZlibDecoder::new(raw)
1419 .take(limit)
1420 .read_to_end(&mut decompressed)
1421 .is_err()
1422 {
1423 issues.push(EwfIntegrityAnomaly::ChunkDecompressionError {
1424 chunk_index: this_chunk_idx,
1425 });
1426 bytes_remaining = bytes_remaining.saturating_sub(to_hash as u64);
1427 continue;
1428 }
1429 let slice = &decompressed[..decompressed.len().min(to_hash)];
1430 md5_h.update(slice);
1431 sha1_h.update(slice);
1432 sha256_h.update(slice);
1433 } else {
1434 let slice = &raw[..raw.len().min(to_hash)];
1437 md5_h.update(slice);
1438 sha1_h.update(slice);
1439 sha256_h.update(slice);
1440 }
1441 bytes_remaining = bytes_remaining.saturating_sub(to_hash as u64);
1442 progress(AnalysisProgress {
1443 chunks_done: global_chunk_idx,
1444 chunks_total: None,
1445 bytes_done: total_bytes - bytes_remaining,
1446 });
1447 }
1448 }
1449
1450 let computed_md5: [u8; 16] = md5_h.finalize().into();
1451 let computed_sha1: [u8; 20] = sha1_h.finalize().into();
1452 let computed_sha256: [u8; 32] = sha256_h.finalize().into();
1453
1454 let last_sections = match all_sections.last() {
1455 Some(s) => s,
1456 None => return,
1457 };
1458 let last_data = match segments.last() {
1459 Some(d) => d,
1460 None => return,
1461 };
1462
1463 match last_sections.iter().find(|s| s.type_name == "hash") {
1465 Some(hash_sec) => {
1466 let body_start = (hash_sec.offset as usize) + SECTION_DESCRIPTOR_SIZE;
1467 if let Some(stored_slice) = last_data.get(body_start..body_start + 16) {
1468 let stored: [u8; 16] = stored_slice.try_into().unwrap();
1469 if computed_md5 != stored {
1470 issues.push(EwfIntegrityAnomaly::HashMismatch {
1471 computed: computed_md5,
1472 stored,
1473 });
1474 }
1475 }
1476 }
1477 None => issues.push(EwfIntegrityAnomaly::HashSectionMissing),
1478 }
1479
1480 if let Some(digest_sec) = last_sections.iter().find(|s| s.type_name == "digest") {
1482 let body_start = (digest_sec.offset as usize) + SECTION_DESCRIPTOR_SIZE;
1483 if let Some(sha1_slice) = last_data.get(body_start + 16..body_start + 36) {
1484 let stored: [u8; 20] = sha1_slice.try_into().unwrap();
1485 if stored != [0u8; 20] && computed_sha1 != stored {
1487 issues.push(EwfIntegrityAnomaly::DigestSha1Mismatch {
1488 computed: computed_sha1,
1489 stored,
1490 });
1491 }
1492 }
1493 }
1494
1495 if let Some(expected) = expected_md5 {
1497 if computed_md5 != expected {
1498 issues.push(EwfIntegrityAnomaly::ExternalMd5Mismatch {
1499 computed: computed_md5,
1500 expected,
1501 });
1502 }
1503 }
1504 if let Some(expected) = expected_sha1 {
1505 if computed_sha1 != expected {
1506 issues.push(EwfIntegrityAnomaly::ExternalSha1Mismatch {
1507 computed: computed_sha1,
1508 expected,
1509 });
1510 }
1511 }
1512 if let Some(expected) = expected_sha256 {
1513 if computed_sha256 != expected {
1514 issues.push(EwfIntegrityAnomaly::ExternalSha256Mismatch {
1515 computed: computed_sha256,
1516 expected,
1517 });
1518 }
1519 }
1520}
1521
1522fn parse_media_info_body(body: &[u8]) -> bool {
1532 if body.is_empty() {
1533 return false;
1534 }
1535 let mut decompressed = Vec::new();
1536 if ZlibDecoder::new(body).read_to_end(&mut decompressed).is_err() {
1537 return false;
1538 }
1539 let text_bytes = if decompressed.starts_with(&[0xFF, 0xFE]) {
1541 &decompressed[2..]
1542 } else {
1543 &decompressed[..]
1544 };
1545 if text_bytes.len() % 2 != 0 {
1547 return false;
1548 }
1549 let units: Vec<u16> = text_bytes
1550 .chunks_exact(2)
1551 .map(|b| u16::from_le_bytes([b[0], b[1]]))
1552 .collect();
1553 String::from_utf16(&units).is_ok()
1554}
1555
1556fn verify_ewf2_sector_data(
1560 data: &[u8],
1561 ct_start: usize,
1562 ct_end: usize,
1563 stored_md5: Option<[u8; 16]>,
1564 stored_sha1: Option<[u8; 20]>,
1565 stored_sha256: Option<[u8; 32]>,
1566 issues: &mut Vec<EwfIntegrityAnomaly>,
1567 progress: &mut dyn FnMut(AnalysisProgress),
1568) -> Option<ComputedHashes> {
1569 let tbl = match data.get(ct_start..ct_end) {
1570 Some(t) => t,
1571 None => return None,
1572 };
1573 if tbl.len() < EVF2_CHUNK_TABLE_HEADER_SIZE + EVF2_CHUNK_TABLE_ENTRY_SIZE {
1574 return None;
1575 }
1576 let chunk_count = u64::from_le_bytes(tbl[8..16].try_into().unwrap()) as usize;
1577
1578 let checksum_off = EVF2_CHUNK_TABLE_HEADER_SIZE + chunk_count * EVF2_CHUNK_TABLE_ENTRY_SIZE;
1580 if checksum_off + 4 <= tbl.len() {
1581 let computed_cs = adler32(&tbl[EVF2_CHUNK_TABLE_HEADER_SIZE..checksum_off]);
1582 let stored_cs = u32::from_le_bytes(tbl[checksum_off..checksum_off + 4].try_into().unwrap());
1583 if computed_cs != stored_cs {
1584 issues.push(EwfIntegrityAnomaly::Ewf2ChunkTableChecksumMismatch {
1585 computed: computed_cs,
1586 stored: stored_cs,
1587 });
1588 }
1589 }
1590
1591 let mut md5_h = Md5::new();
1592 let mut sha1_h = Sha1::new();
1593 let mut sha256_h = Sha256::new();
1594
1595 for i in 0..chunk_count {
1596 let entry_off = EVF2_CHUNK_TABLE_HEADER_SIZE + i * EVF2_CHUNK_TABLE_ENTRY_SIZE;
1597 if entry_off + EVF2_CHUNK_TABLE_ENTRY_SIZE > tbl.len() {
1598 break;
1599 }
1600 let file_offset = u64::from_le_bytes(tbl[entry_off..entry_off + 8].try_into().unwrap()) as usize;
1601 let chunk_data_size = u32::from_le_bytes(tbl[entry_off + 8..entry_off + 12].try_into().unwrap()) as usize;
1602 let flags = u32::from_le_bytes(tbl[entry_off + 12..entry_off + 16].try_into().unwrap());
1603
1604 let raw_size = chunk_data_size.saturating_sub(4);
1606 let chunk_raw = match data.get(file_offset..file_offset + raw_size) {
1607 Some(r) => r,
1608 None => break,
1609 };
1610
1611 if chunk_data_size >= 4 {
1613 if let Some(crc_bytes) = data.get(file_offset + raw_size..file_offset + raw_size + 4) {
1614 let stored_crc = u32::from_le_bytes(crc_bytes.try_into().unwrap());
1615 let computed_crc = adler32(chunk_raw);
1616 if computed_crc != stored_crc {
1617 issues.push(EwfIntegrityAnomaly::ChunkChecksumMismatch {
1618 chunk_index: i,
1619 computed: computed_crc,
1620 stored: stored_crc,
1621 });
1622 }
1623 }
1624 }
1625
1626 if flags & EVF2_CHUNK_FLAG_COMPRESSED != 0 {
1627 let mut decompressed = Vec::with_capacity(raw_size);
1629 if ZlibDecoder::new(chunk_raw)
1630 .read_to_end(&mut decompressed)
1631 .is_err()
1632 {
1633 issues.push(EwfIntegrityAnomaly::ChunkDecompressionError { chunk_index: i });
1634 continue;
1635 }
1636 md5_h.update(&decompressed);
1637 sha1_h.update(&decompressed);
1638 sha256_h.update(&decompressed);
1639 } else {
1640 md5_h.update(chunk_raw);
1641 sha1_h.update(chunk_raw);
1642 sha256_h.update(chunk_raw);
1643 }
1644 progress(AnalysisProgress {
1645 chunks_done: i + 1,
1646 chunks_total: Some(chunk_count),
1647 bytes_done: ((i + 1) * raw_size) as u64,
1648 });
1649 }
1650
1651 let computed_md5: [u8; 16] = md5_h.finalize().into();
1652 let computed_sha1: [u8; 20] = sha1_h.finalize().into();
1653 let computed_sha256: [u8; 32] = sha256_h.finalize().into();
1654
1655 if let Some(stored) = stored_md5 {
1656 if computed_md5 != stored {
1657 issues.push(EwfIntegrityAnomaly::HashMismatch { computed: computed_md5, stored });
1658 }
1659 }
1660
1661 if let Some(stored) = stored_sha1 {
1662 if computed_sha1 != stored {
1663 issues.push(EwfIntegrityAnomaly::DigestSha1Mismatch {
1664 computed: computed_sha1,
1665 stored,
1666 });
1667 }
1668 }
1669
1670 if let Some(stored) = stored_sha256 {
1671 if computed_sha256 != stored {
1672 issues.push(EwfIntegrityAnomaly::DigestSha256Mismatch {
1673 computed: computed_sha256,
1674 stored,
1675 });
1676 }
1677 }
1678
1679 Some(ComputedHashes { md5: computed_md5, sha1: computed_sha1, sha256: computed_sha256 })
1680}
1681
1682fn compute_hashes_ewf2(segments: &[&[u8]]) -> Option<ComputedHashes> {
1684 let mut md5_h = Md5::new();
1685 let mut sha1_h = Sha1::new();
1686 let mut sha256_h = Sha256::new();
1687 let mut found_chunks = false;
1688
1689 for &data in segments {
1690 if data.len() < EVF2_FILE_HEADER_SIZE + EVF2_SECTION_DESCRIPTOR_SIZE {
1691 continue;
1692 }
1693
1694 let mut desc_offset = data.len().saturating_sub(EVF2_SECTION_DESCRIPTOR_SIZE);
1696 let mut chunk_table_body: Option<(usize, usize)> = None;
1697
1698 loop {
1699 if desc_offset + EVF2_SECTION_DESCRIPTOR_SIZE > data.len()
1700 || desc_offset < EVF2_FILE_HEADER_SIZE
1701 {
1702 break;
1703 }
1704 let desc = &data[desc_offset..desc_offset + EVF2_SECTION_DESCRIPTOR_SIZE];
1705 let section_type = u32::from_le_bytes(desc[0..4].try_into().unwrap());
1706 let data_flags = u32::from_le_bytes(desc[4..8].try_into().unwrap());
1707 let prev_offset = u64::from_le_bytes(desc[8..16].try_into().unwrap()) as usize;
1708 let data_size = u64::from_le_bytes(desc[16..24].try_into().unwrap()) as usize;
1709 let body_end = desc_offset;
1710 let body_start = desc_offset.saturating_sub(data_size);
1711
1712 if data_flags & EVF2_DATA_FLAG_ENCRYPTED == 0
1713 && section_type == EVF2_TYPE_CHUNK_TABLE
1714 {
1715 chunk_table_body = Some((body_start, body_end));
1716 }
1717
1718 if prev_offset == 0 { break; }
1719 desc_offset = prev_offset;
1720 }
1721
1722 let (ct_start, ct_end) = match chunk_table_body {
1723 Some(b) => b,
1724 None => continue,
1725 };
1726 let tbl = match data.get(ct_start..ct_end) {
1727 Some(t) => t,
1728 None => continue,
1729 };
1730 if tbl.len() < EVF2_CHUNK_TABLE_HEADER_SIZE + EVF2_CHUNK_TABLE_ENTRY_SIZE {
1731 continue;
1732 }
1733 let chunk_count = u64::from_le_bytes(tbl[8..16].try_into().unwrap()) as usize;
1734
1735 for i in 0..chunk_count {
1736 let entry_off = EVF2_CHUNK_TABLE_HEADER_SIZE + i * EVF2_CHUNK_TABLE_ENTRY_SIZE;
1737 if entry_off + EVF2_CHUNK_TABLE_ENTRY_SIZE > tbl.len() { break; }
1738 let file_offset = u64::from_le_bytes(tbl[entry_off..entry_off + 8].try_into().unwrap()) as usize;
1739 let chunk_data_size = u32::from_le_bytes(tbl[entry_off + 8..entry_off + 12].try_into().unwrap()) as usize;
1740 let flags = u32::from_le_bytes(tbl[entry_off + 12..entry_off + 16].try_into().unwrap());
1741 let raw_size = chunk_data_size.saturating_sub(4);
1742 let chunk_raw = match data.get(file_offset..file_offset + raw_size) {
1743 Some(r) => r,
1744 None => break,
1745 };
1746
1747 if flags & EVF2_CHUNK_FLAG_COMPRESSED != 0 {
1748 let mut decompressed = Vec::with_capacity(raw_size);
1749 if ZlibDecoder::new(chunk_raw)
1750 .read_to_end(&mut decompressed)
1751 .is_err()
1752 {
1753 continue;
1754 }
1755 md5_h.update(&decompressed);
1756 sha1_h.update(&decompressed);
1757 sha256_h.update(&decompressed);
1758 } else {
1759 md5_h.update(chunk_raw);
1760 sha1_h.update(chunk_raw);
1761 sha256_h.update(chunk_raw);
1762 }
1763 found_chunks = true;
1764 }
1765 }
1766
1767 if !found_chunks {
1768 return None;
1769 }
1770 Some(ComputedHashes {
1771 md5: md5_h.finalize().into(),
1772 sha1: sha1_h.finalize().into(),
1773 sha256: sha256_h.finalize().into(),
1774 })
1775}
1776
1777fn compute_hashes_ewf1(segments: &[&[u8]]) -> Option<ComputedHashes> {
1780 let first = segments.first().copied()?;
1781 if first.len() < FILE_HEADER_SIZE {
1782 return None;
1783 }
1784 if first[0..8] != EVF_SIGNATURE && first[0..8] != DVF_SIGNATURE && first[0..8] != LVF_SIGNATURE {
1785 return None;
1786 }
1787
1788 let mut dummy = Vec::new();
1789 let sections_first = walk_sections_v1(first, &mut dummy);
1790 let vol_sec = sections_first
1791 .iter()
1792 .find(|s| s.type_name == "volume" || s.type_name == "disk")?;
1793 let geom = check_volume_v1(first, vol_sec.offset, vol_sec.size, &mut dummy)?;
1794
1795 let chunk_size = u64::from(geom.sectors_per_chunk) * u64::from(geom.bytes_per_sector);
1796 let total_bytes = geom.sector_count * u64::from(geom.bytes_per_sector);
1797 let mut bytes_remaining = total_bytes;
1798
1799 let mut md5_h = Md5::new();
1800 let mut sha1_h = Sha1::new();
1801 let mut sha256_h = Sha256::new();
1802
1803 let mut all_sections: Vec<Vec<Section>> = Vec::new();
1804 for &seg in segments {
1805 let mut d = Vec::new();
1806 all_sections.push(walk_sections_v1(seg, &mut d));
1807 }
1808
1809 'outer: for (&seg_data, sections) in segments.iter().zip(all_sections.iter()) {
1810 for (start, end, compressed) in iter_segment_chunks(seg_data, sections) {
1811 if bytes_remaining == 0 {
1812 break 'outer;
1813 }
1814 let to_hash = bytes_remaining.min(chunk_size) as usize;
1815 let raw = &seg_data[start..end];
1816
1817 if compressed {
1818 let limit = (to_hash as u64).saturating_add(1);
1819 let mut decompressed = Vec::with_capacity(to_hash);
1820 if ZlibDecoder::new(raw)
1821 .take(limit)
1822 .read_to_end(&mut decompressed)
1823 .is_err()
1824 {
1825 bytes_remaining = bytes_remaining.saturating_sub(to_hash as u64);
1826 continue;
1827 }
1828 let slice = &decompressed[..decompressed.len().min(to_hash)];
1829 md5_h.update(slice);
1830 sha1_h.update(slice);
1831 sha256_h.update(slice);
1832 } else {
1833 let slice = &raw[..raw.len().min(to_hash)];
1834 md5_h.update(slice);
1835 sha1_h.update(slice);
1836 sha256_h.update(slice);
1837 }
1838 bytes_remaining = bytes_remaining.saturating_sub(to_hash as u64);
1839 }
1840 }
1841
1842 Some(ComputedHashes {
1843 md5: md5_h.finalize().into(),
1844 sha1: sha1_h.finalize().into(),
1845 sha256: sha256_h.finalize().into(),
1846 })
1847}
1848
1849pub(crate) fn adler32(data: &[u8]) -> u32 {
1850 const MOD: u32 = 65521;
1851 let mut s1: u32 = 1;
1852 let mut s2: u32 = 0;
1853 for &b in data {
1854 s1 = (s1 + u32::from(b)) % MOD;
1855 s2 = (s2 + s1) % MOD;
1856 }
1857 (s2 << 16) | s1
1858}
1859
1860
1861impl EwfIntegrityAnomaly {
1862 #[must_use]
1864 pub fn code(&self) -> &'static str {
1865 match self {
1866 Self::InvalidSignature => "EWF-INVALID-SIGNATURE",
1867 Self::SegmentNumberZero => "EWF-SEGMENT-NUMBER-ZERO",
1868 Self::SectionDescriptorCrcMismatch { .. } => "EWF-SECTION-DESCRIPTOR-CRC-MISMATCH",
1869 Self::SectionChainBroken { .. } => "EWF-SECTION-CHAIN-BROKEN",
1870 Self::SectionGapNonZero { .. } => "EWF-SECTION-GAP-NON-ZERO",
1871 Self::VolumeSectionMissing => "EWF-VOLUME-SECTION-MISSING",
1872 Self::UnknownSectionType { .. } => "EWF-UNKNOWN-SECTION-TYPE",
1873 Self::DoneSectionMissing => "EWF-DONE-SECTION-MISSING",
1874 Self::SectorsSectionMissing => "EWF-SECTORS-SECTION-MISSING",
1875 Self::TableSectionMissing => "EWF-TABLE-SECTION-MISSING",
1876 Self::ChunkSizeInvalid { .. } => "EWF-CHUNK-SIZE-INVALID",
1877 Self::SectorCountMismatch { .. } => "EWF-SECTOR-COUNT-MISMATCH",
1878 Self::BytesPerSectorInvalid { .. } => "EWF-BYTES-PER-SECTOR-INVALID",
1879 Self::TableChunkCountMismatch { .. } => "EWF-TABLE-CHUNK-COUNT-MISMATCH",
1880 Self::TableHeaderAdler32Mismatch { .. } => "EWF-TABLE-HEADER-ADLER32-MISMATCH",
1881 Self::TableEntryOutOfBounds { .. } => "EWF-TABLE-ENTRY-OUT-OF-BOUNDS",
1882 Self::TableEntryOutsideSectorsRange { .. } => "EWF-TABLE-ENTRY-OUTSIDE-SECTORS-RANGE",
1883 Self::SectionGapZero { .. } => "EWF-SECTION-GAP-ZERO",
1884 Self::HashMismatch { .. } => "EWF-HASH-MISMATCH",
1885 Self::HashSectionMissing => "EWF-HASH-SECTION-MISSING",
1886 Self::Table2Mismatch { .. } => "EWF-TABLE2-MISMATCH",
1887 Self::BadSectorsPresent { .. } => "EWF-BAD-SECTORS-PRESENT",
1888 Self::SegmentOutOfOrder { .. } => "EWF-SEGMENT-OUT-OF-ORDER",
1889 Self::DigestSha1Mismatch { .. } => "EWF-DIGEST-SHA1-MISMATCH",
1890 Self::DigestSha256Mismatch { .. } => "EWF-DIGEST-SHA256-MISMATCH",
1891 Self::ExternalMd5Mismatch { .. } => "EWF-EXTERNAL-MD5-MISMATCH",
1892 Self::ExternalSha1Mismatch { .. } => "EWF-EXTERNAL-SHA1-MISMATCH",
1893 Self::Ewf2SectionDataHashMismatch { .. } => "EWF-EWF2-SECTION-DATA-HASH-MISMATCH",
1894 Self::Ewf2EncryptedSection { .. } => "EWF-EWF2-ENCRYPTED-SECTION",
1895 Self::Ewf2HashSectionMissing => "EWF-EWF2-HASH-SECTION-MISSING",
1896 Self::VolumeBodyCrcMismatch { .. } => "EWF-VOLUME-BODY-CRC-MISMATCH",
1897 Self::MediaTypeUnknown { .. } => "EWF-MEDIA-TYPE-UNKNOWN",
1898 Self::SetIdentifierMismatch { .. } => "EWF-SET-IDENTIFIER-MISMATCH",
1899 Self::Ewf2MediaInfoMissing => "EWF-EWF2-MEDIA-INFO-MISSING",
1900 Self::Ewf2ChunkTableChecksumMismatch { .. } => "EWF-EWF2-CHUNK-TABLE-CHECKSUM-MISMATCH",
1901 Self::ChunkChecksumMismatch { .. } => "EWF-CHUNK-CHECKSUM-MISMATCH",
1902 Self::ChunkDecompressionError { .. } => "EWF-CHUNK-DECOMPRESSION-ERROR",
1903 Self::UnsupportedCompressionAlgorithm { .. } => "EWF-UNSUPPORTED-COMPRESSION-ALGORITHM",
1904 Self::ExternalSha256Mismatch { .. } => "EWF-EXTERNAL-SHA256-MISMATCH",
1905 Self::Ewf2MediaInfoParseFailed => "EWF-EWF2-MEDIA-INFO-PARSE-FAILED",
1906 }
1907 }
1908}
1909
1910impl forensicnomicon::report::Observation for EwfIntegrityAnomaly {
1911 fn severity(&self) -> Option<Severity> {
1912 Some(self.severity())
1913 }
1914 fn code(&self) -> &'static str {
1915 self.code()
1916 }
1917 fn note(&self) -> String {
1918 self.to_string()
1919 }
1920}