1#![cfg_attr(not(test), deny(clippy::arithmetic_side_effects))]
6
7use flate2::read::ZlibDecoder;
8use md5::{Digest as _, Md5};
9use sha1::Sha1;
10use sha2::Sha256;
11use std::fmt;
12use std::io::Read as _;
13
14use ewf::sections::{
23 self, EwfVolume, SectionDescriptor, TableHeader, EVF_SIGNATURE, FILE_HEADER_SIZE,
24 SECTION_DESCRIPTOR_SIZE as EWF_SECTION_DESCRIPTOR_SIZE, TABLE_HEADER_SIZE,
25};
26
27pub(crate) const SECTION_DESCRIPTOR_SIZE: usize = EWF_SECTION_DESCRIPTOR_SIZE;
30
31const DVF_SIGNATURE: [u8; 8] = [0x64, 0x76, 0x66, 0x09, 0x0d, 0x0a, 0xff, 0x00];
33const LVF_SIGNATURE: [u8; 8] = [0x4c, 0x56, 0x46, 0x09, 0x0d, 0x0a, 0xff, 0x00];
35
36const VOLUME_DATA_MIN: usize = 24;
37const VALID_MEDIA_TYPES: &[u8] = &[0x00, 0x01, 0x03, 0x0e, 0x10];
39
40const KNOWN_TYPES: &[&str] = &[
41 "header",
42 "header2",
43 "volume",
44 "disk",
45 "table",
46 "table2",
47 "sectors",
48 "hash",
49 "digest",
50 "error2",
51 "session",
52 "done",
53 "next",
54 "data",
55 "ltree",
56 "ltreedata",
57];
58
59const EVF2_SIGNATURE: [u8; 8] = [0x45, 0x56, 0x46, 0x32, 0x0d, 0x0a, 0x81, 0x00];
62const LEF2_SIGNATURE: [u8; 8] = [0x4c, 0x45, 0x46, 0x32, 0x0d, 0x0a, 0x81, 0x00];
63const EVF2_FILE_HEADER_SIZE: usize = 32;
64const EVF2_SECTION_DESCRIPTOR_SIZE: usize = 64;
65const EVF2_DATA_FLAG_ENCRYPTED: u32 = 0x0000_0002;
66const EVF2_CHUNK_FLAG_COMPRESSED: u32 = 0x0000_0001;
67const EVF2_TYPE_MEDIA_INFO: u32 = 0x02;
68const EVF2_TYPE_CHUNK_TABLE: u32 = 0x04;
69const EVF2_TYPE_MD5_HASH: u32 = 0x08;
70const EVF2_TYPE_SHA1_HASH: u32 = 0x09;
71const EVF2_TYPE_SHA256_HASH: u32 = 0x0A;
72const EVF2_CHUNK_TABLE_HEADER_SIZE: usize = 32;
73const EVF2_CHUNK_TABLE_ENTRY_SIZE: usize = 16;
74
75pub use forensicnomicon::report::Severity;
80
81#[derive(Debug, Clone, PartialEq, Eq)]
82#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
83pub enum EwfIntegrityAnomaly {
84 InvalidSignature,
86 SegmentNumberZero,
87 SectionDescriptorCrcMismatch {
88 offset: u64,
89 section_type: String,
90 computed: u32,
91 stored: u32,
92 },
93 SectionChainBroken {
94 at_offset: u64,
95 next_offset: u64,
96 },
97 SectionGapNonZero {
98 gap_offset: u64,
99 gap_size: u64,
100 },
101 VolumeSectionMissing,
102 UnknownSectionType {
103 offset: u64,
104 type_name: String,
105 },
106 DoneSectionMissing,
107 SectorsSectionMissing,
109 TableSectionMissing,
111 ChunkSizeInvalid {
112 sectors_per_chunk: u32,
113 bytes_per_sector: u32,
114 },
115 SectorCountMismatch {
116 declared: u64,
117 expected: u64,
118 },
119 BytesPerSectorInvalid {
120 bytes_per_sector: u32,
121 },
122 TableChunkCountMismatch {
123 in_volume: u32,
124 in_table: u32,
125 },
126 TableHeaderAdler32Mismatch {
127 computed: u32,
128 stored: u32,
129 },
130 TableEntryOutOfBounds {
131 chunk_index: u32,
132 entry_offset: u64,
133 file_size: u64,
134 },
135 TableEntryOutsideSectorsRange {
136 chunk_index: u32,
137 entry_offset: u64,
138 sectors_start: u64,
139 sectors_end: u64,
140 },
141 SectionGapZero {
142 gap_offset: u64,
143 gap_size: u64,
144 },
145 HashMismatch {
146 computed: [u8; 16],
147 stored: [u8; 16],
148 },
149 HashSectionMissing,
150 Table2Mismatch {
152 offset: usize,
154 },
155 BadSectorsPresent {
157 count: u32,
159 },
160 SegmentOutOfOrder {
163 segment_number: u16,
164 expected: u16,
165 },
166 DigestSha1Mismatch {
169 computed: [u8; 20],
170 stored: [u8; 20],
171 },
172 DigestSha256Mismatch {
174 computed: [u8; 32],
175 stored: [u8; 32],
176 },
177 ExternalMd5Mismatch {
180 computed: [u8; 16],
181 expected: [u8; 16],
182 },
183 ExternalSha1Mismatch {
185 computed: [u8; 20],
186 expected: [u8; 20],
187 },
188 Ewf2SectionDataHashMismatch {
191 offset: u64,
192 section_type_id: u32,
193 computed: [u8; 16],
194 stored: [u8; 16],
195 },
196 Ewf2EncryptedSection {
198 offset: u64,
199 },
200 Ewf2HashSectionMissing,
202 VolumeBodyCrcMismatch {
205 computed: u32,
206 stored: u32,
207 },
208 MediaTypeUnknown {
211 media_type: u8,
212 },
213 SetIdentifierMismatch {
216 segment: usize,
217 },
218 Ewf2MediaInfoMissing,
220 Ewf2ChunkTableChecksumMismatch {
223 computed: u32,
224 stored: u32,
225 },
226 ChunkChecksumMismatch {
229 chunk_index: usize,
230 computed: u32,
231 stored: u32,
232 },
233 ChunkDecompressionError {
236 chunk_index: usize,
237 },
238 UnsupportedCompressionAlgorithm {
240 method_id: u16,
242 },
243 ExternalSha256Mismatch {
245 computed: [u8; 32],
246 expected: [u8; 32],
247 },
248 Ewf2MediaInfoParseFailed,
252}
253
254impl EwfIntegrityAnomaly {
255 pub fn severity(&self) -> Severity {
256 match self {
257 Self::InvalidSignature => Severity::Critical,
258 Self::SegmentNumberZero => Severity::High,
259 Self::SectionDescriptorCrcMismatch { .. } => Severity::High,
260 Self::SectionChainBroken { .. } => Severity::Critical,
261 Self::SectionGapNonZero { .. } => Severity::Medium,
262 Self::VolumeSectionMissing => Severity::Critical,
263 Self::UnknownSectionType { .. } => Severity::Medium,
264 Self::DoneSectionMissing => Severity::Medium,
265 Self::SectorsSectionMissing => Severity::High,
266 Self::TableSectionMissing => Severity::High,
267 Self::ChunkSizeInvalid { .. } => Severity::High,
268 Self::SectorCountMismatch { .. } => Severity::High,
269 Self::BytesPerSectorInvalid { .. } => Severity::High,
270 Self::TableChunkCountMismatch { .. } => Severity::High,
271 Self::TableHeaderAdler32Mismatch { .. } => Severity::High,
272 Self::TableEntryOutOfBounds { .. } => Severity::High,
273 Self::TableEntryOutsideSectorsRange { .. } => Severity::High,
274 Self::SectionGapZero { .. } => Severity::Info,
275 Self::HashMismatch { .. } => Severity::High,
276 Self::HashSectionMissing => Severity::Medium,
277 Self::Table2Mismatch { .. } => Severity::High,
278 Self::BadSectorsPresent { .. } => Severity::Medium,
279 Self::SegmentOutOfOrder { .. } => Severity::High,
280 Self::DigestSha1Mismatch { .. } => Severity::High,
281 Self::DigestSha256Mismatch { .. } => Severity::High,
282 Self::ExternalMd5Mismatch { .. } => Severity::Critical,
283 Self::ExternalSha1Mismatch { .. } => Severity::Critical,
284 Self::VolumeBodyCrcMismatch { .. } => Severity::High,
285 Self::MediaTypeUnknown { .. } => Severity::Medium,
286 Self::SetIdentifierMismatch { .. } => Severity::High,
287 Self::Ewf2SectionDataHashMismatch { .. } => Severity::High,
288 Self::Ewf2EncryptedSection { .. } => Severity::Medium,
289 Self::Ewf2HashSectionMissing => Severity::Medium,
290 Self::Ewf2MediaInfoMissing => Severity::Medium,
291 Self::Ewf2ChunkTableChecksumMismatch { .. } => Severity::High,
292 Self::ChunkChecksumMismatch { .. } => Severity::High,
293 Self::ChunkDecompressionError { .. } => Severity::High,
294 Self::UnsupportedCompressionAlgorithm { .. } => Severity::High,
295 Self::ExternalSha256Mismatch { .. } => Severity::Critical,
296 Self::Ewf2MediaInfoParseFailed => Severity::High,
297 }
298 }
299}
300
301impl fmt::Display for EwfIntegrityAnomaly {
302 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
303 match self {
304 Self::InvalidSignature =>
305 write!(f, "invalid EWF signature — not a valid E01/Ex01 file"),
306 Self::SegmentNumberZero =>
307 write!(f, "segment number is zero (expected ≥ 1)"),
308 Self::SectionDescriptorCrcMismatch { offset, section_type, computed, stored } =>
309 write!(f, "section '{section_type}' at 0x{offset:x}: descriptor CRC mismatch (computed 0x{computed:08x}, stored 0x{stored:08x})"),
310 Self::SectionChainBroken { at_offset, next_offset } =>
311 write!(f, "section chain broken at 0x{at_offset:x}: next pointer 0x{next_offset:x} is invalid"),
312 Self::SectionGapNonZero { gap_offset, gap_size } =>
313 write!(f, "non-zero data in {gap_size}-byte gap at 0x{gap_offset:x} — possible hidden data"),
314 Self::VolumeSectionMissing =>
315 write!(f, "volume/disk section missing in segment 1"),
316 Self::UnknownSectionType { offset, type_name } =>
317 write!(f, "unknown section type '{type_name}' at 0x{offset:x}"),
318 Self::DoneSectionMissing =>
319 write!(f, "done section missing from final segment"),
320 Self::SectorsSectionMissing =>
321 write!(f, "sectors section missing — chunk data not found in segment"),
322 Self::TableSectionMissing =>
323 write!(f, "table section missing — chunk offset table not found in segment"),
324 Self::ChunkSizeInvalid { sectors_per_chunk, bytes_per_sector } =>
325 write!(f, "invalid chunk size: {sectors_per_chunk} sectors × {bytes_per_sector} bytes/sector"),
326 Self::SectorCountMismatch { declared, expected } =>
327 write!(f, "sector count mismatch: declared {declared}, expected {expected}"),
328 Self::BytesPerSectorInvalid { bytes_per_sector } =>
329 write!(f, "invalid bytes_per_sector: {bytes_per_sector} (expected 512 or 4096)"),
330 Self::TableChunkCountMismatch { in_volume, in_table } =>
331 write!(f, "chunk count mismatch: volume declares {in_volume}, table has {in_table}"),
332 Self::TableHeaderAdler32Mismatch { computed, stored } =>
333 write!(f, "table header Adler-32 mismatch: computed 0x{computed:08x}, stored 0x{stored:08x}"),
334 Self::TableEntryOutOfBounds { chunk_index, entry_offset, file_size } =>
335 write!(f, "table entry for chunk {chunk_index} points outside file: 0x{entry_offset:x} ≥ 0x{file_size:x}"),
336 Self::TableEntryOutsideSectorsRange { chunk_index, entry_offset, sectors_start, sectors_end } =>
337 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}]"),
338 Self::SectionGapZero { gap_offset, gap_size } =>
339 write!(f, "zero-padded {gap_size}-byte gap at 0x{gap_offset:x}"),
340 Self::HashMismatch { computed, stored } =>
341 write!(f, "MD5 mismatch: computed {}, stored {}", hex(computed), hex(stored)),
342 Self::HashSectionMissing =>
343 write!(f, "hash section missing — cannot verify MD5"),
344 Self::Table2Mismatch { offset } =>
345 write!(f, "table2 body differs from table at byte offset {offset} — one redundant copy is corrupt"),
346 Self::BadSectorsPresent { count } =>
347 write!(f, "error2 section reports {count} unreadable sector range(s) from acquisition"),
348 Self::SegmentOutOfOrder { segment_number, expected } =>
349 write!(f, "segment {segment_number} found where segment {expected} was expected"),
350 Self::DigestSha1Mismatch { computed, stored } =>
351 write!(f, "SHA-1 mismatch: computed {}, stored {}", hex(computed), hex(stored)),
352 Self::DigestSha256Mismatch { computed, stored } =>
353 write!(f, "SHA-256 mismatch: computed {}, stored {}", hex(computed), hex(stored)),
354 Self::ExternalMd5Mismatch { computed, expected } =>
355 write!(f, "MD5 does not match chain-of-custody reference: computed {}, expected {}", hex(computed), hex(expected)),
356 Self::ExternalSha1Mismatch { computed, expected } =>
357 write!(f, "SHA-1 does not match chain-of-custody reference: computed {}, expected {}", hex(computed), hex(expected)),
358 Self::ExternalSha256Mismatch { computed, expected } =>
359 write!(f, "SHA-256 does not match chain-of-custody reference: computed {}, expected {}", hex(computed), hex(expected)),
360 Self::Ewf2SectionDataHashMismatch { offset, section_type_id, computed, stored } =>
361 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)),
362 Self::Ewf2EncryptedSection { offset } =>
363 write!(f, "EWF v2 encrypted section at 0x{offset:x} — content not verifiable"),
364 Self::Ewf2HashSectionMissing =>
365 write!(f, "EWF v2 hash section missing from final segment"),
366 Self::VolumeBodyCrcMismatch { computed, stored } =>
367 write!(f, "volume section body CRC mismatch (computed 0x{computed:08x}, stored 0x{stored:08x})"),
368 Self::MediaTypeUnknown { media_type } =>
369 write!(f, "unknown media_type 0x{media_type:02x}"),
370 Self::SetIdentifierMismatch { segment } =>
371 write!(f, "set_identifier GUID mismatch in segment {segment} — segments may be from different acquisitions"),
372 Self::Ewf2MediaInfoMissing =>
373 write!(f, "EWF v2 media information section missing"),
374 Self::Ewf2ChunkTableChecksumMismatch { computed, stored } =>
375 write!(f, "EWF v2 chunk table checksum mismatch (computed 0x{computed:08x}, stored 0x{stored:08x})"),
376 Self::ChunkChecksumMismatch { chunk_index, computed, stored } =>
377 write!(f, "chunk {chunk_index}: Adler-32 mismatch (computed 0x{computed:08x}, stored 0x{stored:08x})"),
378 Self::ChunkDecompressionError { chunk_index } =>
379 write!(f, "chunk {chunk_index}: zlib decompression failed — chunk data is corrupt"),
380 Self::UnsupportedCompressionAlgorithm { method_id } =>
381 write!(f, "EWF v2 file header specifies unsupported compression algorithm 0x{method_id:04x} — only deflate (0/1) is supported"),
382 Self::Ewf2MediaInfoParseFailed =>
383 write!(f, "EWF v2 media information section body could not be decompressed or decoded"),
384 }
385 }
386}
387
388fn hex(bytes: &[u8]) -> String {
389 bytes.iter().map(|b| format!("{b:02x}")).collect()
390}
391
392fn le_u16(data: &[u8], off: usize) -> u16 {
400 let mut b = [0u8; 2];
401 if let Some(s) = data.get(off..off.saturating_add(2)) {
402 b.copy_from_slice(s);
403 }
404 u16::from_le_bytes(b)
405}
406
407fn le_u32(data: &[u8], off: usize) -> u32 {
408 let mut b = [0u8; 4];
409 if let Some(s) = data.get(off..off.saturating_add(4)) {
410 b.copy_from_slice(s);
411 }
412 u32::from_le_bytes(b)
413}
414
415fn le_u64(data: &[u8], off: usize) -> u64 {
416 let mut b = [0u8; 8];
417 if let Some(s) = data.get(off..off.saturating_add(8)) {
418 b.copy_from_slice(s);
419 }
420 u64::from_le_bytes(b)
421}
422
423fn array_at<const N: usize>(data: &[u8], off: usize) -> [u8; N] {
426 let mut b = [0u8; N];
427 if let Some(s) = data.get(off..off.saturating_add(N)) {
428 b.copy_from_slice(s);
429 }
430 b
431}
432
433#[derive(Debug, Clone, PartialEq, Eq)]
436#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
437pub struct AnalysisProgress {
438 pub chunks_done: usize,
440 pub chunks_total: Option<usize>,
442 pub bytes_done: u64,
444}
445
446#[derive(Debug, Clone, PartialEq, Eq)]
448#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
449pub struct ComputedHashes {
450 pub md5: [u8; 16],
451 pub sha1: [u8; 20],
452 pub sha256: [u8; 32],
453}
454
455#[derive(Debug, Clone, PartialEq, Eq)]
457#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
458pub struct EwfHeaderMetadata {
459 pub description: String,
460 pub case_number: String,
461 pub evidence_number: String,
462 pub examiner_name: String,
463 pub acquisition_date: String,
464 pub system_date: String,
465 pub password_hash: String,
466 pub acquisition_software: String,
467}
468
469pub struct EwfIntegrity<'a> {
472 segments: Vec<&'a [u8]>,
473 expected_md5: Option<[u8; 16]>,
474 expected_sha1: Option<[u8; 20]>,
475 expected_sha256: Option<[u8; 32]>,
476}
477
478impl<'a> EwfIntegrity<'a> {
479 pub fn new(data: &'a [u8]) -> Self {
481 Self {
482 segments: vec![data],
483 expected_md5: None,
484 expected_sha1: None,
485 expected_sha256: None,
486 }
487 }
488
489 pub fn from_segments(segs: &[&'a [u8]]) -> Self {
491 Self {
492 segments: segs.to_vec(),
493 expected_md5: None,
494 expected_sha1: None,
495 expected_sha256: None,
496 }
497 }
498
499 pub fn with_expected_md5(mut self, hash: [u8; 16]) -> Self {
502 self.expected_md5 = Some(hash);
503 self
504 }
505
506 pub fn with_expected_sha1(mut self, hash: [u8; 20]) -> Self {
509 self.expected_sha1 = Some(hash);
510 self
511 }
512
513 pub fn with_expected_sha256(mut self, hash: [u8; 32]) -> Self {
516 self.expected_sha256 = Some(hash);
517 self
518 }
519
520 pub fn header_metadata(&self) -> Option<EwfHeaderMetadata> {
526 for &data in &self.segments {
527 if let Some(meta) = parse_header_section(data) {
528 return Some(meta);
529 }
530 }
531 None
532 }
533
534 pub fn compute_hashes(&self) -> Option<ComputedHashes> {
539 let first = self.segments.first().copied().unwrap_or(&[]);
540 if first.len() >= 8 && (first[0..8] == EVF2_SIGNATURE || first[0..8] == LEF2_SIGNATURE) {
541 return compute_hashes_ewf2(&self.segments);
542 }
543 compute_hashes_ewf1(&self.segments)
544 }
545
546 pub fn analyse(&self) -> Vec<EwfIntegrityAnomaly> {
547 let first = self.segments.first().copied().unwrap_or(&[]);
548 if first.len() >= 8 && (first[0..8] == EVF2_SIGNATURE || first[0..8] == LEF2_SIGNATURE) {
549 return self.analyse_all_ewf2();
550 }
551 self.analyse_all_ewf1()
552 }
553
554 pub fn analyse_with_progress(
562 &self,
563 progress: impl FnMut(AnalysisProgress),
564 ) -> Vec<EwfIntegrityAnomaly> {
565 let first = self.segments.first().copied().unwrap_or(&[]);
566 if first.len() >= 8 && (first[0..8] == EVF2_SIGNATURE || first[0..8] == LEF2_SIGNATURE) {
567 return self.analyse_all_ewf2_with_progress(progress);
568 }
569 self.analyse_all_ewf1_with_progress(progress)
570 }
571
572 fn analyse_all_ewf1(&self) -> Vec<EwfIntegrityAnomaly> {
575 let mut issues = Vec::new();
576 let n = self.segments.len();
577 let multi = n > 1;
578 let mut geometry: Option<VolumeGeometry> = None;
579 let mut all_sections: Vec<Vec<Section>> = Vec::with_capacity(n);
580 let mut total_table_entries: u32 = 0;
581
582 for (idx, &data) in self.segments.iter().enumerate() {
583 let expected_seg_num = idx.saturating_add(1) as u16;
584 let is_last = idx == n.saturating_sub(1);
585 let file_size = data.len() as u64;
586
587 if data.len() < FILE_HEADER_SIZE {
588 issues.push(EwfIntegrityAnomaly::SectionChainBroken {
589 at_offset: 0,
590 next_offset: 0,
591 });
592 all_sections.push(Vec::new());
593 continue;
594 }
595
596 if data[0..8] != EVF_SIGNATURE
597 && data[0..8] != DVF_SIGNATURE
598 && data[0..8] != LVF_SIGNATURE
599 {
600 issues.push(EwfIntegrityAnomaly::InvalidSignature);
601 }
602
603 let seg_num = le_u16(data, 9);
604 if seg_num == 0 {
605 issues.push(EwfIntegrityAnomaly::SegmentNumberZero);
606 } else if seg_num != expected_seg_num {
607 issues.push(EwfIntegrityAnomaly::SegmentOutOfOrder {
608 segment_number: seg_num,
609 expected: expected_seg_num,
610 });
611 }
612
613 let sections = walk_sections_v1(data, &mut issues);
614
615 if let Some(vol_sec) = sections
617 .iter()
618 .find(|s| s.type_name == "volume" || s.type_name == "disk")
619 {
620 if idx == 0 {
621 geometry = check_volume_v1(data, vol_sec.offset, vol_sec.size, &mut issues);
622 } else {
623 let later = check_volume_v1(data, vol_sec.offset, vol_sec.size, &mut issues);
625 if let (Some(ref base), Some(ref later_geom)) = (&geometry, &later) {
626 let base_guid = base.set_identifier;
627 let later_guid = later_geom.set_identifier;
628 let neither_zero = base_guid != [0u8; 16] && later_guid != [0u8; 16];
629 if neither_zero && base_guid != later_guid {
630 issues.push(EwfIntegrityAnomaly::SetIdentifierMismatch {
631 segment: idx.saturating_add(1),
632 });
633 }
634 }
635 }
636 } else if idx == 0 {
637 issues.push(EwfIntegrityAnomaly::VolumeSectionMissing);
638 }
639
640 let vol_count = if !multi && idx == 0 {
643 geometry.as_ref().map(|g| g.chunk_count)
644 } else {
645 None
646 };
647 let sectors_section = sections.iter().find(|s| s.type_name == "sectors");
648 let sectors_range = sectors_section.map(|s| {
649 (
650 s.offset.saturating_add(SECTION_DESCRIPTOR_SIZE as u64),
651 s.offset.saturating_add(s.size),
652 )
653 });
654 if sectors_section.is_none() {
655 issues.push(EwfIntegrityAnomaly::SectorsSectionMissing);
656 }
657 if let Some(table) = sections.iter().find(|s| s.type_name == "table") {
658 let data_start = (table.offset as usize).saturating_add(SECTION_DESCRIPTOR_SIZE);
659 if data.len() >= data_start.saturating_add(4) {
660 let count = le_u32(data, data_start);
661 total_table_entries = total_table_entries.saturating_add(count);
662 }
663 check_table_v1(
664 data,
665 table.offset,
666 vol_count,
667 file_size,
668 sectors_range,
669 &mut issues,
670 );
671 } else {
672 issues.push(EwfIntegrityAnomaly::TableSectionMissing);
673 }
674
675 if let (Some(t1), Some(t2)) = (
677 sections.iter().find(|s| s.type_name == "table"),
678 sections.iter().find(|s| s.type_name == "table2"),
679 ) {
680 let b1_start = t1.offset.saturating_add(SECTION_DESCRIPTOR_SIZE as u64) as usize;
681 let b1_end = t1.offset.saturating_add(t1.size) as usize;
682 let b2_start = t2.offset.saturating_add(SECTION_DESCRIPTOR_SIZE as u64) as usize;
683 let b2_end = t2.offset.saturating_add(t2.size) as usize;
684 if let (Some(body1), Some(body2)) =
685 (data.get(b1_start..b1_end), data.get(b2_start..b2_end))
686 {
687 if body1.len() == body2.len() {
688 if let Some(offset) = body1.iter().zip(body2).position(|(a, b)| a != b) {
689 issues.push(EwfIntegrityAnomaly::Table2Mismatch { offset });
690 }
691 } else {
692 issues.push(EwfIntegrityAnomaly::Table2Mismatch { offset: 0 });
693 }
694 }
695 }
696
697 if let Some(e2) = sections.iter().find(|s| s.type_name == "error2") {
699 let body_start = e2.offset.saturating_add(SECTION_DESCRIPTOR_SIZE as u64) as usize;
700 if body_start.saturating_add(4) <= data.len() {
701 let count = le_u32(data, body_start);
702 if count > 0 {
703 issues.push(EwfIntegrityAnomaly::BadSectorsPresent { count });
704 }
705 }
706 }
707
708 if is_last && !sections.iter().any(|s| s.type_name == "done") {
710 issues.push(EwfIntegrityAnomaly::DoneSectionMissing);
711 }
712
713 all_sections.push(sections);
714 }
715
716 if multi {
718 if let Some(geom) = &geometry {
719 if total_table_entries != geom.chunk_count {
720 issues.push(EwfIntegrityAnomaly::TableChunkCountMismatch {
721 in_volume: geom.chunk_count,
722 in_table: total_table_entries,
723 });
724 }
725 }
726 }
727
728 if let Some(geom) = &geometry {
730 check_hash_all_segments(
731 &self.segments,
732 &all_sections,
733 geom,
734 self.expected_md5,
735 self.expected_sha1,
736 self.expected_sha256,
737 &mut issues,
738 &mut |_| {},
739 );
740 }
741
742 issues
743 }
744
745 fn analyse_all_ewf2(&self) -> Vec<EwfIntegrityAnomaly> {
748 self.analyse_all_ewf2_with_progress(|_| {})
749 }
750
751 fn analyse_all_ewf2_impl(
752 &self,
753 progress: &mut dyn FnMut(AnalysisProgress),
754 ) -> Vec<EwfIntegrityAnomaly> {
755 let mut issues = Vec::new();
756 let n = self.segments.len();
757
758 let mut final_stored_md5: Option<[u8; 16]> = None;
760 let mut final_stored_sha1: Option<[u8; 20]> = None;
761 let mut final_stored_sha256: Option<[u8; 32]> = None;
762
763 for (idx, &data) in self.segments.iter().enumerate() {
764 let expected_seg_num = idx.saturating_add(1) as u32;
765
766 if data.len() < EVF2_FILE_HEADER_SIZE + EVF2_SECTION_DESCRIPTOR_SIZE {
767 issues.push(EwfIntegrityAnomaly::SectionChainBroken {
768 at_offset: 0,
769 next_offset: 0,
770 });
771 continue;
772 }
773
774 if data[0..8] != EVF2_SIGNATURE && data[0..8] != LEF2_SIGNATURE {
775 issues.push(EwfIntegrityAnomaly::InvalidSignature);
776 }
777
778 let seg_num = le_u32(data, 12);
779 if seg_num == 0 {
780 issues.push(EwfIntegrityAnomaly::SegmentNumberZero);
781 } else if seg_num != expected_seg_num {
782 issues.push(EwfIntegrityAnomaly::SegmentOutOfOrder {
783 segment_number: seg_num as u16,
784 expected: expected_seg_num as u16,
785 });
786 }
787
788 let compression_method = le_u16(data, 10);
791 if compression_method > 1 {
792 issues.push(EwfIntegrityAnomaly::UnsupportedCompressionAlgorithm {
793 method_id: compression_method,
794 });
795 }
796
797 let mut has_hash = false;
800 let mut has_media_info = false;
801 let mut chunk_table_body: Option<(usize, usize)> = None;
802 let mut stored_sector_md5: Option<[u8; 16]> = None;
803 let mut stored_sector_sha1: Option<[u8; 20]> = None;
804 let mut stored_sector_sha256: Option<[u8; 32]> = None;
805 let mut desc_offset = data.len().saturating_sub(EVF2_SECTION_DESCRIPTOR_SIZE);
806
807 loop {
808 if desc_offset.saturating_add(EVF2_SECTION_DESCRIPTOR_SIZE) > data.len()
809 || desc_offset < EVF2_FILE_HEADER_SIZE
810 {
811 break;
812 }
813 let desc =
814 &data[desc_offset..desc_offset.saturating_add(EVF2_SECTION_DESCRIPTOR_SIZE)];
815 let section_type = le_u32(desc, 0);
816 let data_flags = le_u32(desc, 4);
817 let prev_offset = le_u64(desc, 8) as usize;
818 let data_size = le_u64(desc, 16) as usize;
819 let stored_hash: [u8; 16] = array_at(desc, 32);
820
821 let body_end = desc_offset;
823 let body_start = desc_offset.saturating_sub(data_size);
824
825 if data_flags & EVF2_DATA_FLAG_ENCRYPTED != 0 {
826 issues.push(EwfIntegrityAnomaly::Ewf2EncryptedSection {
827 offset: desc_offset as u64,
828 });
829 } else {
830 if stored_hash != [0u8; 16] {
831 if let Some(body) = data.get(body_start..body_end) {
832 let computed: [u8; 16] = Md5::digest(body).into();
833 if computed != stored_hash {
834 issues.push(EwfIntegrityAnomaly::Ewf2SectionDataHashMismatch {
835 offset: desc_offset as u64,
836 section_type_id: section_type,
837 computed,
838 stored: stored_hash,
839 });
840 }
841 }
842 }
843
844 match section_type {
845 EVF2_TYPE_MEDIA_INFO => {
846 has_media_info = true;
847 if let Some(body) = data.get(body_start..body_end) {
848 if !parse_media_info_body(body) {
849 issues.push(EwfIntegrityAnomaly::Ewf2MediaInfoParseFailed);
850 }
851 } else {
852 issues.push(EwfIntegrityAnomaly::Ewf2MediaInfoParseFailed);
853 }
854 }
855 EVF2_TYPE_CHUNK_TABLE => {
856 chunk_table_body = Some((body_start, body_end));
857 }
858 EVF2_TYPE_MD5_HASH => {
859 has_hash = true;
860 if data_size >= 16 {
862 if let Some(body) = data.get(body_start..body_end) {
863 let mut h = [0u8; 16];
864 h.copy_from_slice(&body[..16]);
865 stored_sector_md5 = Some(h);
866 }
867 }
868 }
869 EVF2_TYPE_SHA1_HASH => {
870 has_hash = true;
871 if data_size >= 20 {
872 if let Some(body) = data.get(body_start..body_end) {
873 let mut h = [0u8; 20];
874 h.copy_from_slice(&body[..20]);
875 stored_sector_sha1 = Some(h);
876 }
877 }
878 }
879 EVF2_TYPE_SHA256_HASH => {
880 has_hash = true;
881 if data_size >= 32 {
882 if let Some(body) = data.get(body_start..body_end) {
883 let mut h = [0u8; 32];
884 h.copy_from_slice(&body[..32]);
885 stored_sector_sha256 = Some(h);
886 }
887 }
888 }
889 _ => {}
890 }
891 }
892
893 if prev_offset == 0 || prev_offset >= desc_offset {
899 break;
900 }
901 desc_offset = prev_offset;
902 }
903
904 if idx == n.saturating_sub(1) && !has_hash {
905 issues.push(EwfIntegrityAnomaly::Ewf2HashSectionMissing);
906 }
907 if idx == 0 && !has_media_info {
908 issues.push(EwfIntegrityAnomaly::Ewf2MediaInfoMissing);
909 }
910
911 if idx == n.saturating_sub(1) {
913 final_stored_md5 = stored_sector_md5;
914 final_stored_sha1 = stored_sector_sha1;
915 final_stored_sha256 = stored_sector_sha256;
916 }
917
918 if let Some((ct_start, ct_end)) = chunk_table_body {
921 verify_ewf2_sector_data(
922 data,
923 ct_start,
924 ct_end,
925 None,
926 None,
927 None,
928 &mut issues,
929 progress,
930 );
931 }
932 }
933
934 if let Some(computed) = compute_hashes_ewf2(&self.segments) {
937 if let Some(stored) = final_stored_md5 {
938 if computed.md5 != stored {
939 issues.push(EwfIntegrityAnomaly::HashMismatch {
940 computed: computed.md5,
941 stored,
942 });
943 }
944 }
945 if let Some(stored) = final_stored_sha1 {
946 if computed.sha1 != stored {
947 issues.push(EwfIntegrityAnomaly::DigestSha1Mismatch {
948 computed: computed.sha1,
949 stored,
950 });
951 }
952 }
953 if let Some(stored) = final_stored_sha256 {
954 if computed.sha256 != stored {
955 issues.push(EwfIntegrityAnomaly::DigestSha256Mismatch {
956 computed: computed.sha256,
957 stored,
958 });
959 }
960 }
961 if let Some(expected) = self.expected_md5 {
962 if computed.md5 != expected {
963 issues.push(EwfIntegrityAnomaly::ExternalMd5Mismatch {
964 computed: computed.md5,
965 expected,
966 });
967 }
968 }
969 if let Some(expected) = self.expected_sha1 {
970 if computed.sha1 != expected {
971 issues.push(EwfIntegrityAnomaly::ExternalSha1Mismatch {
972 computed: computed.sha1,
973 expected,
974 });
975 }
976 }
977 if let Some(expected) = self.expected_sha256 {
978 if computed.sha256 != expected {
979 issues.push(EwfIntegrityAnomaly::ExternalSha256Mismatch {
980 computed: computed.sha256,
981 expected,
982 });
983 }
984 }
985 }
986
987 issues
988 }
989
990 fn analyse_all_ewf1_with_progress(
991 &self,
992 mut progress: impl FnMut(AnalysisProgress),
993 ) -> Vec<EwfIntegrityAnomaly> {
994 let mut issues = Vec::new();
995 let n = self.segments.len();
996 let multi = n > 1;
997 let mut geometry: Option<VolumeGeometry> = None;
998 let mut all_sections: Vec<Vec<Section>> = Vec::with_capacity(n);
999 let mut total_table_entries: u32 = 0;
1000
1001 for (idx, &data) in self.segments.iter().enumerate() {
1002 let expected_seg_num = idx.saturating_add(1) as u16;
1003 let is_last = idx == n.saturating_sub(1);
1004 let file_size = data.len() as u64;
1005
1006 if data.len() < FILE_HEADER_SIZE {
1007 issues.push(EwfIntegrityAnomaly::SectionChainBroken {
1008 at_offset: 0,
1009 next_offset: 0,
1010 });
1011 all_sections.push(Vec::new());
1012 continue;
1013 }
1014 if data[0..8] != EVF_SIGNATURE
1015 && data[0..8] != DVF_SIGNATURE
1016 && data[0..8] != LVF_SIGNATURE
1017 {
1018 issues.push(EwfIntegrityAnomaly::InvalidSignature);
1019 }
1020 let seg_num = le_u16(data, 9);
1021 if seg_num == 0 {
1022 issues.push(EwfIntegrityAnomaly::SegmentNumberZero);
1023 } else if seg_num != expected_seg_num {
1024 issues.push(EwfIntegrityAnomaly::SegmentOutOfOrder {
1025 segment_number: seg_num,
1026 expected: expected_seg_num,
1027 });
1028 }
1029 let sections = walk_sections_v1(data, &mut issues);
1030 if let Some(vol_sec) = sections
1031 .iter()
1032 .find(|s| s.type_name == "volume" || s.type_name == "disk")
1033 {
1034 if idx == 0 {
1035 geometry = check_volume_v1(data, vol_sec.offset, vol_sec.size, &mut issues);
1036 } else {
1037 let later = check_volume_v1(data, vol_sec.offset, vol_sec.size, &mut issues);
1038 if let (Some(ref base), Some(ref later_geom)) = (&geometry, &later) {
1039 let base_guid = base.set_identifier;
1040 let later_guid = later_geom.set_identifier;
1041 if base_guid != [0u8; 16]
1042 && later_guid != [0u8; 16]
1043 && base_guid != later_guid
1044 {
1045 issues.push(EwfIntegrityAnomaly::SetIdentifierMismatch {
1046 segment: idx.saturating_add(1),
1047 });
1048 }
1049 }
1050 }
1051 } else if idx == 0 {
1052 issues.push(EwfIntegrityAnomaly::VolumeSectionMissing);
1053 }
1054 let vol_count = if !multi && idx == 0 {
1055 geometry.as_ref().map(|g| g.chunk_count)
1056 } else {
1057 None
1058 };
1059 let sectors_section = sections.iter().find(|s| s.type_name == "sectors");
1060 let sectors_range = sectors_section.map(|s| {
1061 (
1062 s.offset.saturating_add(SECTION_DESCRIPTOR_SIZE as u64),
1063 s.offset.saturating_add(s.size),
1064 )
1065 });
1066 if sectors_section.is_none() {
1067 issues.push(EwfIntegrityAnomaly::SectorsSectionMissing);
1068 }
1069 if let Some(table) = sections.iter().find(|s| s.type_name == "table") {
1070 let data_start = (table.offset as usize).saturating_add(SECTION_DESCRIPTOR_SIZE);
1071 if data.len() >= data_start.saturating_add(4) {
1072 let count = le_u32(data, data_start);
1073 total_table_entries = total_table_entries.saturating_add(count);
1074 }
1075 check_table_v1(
1076 data,
1077 table.offset,
1078 vol_count,
1079 file_size,
1080 sectors_range,
1081 &mut issues,
1082 );
1083 } else {
1084 issues.push(EwfIntegrityAnomaly::TableSectionMissing);
1085 }
1086 if let (Some(t1), Some(t2)) = (
1087 sections.iter().find(|s| s.type_name == "table"),
1088 sections.iter().find(|s| s.type_name == "table2"),
1089 ) {
1090 let b1_start = t1.offset.saturating_add(SECTION_DESCRIPTOR_SIZE as u64) as usize;
1091 let b1_end = t1.offset.saturating_add(t1.size) as usize;
1092 let b2_start = t2.offset.saturating_add(SECTION_DESCRIPTOR_SIZE as u64) as usize;
1093 let b2_end = t2.offset.saturating_add(t2.size) as usize;
1094 if let (Some(body1), Some(body2)) =
1095 (data.get(b1_start..b1_end), data.get(b2_start..b2_end))
1096 {
1097 if body1.len() == body2.len() {
1098 if let Some(offset) = body1.iter().zip(body2).position(|(a, b)| a != b) {
1099 issues.push(EwfIntegrityAnomaly::Table2Mismatch { offset });
1100 }
1101 } else {
1102 issues.push(EwfIntegrityAnomaly::Table2Mismatch { offset: 0 });
1103 }
1104 }
1105 }
1106 if let Some(e2) = sections.iter().find(|s| s.type_name == "error2") {
1107 let body_start = e2.offset.saturating_add(SECTION_DESCRIPTOR_SIZE as u64) as usize;
1108 if body_start.saturating_add(4) <= data.len() {
1109 let count = le_u32(data, body_start);
1110 if count > 0 {
1111 issues.push(EwfIntegrityAnomaly::BadSectorsPresent { count });
1112 }
1113 }
1114 }
1115 if is_last && !sections.iter().any(|s| s.type_name == "done") {
1116 issues.push(EwfIntegrityAnomaly::DoneSectionMissing);
1117 }
1118 all_sections.push(sections);
1119 }
1120
1121 if multi {
1122 if let Some(geom) = &geometry {
1123 if total_table_entries != geom.chunk_count {
1124 issues.push(EwfIntegrityAnomaly::TableChunkCountMismatch {
1125 in_volume: geom.chunk_count,
1126 in_table: total_table_entries,
1127 });
1128 }
1129 }
1130 }
1131
1132 if let Some(geom) = &geometry {
1133 check_hash_all_segments(
1134 &self.segments,
1135 &all_sections,
1136 geom,
1137 self.expected_md5,
1138 self.expected_sha1,
1139 self.expected_sha256,
1140 &mut issues,
1141 &mut progress,
1142 );
1143 }
1144 issues
1145 }
1146
1147 fn analyse_all_ewf2_with_progress(
1148 &self,
1149 mut progress: impl FnMut(AnalysisProgress),
1150 ) -> Vec<EwfIntegrityAnomaly> {
1151 self.analyse_all_ewf2_impl(&mut progress)
1152 }
1153}
1154
1155fn parse_header_section(data: &[u8]) -> Option<EwfHeaderMetadata> {
1158 if data.len() < FILE_HEADER_SIZE + SECTION_DESCRIPTOR_SIZE {
1159 return None;
1160 }
1161 let desc_off = FILE_HEADER_SIZE;
1162 let desc = SectionDescriptor::parse(&data[desc_off..], desc_off as u64).ok()?;
1165 if desc.section_type != "header" {
1166 return None;
1167 }
1168 let section_size = desc.section_size as usize;
1169 let body_start = desc_off.saturating_add(SECTION_DESCRIPTOR_SIZE);
1170 let body_end = desc_off.saturating_add(section_size).min(data.len());
1171 if body_start >= body_end {
1172 return None;
1173 }
1174 let compressed = &data[body_start..body_end];
1175
1176 let mut decoder = ZlibDecoder::new(compressed);
1177 let mut text = String::new();
1178 decoder.read_to_string(&mut text).ok()?;
1179
1180 parse_header_text(&text)
1181}
1182
1183fn parse_header_text(text: &str) -> Option<EwfHeaderMetadata> {
1184 let lines: Vec<&str> = text
1190 .lines()
1191 .map(|l| l.trim_end_matches('\r'))
1192 .filter(|l| !l.is_empty())
1193 .collect();
1194 if lines.len() < 4 {
1195 return None;
1196 }
1197 let keys: Vec<&str> = lines[2].split('\t').collect();
1198 let vals: Vec<&str> = lines[3].split('\t').collect();
1199
1200 let mut meta = EwfHeaderMetadata {
1201 description: String::new(),
1202 case_number: String::new(),
1203 evidence_number: String::new(),
1204 examiner_name: String::new(),
1205 acquisition_date: String::new(),
1206 system_date: String::new(),
1207 password_hash: String::new(),
1208 acquisition_software: String::new(),
1209 };
1210
1211 for (i, &key) in keys.iter().enumerate() {
1212 let val = vals.get(i).copied().unwrap_or("").to_owned();
1213 match key {
1214 "a" => meta.description = val,
1215 "c" => meta.case_number = val,
1216 "e" => meta.evidence_number = val,
1217 "t" => meta.examiner_name = val,
1218 "m" => meta.acquisition_date = val,
1219 "u" => meta.system_date = val,
1220 "p" => meta.password_hash = val,
1221 "r" => meta.acquisition_software = val,
1222 _ => {}
1223 }
1224 }
1225
1226 Some(meta)
1227}
1228
1229struct Section {
1230 type_name: String,
1231 offset: u64,
1232 size: u64,
1233}
1234
1235struct VolumeGeometry {
1236 chunk_count: u32,
1237 sectors_per_chunk: u32,
1238 bytes_per_sector: u32,
1239 sector_count: u64,
1240 set_identifier: [u8; 16],
1242}
1243
1244fn walk_sections_v1(data: &[u8], issues: &mut Vec<EwfIntegrityAnomaly>) -> Vec<Section> {
1245 let file_size = data.len() as u64;
1246 let mut sections = Vec::new();
1247 let mut pos = FILE_HEADER_SIZE as u64;
1248
1249 loop {
1250 let off = pos as usize;
1251 if off.saturating_add(SECTION_DESCRIPTOR_SIZE) > data.len() {
1252 break;
1253 }
1254 let raw = &data[off..off.saturating_add(SECTION_DESCRIPTOR_SIZE)];
1255
1256 let Ok(desc) = SectionDescriptor::parse(raw, pos) else {
1259 break;
1260 };
1261 let crc_ok = desc.verify_crc(raw);
1262 let stored_crc = desc.stored_crc;
1263 let next = desc.next;
1264 let section_size = desc.section_size;
1265 let type_name = desc.section_type;
1266
1267 if !crc_ok {
1268 issues.push(EwfIntegrityAnomaly::SectionDescriptorCrcMismatch {
1269 offset: pos,
1270 section_type: type_name.clone(),
1271 computed: adler32(&raw[SectionDescriptor::crc_covers()]),
1272 stored: stored_crc,
1273 });
1274 }
1275
1276 if !KNOWN_TYPES.contains(&type_name.as_str()) {
1277 issues.push(EwfIntegrityAnomaly::UnknownSectionType {
1278 offset: pos,
1279 type_name: type_name.clone(),
1280 });
1281 }
1282
1283 let section_end = pos.saturating_add(section_size);
1284
1285 sections.push(Section {
1286 type_name: type_name.clone(),
1287 offset: pos,
1288 size: section_size,
1289 });
1290
1291 if type_name == "done" || type_name == "next" {
1293 break;
1294 }
1295
1296 if next == 0 || next > file_size || next <= pos {
1297 issues.push(EwfIntegrityAnomaly::SectionChainBroken {
1298 at_offset: pos,
1299 next_offset: next,
1300 });
1301 break;
1302 }
1303
1304 if next > section_end {
1305 let gap_offset = section_end;
1306 let gap_size = next.saturating_sub(section_end);
1307 let non_zero = data
1308 .get(section_end as usize..next as usize)
1309 .is_some_and(|s| s.iter().any(|&b| b != 0));
1310 if non_zero {
1311 issues.push(EwfIntegrityAnomaly::SectionGapNonZero {
1312 gap_offset,
1313 gap_size,
1314 });
1315 } else {
1316 issues.push(EwfIntegrityAnomaly::SectionGapZero {
1317 gap_offset,
1318 gap_size,
1319 });
1320 }
1321 }
1322
1323 pos = next;
1324 }
1325
1326 sections
1327}
1328
1329fn check_volume_v1(
1330 data: &[u8],
1331 desc_offset: u64,
1332 section_size: u64,
1333 issues: &mut Vec<EwfIntegrityAnomaly>,
1334) -> Option<VolumeGeometry> {
1335 let data_start = (desc_offset as usize).saturating_add(SECTION_DESCRIPTOR_SIZE);
1336 if data.len() < data_start.saturating_add(VOLUME_DATA_MIN) {
1337 return None;
1338 }
1339 let body_len = (section_size as usize).saturating_sub(SECTION_DESCRIPTOR_SIZE);
1340 let vol_end = data_start.saturating_add(body_len).min(data.len());
1341 let vol = &data[data_start..vol_end];
1342
1343 let parsed = EwfVolume::parse(vol).ok()?;
1347 let chunk_count = parsed.chunk_count;
1348 let sectors_per_chunk = parsed.sectors_per_chunk;
1349 let bytes_per_sector = parsed.bytes_per_sector;
1350 let sector_count = parsed.sector_count;
1351
1352 if !VALID_MEDIA_TYPES.contains(&parsed.media_type) {
1354 issues.push(EwfIntegrityAnomaly::MediaTypeUnknown {
1355 media_type: parsed.media_type,
1356 });
1357 }
1358
1359 if bytes_per_sector != 512 && bytes_per_sector != 4096 {
1360 issues.push(EwfIntegrityAnomaly::BytesPerSectorInvalid { bytes_per_sector });
1361 }
1362 if sectors_per_chunk == 0 || !sectors_per_chunk.is_power_of_two() {
1363 issues.push(EwfIntegrityAnomaly::ChunkSizeInvalid {
1364 sectors_per_chunk,
1365 bytes_per_sector,
1366 });
1367 }
1368
1369 let max_sectors = u64::from(chunk_count).saturating_mul(u64::from(sectors_per_chunk));
1370 let min_sectors = max_sectors.saturating_sub(u64::from(sectors_per_chunk));
1371 if sectors_per_chunk.is_power_of_two() {
1372 let out_of_range =
1373 sector_count > max_sectors || (chunk_count > 0 && sector_count <= min_sectors);
1374 if out_of_range {
1375 issues.push(EwfIntegrityAnomaly::SectorCountMismatch {
1376 declared: sector_count,
1377 expected: max_sectors,
1378 });
1379 }
1380 }
1381
1382 let set_identifier = parsed.set_identifier;
1383
1384 if parsed.verify_crc(vol) == Some(false) {
1387 issues.push(EwfIntegrityAnomaly::VolumeBodyCrcMismatch {
1388 computed: adler32(&vol[EwfVolume::crc_covers()]),
1389 stored: parsed.stored_crc.unwrap_or(0),
1390 });
1391 }
1392
1393 Some(VolumeGeometry {
1394 chunk_count,
1395 sectors_per_chunk,
1396 bytes_per_sector,
1397 sector_count,
1398 set_identifier,
1399 })
1400}
1401
1402fn check_table_v1(
1403 data: &[u8],
1404 desc_offset: u64,
1405 volume_chunk_count: Option<u32>,
1406 file_size: u64,
1407 sectors_range: Option<(u64, u64)>,
1408 issues: &mut Vec<EwfIntegrityAnomaly>,
1409) {
1410 let data_start = (desc_offset as usize).saturating_add(SECTION_DESCRIPTOR_SIZE);
1411 if data.len() < data_start.saturating_add(TABLE_HEADER_SIZE) {
1412 return;
1413 }
1414 let tbl = &data[data_start..];
1415
1416 let Ok(header) = TableHeader::parse(tbl) else {
1419 return;
1420 };
1421 let entry_count = header.entry_count;
1422 let base_offset = header.base_offset;
1423
1424 if header.verify_crc(tbl) == Some(false) {
1427 issues.push(EwfIntegrityAnomaly::TableHeaderAdler32Mismatch {
1428 computed: adler32(&tbl[TableHeader::crc_covers()]),
1429 stored: header.stored_crc,
1430 });
1431 }
1432
1433 if let Some(vol_count) = volume_chunk_count {
1434 if entry_count != vol_count {
1435 issues.push(EwfIntegrityAnomaly::TableChunkCountMismatch {
1436 in_volume: vol_count,
1437 in_table: entry_count,
1438 });
1439 }
1440 }
1441
1442 let entries_start = data_start.saturating_add(TABLE_HEADER_SIZE);
1443 for i in 0..entry_count {
1444 let entry_off = entries_start.saturating_add((i as usize).saturating_mul(4));
1445 let Some(entry_bytes) = data.get(entry_off..entry_off.saturating_add(4)) else {
1446 break;
1447 };
1448 let Ok(entry) = sections::TableEntry::parse(entry_bytes) else {
1450 break;
1451 };
1452 let chunk_rel = u64::from(entry.chunk_offset);
1453 let absolute = base_offset.saturating_add(chunk_rel);
1454 if absolute >= file_size {
1455 issues.push(EwfIntegrityAnomaly::TableEntryOutOfBounds {
1456 chunk_index: i,
1457 entry_offset: absolute,
1458 file_size,
1459 });
1460 } else if let Some((sec_start, sec_end)) = sectors_range {
1461 if absolute < sec_start || absolute >= sec_end {
1462 issues.push(EwfIntegrityAnomaly::TableEntryOutsideSectorsRange {
1463 chunk_index: i,
1464 entry_offset: absolute,
1465 sectors_start: sec_start,
1466 sectors_end: sec_end,
1467 });
1468 }
1469 }
1470 }
1471}
1472
1473fn iter_segment_chunks(data: &[u8], sections: &[Section]) -> Vec<(usize, usize, bool)> {
1475 let table = match sections.iter().find(|s| s.type_name == "table") {
1476 Some(s) => s,
1477 None => return Vec::new(),
1478 };
1479 let sectors = match sections.iter().find(|s| s.type_name == "sectors") {
1480 Some(s) => s,
1481 None => return Vec::new(),
1482 };
1483
1484 let tbl_data_start = (table.offset as usize).saturating_add(SECTION_DESCRIPTOR_SIZE);
1485 if data.len() < tbl_data_start.saturating_add(TABLE_HEADER_SIZE) {
1486 return Vec::new();
1487 }
1488 let tbl = &data[tbl_data_start..];
1489 let Ok(header) = TableHeader::parse(tbl) else {
1491 return Vec::new();
1492 };
1493 let entry_count = header.entry_count as usize;
1494 let base_offset = header.base_offset as usize;
1495 let entries_start = tbl_data_start.saturating_add(TABLE_HEADER_SIZE);
1496 let sectors_body_end = sectors.offset.saturating_add(sectors.size) as usize;
1497
1498 let entry_at = |idx: usize| -> Option<(bool, usize)> {
1501 let off = entries_start.saturating_add(idx.saturating_mul(4));
1502 let bytes = data.get(off..off.saturating_add(4))?;
1503 let e = sections::TableEntry::parse(bytes).ok()?;
1504 Some((e.compressed, e.chunk_offset as usize))
1505 };
1506
1507 let mut chunks = Vec::with_capacity(entry_count.min(data.len()));
1511 for i in 0..entry_count {
1512 let Some((compressed, rel)) = entry_at(i) else {
1513 break;
1514 };
1515 let start = base_offset.saturating_add(rel);
1516
1517 let end = if i.saturating_add(1) < entry_count {
1518 let Some((_, next_rel)) = entry_at(i.saturating_add(1)) else {
1519 break;
1520 };
1521 base_offset.saturating_add(next_rel)
1522 } else {
1523 sectors_body_end.min(data.len())
1524 };
1525
1526 if start >= end || end > data.len() {
1527 break;
1528 }
1529 chunks.push((start, end, compressed));
1530 }
1531 chunks
1532}
1533
1534fn check_hash_all_segments(
1536 segments: &[&[u8]],
1537 all_sections: &[Vec<Section>],
1538 geom: &VolumeGeometry,
1539 expected_md5: Option<[u8; 16]>,
1540 expected_sha1: Option<[u8; 20]>,
1541 expected_sha256: Option<[u8; 32]>,
1542 issues: &mut Vec<EwfIntegrityAnomaly>,
1543 progress: &mut dyn FnMut(AnalysisProgress),
1544) {
1545 let chunk_size =
1546 u64::from(geom.sectors_per_chunk).saturating_mul(u64::from(geom.bytes_per_sector));
1547 let total_bytes = geom
1548 .sector_count
1549 .saturating_mul(u64::from(geom.bytes_per_sector));
1550 let mut bytes_remaining = total_bytes;
1551
1552 let mut md5_h = Md5::new();
1553 let mut sha1_h = Sha1::new();
1554 let mut sha256_h = Sha256::new();
1555
1556 let chunk_size_usize = chunk_size as usize;
1557 let mut global_chunk_idx: usize = 0;
1558
1559 'outer: for (&seg_data, sections) in segments.iter().zip(all_sections.iter()) {
1560 for (start, end, compressed) in iter_segment_chunks(seg_data, sections) {
1561 if bytes_remaining == 0 {
1562 break 'outer;
1563 }
1564 let to_hash = bytes_remaining.min(chunk_size) as usize;
1565 let raw = &seg_data[start..end];
1566
1567 let this_chunk_idx = global_chunk_idx;
1577 global_chunk_idx = global_chunk_idx.saturating_add(1);
1578
1579 let has_uncompressed_checksum = !compressed && (raw.len() > chunk_size_usize);
1580 if has_uncompressed_checksum && raw.len() >= chunk_size_usize.saturating_add(4) {
1581 let crc_end = chunk_size_usize;
1582 let stored = le_u32(raw, crc_end);
1583 let computed = adler32(&raw[..crc_end]);
1584 if computed != stored {
1585 issues.push(EwfIntegrityAnomaly::ChunkChecksumMismatch {
1586 chunk_index: this_chunk_idx,
1587 computed,
1588 stored,
1589 });
1590 }
1591 }
1592
1593 if compressed {
1594 let limit = (to_hash as u64).saturating_add(1);
1595 let mut decompressed = Vec::with_capacity(to_hash.min(raw.len()));
1600 if ZlibDecoder::new(raw)
1601 .take(limit)
1602 .read_to_end(&mut decompressed)
1603 .is_err()
1604 {
1605 issues.push(EwfIntegrityAnomaly::ChunkDecompressionError {
1606 chunk_index: this_chunk_idx,
1607 });
1608 bytes_remaining = bytes_remaining.saturating_sub(to_hash as u64);
1609 continue;
1610 }
1611 let slice = &decompressed[..decompressed.len().min(to_hash)];
1612 md5_h.update(slice);
1613 sha1_h.update(slice);
1614 sha256_h.update(slice);
1615 } else {
1616 let slice = &raw[..raw.len().min(to_hash)];
1619 md5_h.update(slice);
1620 sha1_h.update(slice);
1621 sha256_h.update(slice);
1622 }
1623 bytes_remaining = bytes_remaining.saturating_sub(to_hash as u64);
1624 progress(AnalysisProgress {
1625 chunks_done: global_chunk_idx,
1626 chunks_total: None,
1627 bytes_done: total_bytes.saturating_sub(bytes_remaining),
1628 });
1629 }
1630 }
1631
1632 let computed_md5: [u8; 16] = md5_h.finalize().into();
1633 let computed_sha1: [u8; 20] = sha1_h.finalize().into();
1634 let computed_sha256: [u8; 32] = sha256_h.finalize().into();
1635
1636 let last_sections = match all_sections.last() {
1637 Some(s) => s,
1638 None => return,
1639 };
1640 let last_data = match segments.last() {
1641 Some(d) => d,
1642 None => return,
1643 };
1644
1645 match last_sections.iter().find(|s| s.type_name == "hash") {
1647 Some(hash_sec) => {
1648 let body_start = (hash_sec.offset as usize).saturating_add(SECTION_DESCRIPTOR_SIZE);
1649 if let Some(stored_slice) = last_data.get(body_start..body_start.saturating_add(16)) {
1650 let stored: [u8; 16] = stored_slice.try_into().unwrap_or([0u8; 16]);
1651 if computed_md5 != stored {
1652 issues.push(EwfIntegrityAnomaly::HashMismatch {
1653 computed: computed_md5,
1654 stored,
1655 });
1656 }
1657 }
1658 }
1659 None => issues.push(EwfIntegrityAnomaly::HashSectionMissing),
1660 }
1661
1662 if let Some(digest_sec) = last_sections.iter().find(|s| s.type_name == "digest") {
1664 let body_start = (digest_sec.offset as usize).saturating_add(SECTION_DESCRIPTOR_SIZE);
1665 if let Some(sha1_slice) =
1666 last_data.get(body_start.saturating_add(16)..body_start.saturating_add(36))
1667 {
1668 let stored: [u8; 20] = sha1_slice.try_into().unwrap_or([0u8; 20]);
1669 if stored != [0u8; 20] && computed_sha1 != stored {
1671 issues.push(EwfIntegrityAnomaly::DigestSha1Mismatch {
1672 computed: computed_sha1,
1673 stored,
1674 });
1675 }
1676 }
1677 }
1678
1679 if let Some(expected) = expected_md5 {
1681 if computed_md5 != expected {
1682 issues.push(EwfIntegrityAnomaly::ExternalMd5Mismatch {
1683 computed: computed_md5,
1684 expected,
1685 });
1686 }
1687 }
1688 if let Some(expected) = expected_sha1 {
1689 if computed_sha1 != expected {
1690 issues.push(EwfIntegrityAnomaly::ExternalSha1Mismatch {
1691 computed: computed_sha1,
1692 expected,
1693 });
1694 }
1695 }
1696 if let Some(expected) = expected_sha256 {
1697 if computed_sha256 != expected {
1698 issues.push(EwfIntegrityAnomaly::ExternalSha256Mismatch {
1699 computed: computed_sha256,
1700 expected,
1701 });
1702 }
1703 }
1704}
1705
1706fn parse_media_info_body(body: &[u8]) -> bool {
1716 if body.is_empty() {
1717 return false;
1718 }
1719 let mut decompressed = Vec::new();
1720 if ZlibDecoder::new(body)
1721 .read_to_end(&mut decompressed)
1722 .is_err()
1723 {
1724 return false;
1725 }
1726 let text_bytes = if decompressed.starts_with(&[0xFF, 0xFE]) {
1728 &decompressed[2..]
1729 } else {
1730 &decompressed[..]
1731 };
1732 if text_bytes.len() % 2 != 0 {
1734 return false;
1735 }
1736 let units: Vec<u16> = text_bytes
1737 .chunks_exact(2)
1738 .map(|b| u16::from_le_bytes([b[0], b[1]]))
1739 .collect();
1740 String::from_utf16(&units).is_ok()
1741}
1742
1743fn verify_ewf2_sector_data(
1747 data: &[u8],
1748 ct_start: usize,
1749 ct_end: usize,
1750 stored_md5: Option<[u8; 16]>,
1751 stored_sha1: Option<[u8; 20]>,
1752 stored_sha256: Option<[u8; 32]>,
1753 issues: &mut Vec<EwfIntegrityAnomaly>,
1754 progress: &mut dyn FnMut(AnalysisProgress),
1755) -> Option<ComputedHashes> {
1756 let tbl = data.get(ct_start..ct_end)?;
1757 if tbl.len() < EVF2_CHUNK_TABLE_HEADER_SIZE + EVF2_CHUNK_TABLE_ENTRY_SIZE {
1758 return None;
1759 }
1760 let chunk_count = le_u64(tbl, 8) as usize;
1761
1762 let checksum_off = EVF2_CHUNK_TABLE_HEADER_SIZE
1764 .saturating_add(chunk_count.saturating_mul(EVF2_CHUNK_TABLE_ENTRY_SIZE));
1765 if checksum_off.saturating_add(4) <= tbl.len() {
1766 let computed_cs = adler32(&tbl[EVF2_CHUNK_TABLE_HEADER_SIZE..checksum_off]);
1767 let stored_cs = le_u32(tbl, checksum_off);
1768 if computed_cs != stored_cs {
1769 issues.push(EwfIntegrityAnomaly::Ewf2ChunkTableChecksumMismatch {
1770 computed: computed_cs,
1771 stored: stored_cs,
1772 });
1773 }
1774 }
1775
1776 let mut md5_h = Md5::new();
1777 let mut sha1_h = Sha1::new();
1778 let mut sha256_h = Sha256::new();
1779
1780 for i in 0..chunk_count {
1781 let entry_off = EVF2_CHUNK_TABLE_HEADER_SIZE
1782 .saturating_add(i.saturating_mul(EVF2_CHUNK_TABLE_ENTRY_SIZE));
1783 if entry_off.saturating_add(EVF2_CHUNK_TABLE_ENTRY_SIZE) > tbl.len() {
1784 break;
1785 }
1786 let file_offset = le_u64(tbl, entry_off) as usize;
1787 let chunk_data_size = le_u32(tbl, entry_off.saturating_add(8)) as usize;
1788 let flags = le_u32(tbl, entry_off.saturating_add(12));
1789
1790 let raw_size = chunk_data_size.saturating_sub(4);
1792 let chunk_raw = match data.get(file_offset..file_offset.saturating_add(raw_size)) {
1793 Some(r) => r,
1794 None => break,
1795 };
1796
1797 if chunk_data_size >= 4 {
1799 let crc_start = file_offset.saturating_add(raw_size);
1800 if let Some(crc_bytes) = data.get(crc_start..crc_start.saturating_add(4)) {
1801 let stored_crc = u32::from_le_bytes(crc_bytes.try_into().unwrap_or([0u8; 4]));
1802 let computed_crc = adler32(chunk_raw);
1803 if computed_crc != stored_crc {
1804 issues.push(EwfIntegrityAnomaly::ChunkChecksumMismatch {
1805 chunk_index: i,
1806 computed: computed_crc,
1807 stored: stored_crc,
1808 });
1809 }
1810 }
1811 }
1812
1813 if flags & EVF2_CHUNK_FLAG_COMPRESSED != 0 {
1814 let mut decompressed = Vec::with_capacity(raw_size.min(chunk_raw.len()));
1817 if ZlibDecoder::new(chunk_raw)
1818 .read_to_end(&mut decompressed)
1819 .is_err()
1820 {
1821 issues.push(EwfIntegrityAnomaly::ChunkDecompressionError { chunk_index: i });
1822 continue;
1823 }
1824 md5_h.update(&decompressed);
1825 sha1_h.update(&decompressed);
1826 sha256_h.update(&decompressed);
1827 } else {
1828 md5_h.update(chunk_raw);
1829 sha1_h.update(chunk_raw);
1830 sha256_h.update(chunk_raw);
1831 }
1832 progress(AnalysisProgress {
1833 chunks_done: i.saturating_add(1),
1834 chunks_total: Some(chunk_count),
1835 bytes_done: i.saturating_add(1).saturating_mul(raw_size) as u64,
1836 });
1837 }
1838
1839 let computed_md5: [u8; 16] = md5_h.finalize().into();
1840 let computed_sha1: [u8; 20] = sha1_h.finalize().into();
1841 let computed_sha256: [u8; 32] = sha256_h.finalize().into();
1842
1843 if let Some(stored) = stored_md5 {
1844 if computed_md5 != stored {
1845 issues.push(EwfIntegrityAnomaly::HashMismatch {
1846 computed: computed_md5,
1847 stored,
1848 });
1849 }
1850 }
1851
1852 if let Some(stored) = stored_sha1 {
1853 if computed_sha1 != stored {
1854 issues.push(EwfIntegrityAnomaly::DigestSha1Mismatch {
1855 computed: computed_sha1,
1856 stored,
1857 });
1858 }
1859 }
1860
1861 if let Some(stored) = stored_sha256 {
1862 if computed_sha256 != stored {
1863 issues.push(EwfIntegrityAnomaly::DigestSha256Mismatch {
1864 computed: computed_sha256,
1865 stored,
1866 });
1867 }
1868 }
1869
1870 Some(ComputedHashes {
1871 md5: computed_md5,
1872 sha1: computed_sha1,
1873 sha256: computed_sha256,
1874 })
1875}
1876
1877fn compute_hashes_ewf2(segments: &[&[u8]]) -> Option<ComputedHashes> {
1879 let mut md5_h = Md5::new();
1880 let mut sha1_h = Sha1::new();
1881 let mut sha256_h = Sha256::new();
1882 let mut found_chunks = false;
1883
1884 for &data in segments {
1885 if data.len() < EVF2_FILE_HEADER_SIZE + EVF2_SECTION_DESCRIPTOR_SIZE {
1886 continue;
1887 }
1888
1889 let mut desc_offset = data.len().saturating_sub(EVF2_SECTION_DESCRIPTOR_SIZE);
1891 let mut chunk_table_body: Option<(usize, usize)> = None;
1892
1893 loop {
1894 if desc_offset.saturating_add(EVF2_SECTION_DESCRIPTOR_SIZE) > data.len()
1895 || desc_offset < EVF2_FILE_HEADER_SIZE
1896 {
1897 break;
1898 }
1899 let desc = &data[desc_offset..desc_offset.saturating_add(EVF2_SECTION_DESCRIPTOR_SIZE)];
1900 let section_type = le_u32(desc, 0);
1901 let data_flags = le_u32(desc, 4);
1902 let prev_offset = le_u64(desc, 8) as usize;
1903 let data_size = le_u64(desc, 16) as usize;
1904 let body_end = desc_offset;
1905 let body_start = desc_offset.saturating_sub(data_size);
1906
1907 if data_flags & EVF2_DATA_FLAG_ENCRYPTED == 0 && section_type == EVF2_TYPE_CHUNK_TABLE {
1908 chunk_table_body = Some((body_start, body_end));
1909 }
1910
1911 if prev_offset == 0 || prev_offset >= desc_offset {
1914 break;
1915 }
1916 desc_offset = prev_offset;
1917 }
1918
1919 let (ct_start, ct_end) = match chunk_table_body {
1920 Some(b) => b,
1921 None => continue,
1922 };
1923 let tbl = match data.get(ct_start..ct_end) {
1924 Some(t) => t,
1925 None => continue,
1926 };
1927 if tbl.len() < EVF2_CHUNK_TABLE_HEADER_SIZE + EVF2_CHUNK_TABLE_ENTRY_SIZE {
1928 continue;
1929 }
1930 let chunk_count = le_u64(tbl, 8) as usize;
1931
1932 for i in 0..chunk_count {
1933 let entry_off = EVF2_CHUNK_TABLE_HEADER_SIZE
1934 .saturating_add(i.saturating_mul(EVF2_CHUNK_TABLE_ENTRY_SIZE));
1935 if entry_off.saturating_add(EVF2_CHUNK_TABLE_ENTRY_SIZE) > tbl.len() {
1936 break;
1937 }
1938 let file_offset = le_u64(tbl, entry_off) as usize;
1939 let chunk_data_size = le_u32(tbl, entry_off.saturating_add(8)) as usize;
1940 let flags = le_u32(tbl, entry_off.saturating_add(12));
1941 let raw_size = chunk_data_size.saturating_sub(4);
1942 let chunk_raw = match data.get(file_offset..file_offset.saturating_add(raw_size)) {
1943 Some(r) => r,
1944 None => break,
1945 };
1946
1947 if flags & EVF2_CHUNK_FLAG_COMPRESSED != 0 {
1948 let mut decompressed = Vec::with_capacity(raw_size.min(chunk_raw.len()));
1950 if ZlibDecoder::new(chunk_raw)
1951 .read_to_end(&mut decompressed)
1952 .is_err()
1953 {
1954 continue;
1955 }
1956 md5_h.update(&decompressed);
1957 sha1_h.update(&decompressed);
1958 sha256_h.update(&decompressed);
1959 } else {
1960 md5_h.update(chunk_raw);
1961 sha1_h.update(chunk_raw);
1962 sha256_h.update(chunk_raw);
1963 }
1964 found_chunks = true;
1965 }
1966 }
1967
1968 if !found_chunks {
1969 return None;
1970 }
1971 Some(ComputedHashes {
1972 md5: md5_h.finalize().into(),
1973 sha1: sha1_h.finalize().into(),
1974 sha256: sha256_h.finalize().into(),
1975 })
1976}
1977
1978fn compute_hashes_ewf1(segments: &[&[u8]]) -> Option<ComputedHashes> {
1981 let first = segments.first().copied()?;
1982 if first.len() < FILE_HEADER_SIZE {
1983 return None;
1984 }
1985 if first[0..8] != EVF_SIGNATURE && first[0..8] != DVF_SIGNATURE && first[0..8] != LVF_SIGNATURE
1986 {
1987 return None;
1988 }
1989
1990 let mut dummy = Vec::new();
1991 let sections_first = walk_sections_v1(first, &mut dummy);
1992 let vol_sec = sections_first
1993 .iter()
1994 .find(|s| s.type_name == "volume" || s.type_name == "disk")?;
1995 let geom = check_volume_v1(first, vol_sec.offset, vol_sec.size, &mut dummy)?;
1996
1997 let chunk_size =
1998 u64::from(geom.sectors_per_chunk).saturating_mul(u64::from(geom.bytes_per_sector));
1999 let total_bytes = geom
2000 .sector_count
2001 .saturating_mul(u64::from(geom.bytes_per_sector));
2002 let mut bytes_remaining = total_bytes;
2003
2004 let mut md5_h = Md5::new();
2005 let mut sha1_h = Sha1::new();
2006 let mut sha256_h = Sha256::new();
2007
2008 let mut all_sections: Vec<Vec<Section>> = Vec::new();
2009 for &seg in segments {
2010 let mut d = Vec::new();
2011 all_sections.push(walk_sections_v1(seg, &mut d));
2012 }
2013
2014 'outer: for (&seg_data, sections) in segments.iter().zip(all_sections.iter()) {
2015 for (start, end, compressed) in iter_segment_chunks(seg_data, sections) {
2016 if bytes_remaining == 0 {
2017 break 'outer;
2018 }
2019 let to_hash = bytes_remaining.min(chunk_size) as usize;
2020 let raw = &seg_data[start..end];
2021
2022 if compressed {
2023 let limit = (to_hash as u64).saturating_add(1);
2024 let mut decompressed = Vec::with_capacity(to_hash.min(raw.len()));
2027 if ZlibDecoder::new(raw)
2028 .take(limit)
2029 .read_to_end(&mut decompressed)
2030 .is_err()
2031 {
2032 bytes_remaining = bytes_remaining.saturating_sub(to_hash as u64);
2033 continue;
2034 }
2035 let slice = &decompressed[..decompressed.len().min(to_hash)];
2036 md5_h.update(slice);
2037 sha1_h.update(slice);
2038 sha256_h.update(slice);
2039 } else {
2040 let slice = &raw[..raw.len().min(to_hash)];
2041 md5_h.update(slice);
2042 sha1_h.update(slice);
2043 sha256_h.update(slice);
2044 }
2045 bytes_remaining = bytes_remaining.saturating_sub(to_hash as u64);
2046 }
2047 }
2048
2049 Some(ComputedHashes {
2050 md5: md5_h.finalize().into(),
2051 sha1: sha1_h.finalize().into(),
2052 sha256: sha256_h.finalize().into(),
2053 })
2054}
2055
2056pub(crate) fn adler32(data: &[u8]) -> u32 {
2057 sections::adler32(data)
2061}
2062
2063impl EwfIntegrityAnomaly {
2064 #[must_use]
2066 pub fn code(&self) -> &'static str {
2067 match self {
2068 Self::InvalidSignature => "EWF-INVALID-SIGNATURE",
2069 Self::SegmentNumberZero => "EWF-SEGMENT-NUMBER-ZERO",
2070 Self::SectionDescriptorCrcMismatch { .. } => "EWF-SECTION-DESCRIPTOR-CRC-MISMATCH",
2071 Self::SectionChainBroken { .. } => "EWF-SECTION-CHAIN-BROKEN",
2072 Self::SectionGapNonZero { .. } => "EWF-SECTION-GAP-NON-ZERO",
2073 Self::VolumeSectionMissing => "EWF-VOLUME-SECTION-MISSING",
2074 Self::UnknownSectionType { .. } => "EWF-UNKNOWN-SECTION-TYPE",
2075 Self::DoneSectionMissing => "EWF-DONE-SECTION-MISSING",
2076 Self::SectorsSectionMissing => "EWF-SECTORS-SECTION-MISSING",
2077 Self::TableSectionMissing => "EWF-TABLE-SECTION-MISSING",
2078 Self::ChunkSizeInvalid { .. } => "EWF-CHUNK-SIZE-INVALID",
2079 Self::SectorCountMismatch { .. } => "EWF-SECTOR-COUNT-MISMATCH",
2080 Self::BytesPerSectorInvalid { .. } => "EWF-BYTES-PER-SECTOR-INVALID",
2081 Self::TableChunkCountMismatch { .. } => "EWF-TABLE-CHUNK-COUNT-MISMATCH",
2082 Self::TableHeaderAdler32Mismatch { .. } => "EWF-TABLE-HEADER-ADLER32-MISMATCH",
2083 Self::TableEntryOutOfBounds { .. } => "EWF-TABLE-ENTRY-OUT-OF-BOUNDS",
2084 Self::TableEntryOutsideSectorsRange { .. } => "EWF-TABLE-ENTRY-OUTSIDE-SECTORS-RANGE",
2085 Self::SectionGapZero { .. } => "EWF-SECTION-GAP-ZERO",
2086 Self::HashMismatch { .. } => "EWF-HASH-MISMATCH",
2087 Self::HashSectionMissing => "EWF-HASH-SECTION-MISSING",
2088 Self::Table2Mismatch { .. } => "EWF-TABLE2-MISMATCH",
2089 Self::BadSectorsPresent { .. } => "EWF-BAD-SECTORS-PRESENT",
2090 Self::SegmentOutOfOrder { .. } => "EWF-SEGMENT-OUT-OF-ORDER",
2091 Self::DigestSha1Mismatch { .. } => "EWF-DIGEST-SHA1-MISMATCH",
2092 Self::DigestSha256Mismatch { .. } => "EWF-DIGEST-SHA256-MISMATCH",
2093 Self::ExternalMd5Mismatch { .. } => "EWF-EXTERNAL-MD5-MISMATCH",
2094 Self::ExternalSha1Mismatch { .. } => "EWF-EXTERNAL-SHA1-MISMATCH",
2095 Self::Ewf2SectionDataHashMismatch { .. } => "EWF-EWF2-SECTION-DATA-HASH-MISMATCH",
2096 Self::Ewf2EncryptedSection { .. } => "EWF-EWF2-ENCRYPTED-SECTION",
2097 Self::Ewf2HashSectionMissing => "EWF-EWF2-HASH-SECTION-MISSING",
2098 Self::VolumeBodyCrcMismatch { .. } => "EWF-VOLUME-BODY-CRC-MISMATCH",
2099 Self::MediaTypeUnknown { .. } => "EWF-MEDIA-TYPE-UNKNOWN",
2100 Self::SetIdentifierMismatch { .. } => "EWF-SET-IDENTIFIER-MISMATCH",
2101 Self::Ewf2MediaInfoMissing => "EWF-EWF2-MEDIA-INFO-MISSING",
2102 Self::Ewf2ChunkTableChecksumMismatch { .. } => "EWF-EWF2-CHUNK-TABLE-CHECKSUM-MISMATCH",
2103 Self::ChunkChecksumMismatch { .. } => "EWF-CHUNK-CHECKSUM-MISMATCH",
2104 Self::ChunkDecompressionError { .. } => "EWF-CHUNK-DECOMPRESSION-ERROR",
2105 Self::UnsupportedCompressionAlgorithm { .. } => "EWF-UNSUPPORTED-COMPRESSION-ALGORITHM",
2106 Self::ExternalSha256Mismatch { .. } => "EWF-EXTERNAL-SHA256-MISMATCH",
2107 Self::Ewf2MediaInfoParseFailed => "EWF-EWF2-MEDIA-INFO-PARSE-FAILED",
2108 }
2109 }
2110}
2111
2112impl forensicnomicon::report::Observation for EwfIntegrityAnomaly {
2113 fn severity(&self) -> Option<Severity> {
2114 Some(self.severity())
2115 }
2116 fn code(&self) -> &'static str {
2117 self.code()
2118 }
2119 fn note(&self) -> String {
2120 self.to_string()
2121 }
2122}
2123
2124#[cfg(test)]
2125mod adler32_tests {
2126 use super::adler32;
2127
2128 #[test]
2133 fn adler32_matches_published_vectors() {
2134 assert_eq!(adler32(b""), 0x0000_0001);
2135 assert_eq!(adler32(b"abc"), 0x024D_0127);
2136 assert_eq!(adler32(b"Wikipedia"), 0x11E6_0398);
2137 }
2138}