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