1use std::fs::File;
35use std::io::{self, Read, Write};
36use std::path::{Path, PathBuf};
37
38use ewf::sections::{
39 adler32, EwfVolume, SectionDescriptor, TableEntry, TableHeader, EVF_SIGNATURE,
40 FILE_HEADER_SIZE, SECTION_DESCRIPTOR_SIZE, TABLE_HEADER_SIZE,
41};
42use flate2::read::ZlibDecoder;
43use memmap2::Mmap;
44
45#[derive(Debug, Clone, PartialEq, Eq)]
53#[non_exhaustive]
54pub struct RecoveryReport {
55 pub image_size: u64,
58 pub chunk_size: u64,
60 pub chunks_total: usize,
62 pub chunks_recovered_primary: usize,
64 pub chunks_recovered_table2: usize,
67 pub chunks_zero_filled: usize,
69 pub chunks_crc_flagged: usize,
74 pub bytes_recovered: u64,
77 pub bytes_zero_filled: u64,
79 pub truncation_offset: Option<u64>,
82 pub lost_chunks: Vec<usize>,
84 pub crc_flagged_chunks: Vec<usize>,
86}
87
88pub struct EwfRecover {
94 segment_paths: Vec<PathBuf>,
95}
96
97impl EwfRecover {
98 #[must_use]
103 pub fn from_path(path: impl AsRef<Path>) -> Self {
104 Self {
105 segment_paths: discover_segments(path.as_ref()),
106 }
107 }
108
109 #[must_use]
111 pub fn from_paths(paths: &[impl AsRef<Path>]) -> Self {
112 Self {
113 segment_paths: paths.iter().map(|p| p.as_ref().to_path_buf()).collect(),
114 }
115 }
116
117 pub fn recover_to_raw(&self, out_path: impl AsRef<Path>) -> io::Result<RecoveryReport> {
131 if self.segment_paths.is_empty() {
132 return Err(io::Error::new(
133 io::ErrorKind::InvalidInput,
134 "no EWF segments to recover",
135 ));
136 }
137
138 let mmaps = self
141 .segment_paths
142 .iter()
143 .map(|p| {
144 let file = File::open(p)?;
145 #[allow(unsafe_code)]
148 unsafe {
149 Mmap::map(&file)
150 }
151 })
152 .collect::<io::Result<Vec<Mmap>>>()?;
153 let segments: Vec<&[u8]> = mmaps.iter().map(std::convert::AsRef::as_ref).collect();
154
155 recover_segments(&segments, out_path.as_ref())
156 }
157}
158
159struct Section {
161 type_name: String,
162 offset: u64,
163 size: u64,
164}
165
166struct Geometry {
168 chunk_count: u32,
169 sectors_per_chunk: u32,
170 bytes_per_sector: u32,
171 sector_count: u64,
172}
173
174fn walk_sections(data: &[u8]) -> (Vec<Section>, Option<u64>) {
180 let file_size = data.len() as u64;
181 let mut sections = Vec::new();
182 let mut pos = FILE_HEADER_SIZE as u64;
183 let mut truncation: Option<u64> = None;
184
185 loop {
186 let off = pos as usize;
187 if off.saturating_add(SECTION_DESCRIPTOR_SIZE) > data.len() {
188 if pos < file_size {
191 truncation = Some(pos);
192 }
193 break;
194 }
195 let raw = &data[off..off.saturating_add(SECTION_DESCRIPTOR_SIZE)];
196 let Ok(desc) = SectionDescriptor::parse(raw, pos) else {
197 truncation = Some(pos);
201 break;
202 };
203 let next = desc.next;
204 let section_size = desc.section_size;
205 let type_name = desc.section_type;
206
207 sections.push(Section {
208 type_name: type_name.clone(),
209 offset: pos,
210 size: section_size,
211 });
212
213 if type_name == "done" || type_name == "next" {
214 break;
215 }
216
217 if next == 0 || next <= pos {
219 break;
220 }
221 if next > file_size {
222 truncation = Some(next);
223 break;
224 }
225 pos = next;
226 }
227
228 (sections, truncation)
229}
230
231fn read_geometry(data: &[u8], sections: &[Section]) -> Option<Geometry> {
233 let vol = sections
234 .iter()
235 .find(|s| s.type_name == "volume" || s.type_name == "disk")?;
236 let data_start = (vol.offset as usize).saturating_add(SECTION_DESCRIPTOR_SIZE);
237 let body_len = (vol.size as usize).saturating_sub(SECTION_DESCRIPTOR_SIZE);
238 let vol_end = data_start.saturating_add(body_len).min(data.len());
239 let body = data.get(data_start..vol_end)?;
240 let parsed = EwfVolume::parse(body).ok()?;
241 if parsed.sectors_per_chunk == 0 || parsed.bytes_per_sector == 0 {
242 return None;
243 }
244 Some(Geometry {
245 chunk_count: parsed.chunk_count,
246 sectors_per_chunk: parsed.sectors_per_chunk,
247 bytes_per_sector: parsed.bytes_per_sector,
248 sector_count: parsed.sector_count,
249 })
250}
251
252struct TableRef {
254 entry_count: usize,
255 base_offset: u64,
256 entries_file_offset: usize,
257}
258
259fn table_ref(data: &[u8], sections: &[Section], name: &str) -> Option<TableRef> {
261 let sec = sections.iter().find(|s| s.type_name == name)?;
262 let hdr_start = (sec.offset as usize).saturating_add(SECTION_DESCRIPTOR_SIZE);
263 let hdr = data.get(hdr_start..hdr_start.saturating_add(TABLE_HEADER_SIZE))?;
264 let header = TableHeader::parse(hdr).ok()?;
265 Some(TableRef {
266 entry_count: header.entry_count as usize,
267 base_offset: header.base_offset,
268 entries_file_offset: hdr_start.saturating_add(TABLE_HEADER_SIZE),
269 })
270}
271
272fn sectors_data_end(sections: &[Section], data_len: usize) -> Option<usize> {
274 let sec = sections.iter().find(|s| s.type_name == "sectors")?;
275 Some((sec.offset.saturating_add(sec.size) as usize).min(data_len))
276}
277
278fn entry_at(data: &[u8], t: &TableRef, i: usize) -> Option<(bool, u64)> {
280 let off = t.entries_file_offset.saturating_add(i.saturating_mul(4));
281 let bytes = data.get(off..off.saturating_add(4))?;
282 let e = TableEntry::parse(bytes).ok()?;
283 Some((
284 e.compressed,
285 t.base_offset.saturating_add(u64::from(e.chunk_offset)),
286 ))
287}
288
289fn chunk_range(
295 data: &[u8],
296 t: &TableRef,
297 i: usize,
298 sectors_end: Option<usize>,
299) -> Option<(usize, usize, bool)> {
300 let (compressed, abs) = entry_at(data, t, i)?;
301 let start = abs as usize;
302 let end = if i.saturating_add(1) < t.entry_count {
303 let (_, next_abs) = entry_at(data, t, i.saturating_add(1))?;
304 next_abs as usize
305 } else {
306 sectors_end.unwrap_or(data.len())
307 };
308 if start >= end || end > data.len() {
309 return None;
310 }
311 Some((start, end, compressed))
312}
313
314const MAX_CHUNK_SIZE: u64 = 64 * 1024 * 1024;
320
321fn decode_chunk(raw: &[u8], compressed: bool, chunk_size: usize) -> Option<(Vec<u8>, bool)> {
332 if compressed {
333 let mut out = Vec::with_capacity(chunk_size.min(raw.len().saturating_mul(4).max(1)));
334 let limit = (chunk_size as u64).saturating_add(1);
339 ZlibDecoder::new(raw)
340 .take(limit)
341 .read_to_end(&mut out)
342 .ok()?;
343 if out.is_empty() {
344 return None;
345 }
346 out.truncate(chunk_size);
347 Some((out, true))
348 } else {
349 let has_trailing_crc = raw.len() >= chunk_size.saturating_add(4);
353 let crc_ok = if has_trailing_crc {
354 let stored = u32::from_le_bytes([
355 raw[chunk_size],
356 raw[chunk_size + 1],
357 raw[chunk_size + 2],
358 raw[chunk_size + 3],
359 ]);
360 adler32(&raw[..chunk_size]) == stored
361 } else {
362 true
364 };
365 let take = raw.len().min(chunk_size);
366 Some((raw[..take].to_vec(), crc_ok))
367 }
368}
369
370fn locate_chunk(seg_entry_counts: &[usize], idx: usize) -> Option<(usize, usize)> {
373 let mut running = 0usize;
374 for (seg_idx, &count) in seg_entry_counts.iter().enumerate() {
375 if idx < running.saturating_add(count) {
376 return Some((seg_idx, idx.saturating_sub(running)));
377 }
378 running = running.saturating_add(count);
379 }
380 None
381}
382
383fn recover_segments(segments: &[&[u8]], out_path: &Path) -> io::Result<RecoveryReport> {
385 let first = segments.first().copied().unwrap_or(&[]);
388 if first.len() < FILE_HEADER_SIZE || !first.starts_with(&EVF_SIGNATURE) {
389 return Err(io::Error::new(
390 io::ErrorKind::InvalidData,
391 format!(
392 "not an EWF v1 image: first segment is {} bytes, signature {:02x?}",
393 first.len(),
394 first
395 .get(..FILE_HEADER_SIZE.min(first.len()))
396 .unwrap_or(&[])
397 ),
398 ));
399 }
400
401 let mut all_sections: Vec<Vec<Section>> = Vec::with_capacity(segments.len());
403 let mut truncation_offset: Option<u64> = None;
404 for seg in segments {
405 let (sections, trunc) = walk_sections(seg);
406 if truncation_offset.is_none() {
407 truncation_offset = trunc;
408 }
409 all_sections.push(sections);
410 }
411
412 let geom = read_geometry(first, &all_sections[0]).ok_or_else(|| {
415 io::Error::new(
416 io::ErrorKind::InvalidData,
417 "no parseable volume/disk section: cannot establish image geometry",
418 )
419 })?;
420
421 let chunk_size =
422 u64::from(geom.sectors_per_chunk).saturating_mul(u64::from(geom.bytes_per_sector));
423 if chunk_size == 0 || chunk_size > MAX_CHUNK_SIZE {
430 return Err(io::Error::new(
431 io::ErrorKind::InvalidData,
432 format!(
433 "volume declares {} sectors/chunk x {} bytes/sector = {chunk_size} bytes per \
434 chunk, outside the plausible range 1..={MAX_CHUNK_SIZE}",
435 geom.sectors_per_chunk, geom.bytes_per_sector
436 ),
437 ));
438 }
439 let image_size = geom
440 .sector_count
441 .saturating_mul(u64::from(geom.bytes_per_sector));
442 let chunk_size_usize = chunk_size as usize;
443 let total_chunks = geom.chunk_count as usize;
444
445 let mut primary: Vec<Option<TableRef>> = Vec::with_capacity(segments.len());
447 let mut fallback: Vec<Option<TableRef>> = Vec::with_capacity(segments.len());
448 let mut sec_ends: Vec<Option<usize>> = Vec::with_capacity(segments.len());
449 let mut seg_entry_counts: Vec<usize> = Vec::with_capacity(segments.len());
450 for (seg, sections) in segments.iter().zip(all_sections.iter()) {
451 let p = table_ref(seg, sections, "table");
452 let f = table_ref(seg, sections, "table2");
453 let count = p.as_ref().or(f.as_ref()).map_or(0, |t| t.entry_count);
457 seg_entry_counts.push(count);
458 primary.push(p);
459 fallback.push(f);
460 sec_ends.push(sectors_data_end(sections, seg.len()));
461 }
462
463 let mut out = io::BufWriter::new(File::create(out_path)?);
464
465 let mut recovered_primary = 0usize;
466 let mut recovered_table2 = 0usize;
467 let mut zero_filled = 0usize;
468 let mut crc_flagged = 0usize;
469 let mut bytes_recovered = 0u64;
470 let mut bytes_zero_filled = 0u64;
471 let mut lost_chunks: Vec<usize> = Vec::new();
472 let mut crc_flagged_chunks: Vec<usize> = Vec::new();
473
474 let mut bytes_remaining = image_size;
475
476 let decode_from = |table: Option<&TableRef>, seg_idx: usize, local: usize| {
478 let seg = segments[seg_idx];
479 let sec_end = sec_ends[seg_idx];
480 table.and_then(|t| {
481 chunk_range(seg, t, local, sec_end)
482 .and_then(|(s, e, c)| decode_chunk(&seg[s..e], c, chunk_size_usize))
483 })
484 };
485
486 for idx in 0..total_chunks {
487 if bytes_remaining == 0 {
488 break;
489 }
490 let logical = bytes_remaining.min(chunk_size) as usize;
491
492 let decoded: Option<(Vec<u8>, bool, bool)> = match locate_chunk(&seg_entry_counts, idx) {
495 Some((seg_idx, local)) => {
496 let from_primary = decode_from(primary[seg_idx].as_ref(), seg_idx, local);
497 match from_primary {
498 Some((bytes, true)) => Some((bytes, false, true)),
500 other => {
502 let from_t2 = decode_from(fallback[seg_idx].as_ref(), seg_idx, local);
503 match (other, from_t2) {
507 (_, Some((bytes, true))) => Some((bytes, true, true)),
509 (Some((bytes, _)), _) => Some((bytes, false, false)),
511 (None, Some((bytes, false))) => Some((bytes, true, false)),
513 (None, None) => None,
515 }
516 }
517 }
518 }
519 None => None,
520 };
521
522 if let Some((mut bytes, via_table2, crc_ok)) = decoded {
523 if bytes.len() > logical {
525 bytes.truncate(logical);
526 } else if bytes.len() < logical {
527 bytes.resize(logical, 0);
528 }
529 out.write_all(&bytes)?;
530 bytes_recovered = bytes_recovered.saturating_add(logical as u64);
531 if via_table2 {
532 recovered_table2 = recovered_table2.saturating_add(1);
533 } else {
534 recovered_primary = recovered_primary.saturating_add(1);
535 }
536 if !crc_ok {
537 crc_flagged = crc_flagged.saturating_add(1);
538 crc_flagged_chunks.push(idx);
539 }
540 } else {
541 write_zeros(&mut out, logical)?;
543 zero_filled = zero_filled.saturating_add(1);
544 bytes_zero_filled = bytes_zero_filled.saturating_add(logical as u64);
545 lost_chunks.push(idx);
546 }
547 bytes_remaining = bytes_remaining.saturating_sub(logical as u64);
548 }
549
550 while bytes_remaining > 0 {
554 let logical = bytes_remaining.min(chunk_size) as usize;
555 write_zeros(&mut out, logical)?;
556 bytes_zero_filled = bytes_zero_filled.saturating_add(logical as u64);
557 bytes_remaining = bytes_remaining.saturating_sub(logical as u64);
558 }
559
560 out.flush()?;
561
562 Ok(RecoveryReport {
563 image_size,
564 chunk_size,
565 chunks_total: total_chunks,
566 chunks_recovered_primary: recovered_primary,
567 chunks_recovered_table2: recovered_table2,
568 chunks_zero_filled: zero_filled,
569 chunks_crc_flagged: crc_flagged,
570 bytes_recovered,
571 bytes_zero_filled,
572 truncation_offset,
573 lost_chunks,
574 crc_flagged_chunks,
575 })
576}
577
578fn write_zeros(w: &mut impl Write, n: usize) -> io::Result<()> {
580 const BLOCK: usize = 8 * 1024;
581 let zeros = [0u8; BLOCK];
582 let mut left = n;
583 while left > 0 {
584 let take = left.min(BLOCK);
585 w.write_all(&zeros[..take])?;
586 left = left.saturating_sub(take);
587 }
588 Ok(())
589}
590
591fn discover_segments(base: &Path) -> Vec<PathBuf> {
596 let Some(ext) = base.extension().and_then(|e| e.to_str()) else {
597 return vec![base.to_path_buf()];
598 };
599 let lower = ext.to_ascii_lowercase();
602 if lower.len() != 3
603 || !lower.starts_with('e')
604 || !lower[1..].chars().all(|c| c.is_ascii_digit())
605 {
606 return vec![base.to_path_buf()];
607 }
608 let upper = ext.chars().next().is_some_and(|c| c.is_ascii_uppercase());
609 let mut out = vec![base.to_path_buf()];
610 let mut n = 2u32;
611 loop {
612 let e = if upper {
613 format!("E{n:02}")
614 } else {
615 format!("e{n:02}")
616 };
617 let candidate = base.with_extension(&e);
618 if candidate.exists() {
619 out.push(candidate);
620 n = n.saturating_add(1);
621 } else {
622 break;
623 }
624 }
625 out
626}
627
628#[cfg(test)]
629mod tests {
630 use super::*;
631 use flate2::write::ZlibEncoder;
632 use flate2::Compression;
633
634 const CHUNK_SIZE: usize = 32768;
635 const SECTORS_PER_CHUNK: u32 = 64;
636 const BYTES_PER_SECTOR: u32 = 512;
637
638 fn build_compressed_e01(data: &[u8], corrupt_stream: bool, add_table2: bool) -> Vec<u8> {
643 let mut padded = data.to_vec();
644 padded.resize(CHUNK_SIZE, 0);
645 let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
646 enc.write_all(&padded).unwrap();
647 let mut compressed = enc.finish().unwrap();
648 if corrupt_stream {
649 let mid = compressed.len() / 2;
652 compressed[mid] ^= 0xFF;
653 }
654
655 let sector_count = u64::from(CHUNK_SIZE as u32 / BYTES_PER_SECTOR);
656 let mut f = Vec::new();
657
658 f.extend_from_slice(&EVF_SIGNATURE);
660 f.push(0x01);
661 f.extend_from_slice(&1u16.to_le_bytes());
662 f.extend_from_slice(&0u16.to_le_bytes());
663
664 let vol_desc = FILE_HEADER_SIZE as u64;
666 let vol_data = vol_desc + SECTION_DESCRIPTOR_SIZE as u64;
667 let tbl_desc = vol_data + 94;
668 let tbl_hdr = tbl_desc + SECTION_DESCRIPTOR_SIZE as u64;
669 let tbl_entries = tbl_hdr + 24;
670 let after_tbl = tbl_entries + 4;
671 let (tbl2_desc, tbl2_hdr, tbl2_entries, after_tbl2) = if add_table2 {
673 let d = after_tbl;
674 let h = d + SECTION_DESCRIPTOR_SIZE as u64;
675 let e = h + 24;
676 (Some(d), h, e, e + 4)
677 } else {
678 (None, 0, 0, after_tbl)
679 };
680 let sec_desc = after_tbl2;
681 let sec_data = sec_desc + SECTION_DESCRIPTOR_SIZE as u64;
682 let done_desc = sec_data + compressed.len() as u64;
683
684 let mut vd = [0u8; SECTION_DESCRIPTOR_SIZE];
686 vd[..6].copy_from_slice(b"volume");
687 vd[16..24].copy_from_slice(&tbl_desc.to_le_bytes());
688 vd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 94).to_le_bytes());
689 f.extend_from_slice(&vd);
690 let mut vb = [0u8; 94];
691 vb[0..4].copy_from_slice(&1u32.to_le_bytes()); vb[4..8].copy_from_slice(&1u32.to_le_bytes()); vb[8..12].copy_from_slice(&SECTORS_PER_CHUNK.to_le_bytes());
694 vb[12..16].copy_from_slice(&BYTES_PER_SECTOR.to_le_bytes());
695 vb[16..24].copy_from_slice(§or_count.to_le_bytes());
696 f.extend_from_slice(&vb);
697
698 let emit_table = |f: &mut Vec<u8>, name: &[u8], next: u64| {
700 let mut td = [0u8; SECTION_DESCRIPTOR_SIZE];
701 td[..name.len()].copy_from_slice(name);
702 td[16..24].copy_from_slice(&next.to_le_bytes());
703 td[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4).to_le_bytes());
704 f.extend_from_slice(&td);
705 let mut th = [0u8; 24];
706 th[0..4].copy_from_slice(&1u32.to_le_bytes()); th[8..16].copy_from_slice(&sec_data.to_le_bytes()); f.extend_from_slice(&th);
709 f.extend_from_slice(&0x8000_0000u32.to_le_bytes()); };
711 emit_table(&mut f, b"table", tbl2_desc.unwrap_or(sec_desc));
712 if let Some(_d) = tbl2_desc {
713 emit_table(&mut f, b"table2", sec_desc);
714 }
715
716 let mut sd = [0u8; SECTION_DESCRIPTOR_SIZE];
718 sd[..7].copy_from_slice(b"sectors");
719 sd[16..24].copy_from_slice(&done_desc.to_le_bytes());
720 sd[24..32].copy_from_slice(
721 &(SECTION_DESCRIPTOR_SIZE as u64 + compressed.len() as u64).to_le_bytes(),
722 );
723 f.extend_from_slice(&sd);
724 f.extend_from_slice(&compressed);
725
726 let mut dd = [0u8; SECTION_DESCRIPTOR_SIZE];
728 dd[..4].copy_from_slice(b"done");
729 dd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64).to_le_bytes());
730 f.extend_from_slice(&dd);
731
732 let _ = (tbl2_hdr, tbl2_entries, after_tbl2);
734 f
735 }
736
737 fn recover_bytes(image: &[u8]) -> (RecoveryReport, Vec<u8>) {
738 let dir = tempfile::tempdir().unwrap();
739 let src = dir.path().join("img.E01");
740 std::fs::write(&src, image).unwrap();
741 let out = dir.path().join("out.raw");
742 let report = EwfRecover::from_path(&src).recover_to_raw(&out).unwrap();
743 let raw = std::fs::read(&out).unwrap();
744 (report, raw)
745 }
746
747 #[test]
748 fn compressed_chunk_recovers() {
749 let img = build_compressed_e01(b"hello compressed world", false, false);
750 let (report, raw) = recover_bytes(&img);
751 assert_eq!(report.chunks_total, 1);
752 assert_eq!(report.chunks_recovered_primary, 1);
753 assert_eq!(report.chunks_zero_filled, 0);
754 assert_eq!(raw.len(), CHUNK_SIZE);
755 assert_eq!(&raw[..22], b"hello compressed world");
756 }
757
758 #[test]
759 fn corrupt_compressed_chunk_zero_fills() {
760 let img = build_compressed_e01(b"data that becomes garbage", true, false);
764 let (report, raw) = recover_bytes(&img);
765 assert_eq!(report.chunks_zero_filled, 1, "broken zlib must zero-fill");
766 assert_eq!(report.lost_chunks, vec![0]);
767 assert_eq!(raw.len(), CHUNK_SIZE);
768 assert!(raw.iter().all(|&b| b == 0), "lost chunk is all zeros");
769 }
770
771 #[test]
772 fn table2_recovers_when_primary_stream_broken() {
773 let img = build_compressed_e01(b"x", true, true);
777 let (report, _raw) = recover_bytes(&img);
778 assert_eq!(report.chunks_zero_filled, 1);
779 }
780
781 #[test]
782 fn table2_present_clean_recovers_from_primary() {
783 let img = build_compressed_e01(b"good data via primary", false, true);
784 let (report, raw) = recover_bytes(&img);
785 assert_eq!(report.chunks_recovered_primary, 1);
786 assert_eq!(report.chunks_recovered_table2, 0);
787 assert_eq!(&raw[..21], b"good data via primary");
788 }
789
790 #[test]
791 fn from_paths_and_empty_error() {
792 let img = build_compressed_e01(b"z", false, false);
794 let dir = tempfile::tempdir().unwrap();
795 let p = dir.path().join("explicit.E01");
796 std::fs::write(&p, &img).unwrap();
797 let out = dir.path().join("o.raw");
798 let r = EwfRecover::from_paths(&[&p]).recover_to_raw(&out).unwrap();
799 assert_eq!(r.chunks_total, 1);
800
801 let empty: [&Path; 0] = [];
803 let err = EwfRecover::from_paths(&empty)
804 .recover_to_raw(dir.path().join("none.raw"))
805 .unwrap_err();
806 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
807 }
808
809 #[test]
810 fn not_an_ewf_image_errors() {
811 let dir = tempfile::tempdir().unwrap();
812 let p = dir.path().join("garbage.bin");
813 std::fs::write(&p, b"not an ewf file at all").unwrap();
814 let err = EwfRecover::from_paths(&[&p])
815 .recover_to_raw(dir.path().join("o.raw"))
816 .unwrap_err();
817 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
818 }
819
820 #[test]
821 fn valid_signature_but_no_volume_errors() {
822 let mut f = Vec::new();
825 f.extend_from_slice(&EVF_SIGNATURE);
826 f.push(0x01);
827 f.extend_from_slice(&1u16.to_le_bytes());
828 f.extend_from_slice(&0u16.to_le_bytes());
829 let mut dd = [0u8; SECTION_DESCRIPTOR_SIZE];
831 dd[..4].copy_from_slice(b"done");
832 dd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64).to_le_bytes());
833 f.extend_from_slice(&dd);
834 let dir = tempfile::tempdir().unwrap();
835 let p = dir.path().join("novol.E01");
836 std::fs::write(&p, &f).unwrap();
837 let err = EwfRecover::from_paths(&[&p])
838 .recover_to_raw(dir.path().join("o.raw"))
839 .unwrap_err();
840 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
841 }
842
843 #[test]
844 fn walk_sections_flags_short_descriptor_truncation() {
845 let mut f = Vec::new();
848 f.extend_from_slice(&EVF_SIGNATURE);
849 f.push(0x01);
850 f.extend_from_slice(&1u16.to_le_bytes());
851 f.extend_from_slice(&0u16.to_le_bytes());
852 f.extend_from_slice(&[0u8; 10]); let (sections, trunc) = walk_sections(&f);
854 assert!(sections.is_empty());
855 assert_eq!(trunc, Some(FILE_HEADER_SIZE as u64));
856 }
857
858 #[test]
859 fn walk_sections_flags_next_past_eof() {
860 let mut f = Vec::new();
863 f.extend_from_slice(&EVF_SIGNATURE);
864 f.push(0x01);
865 f.extend_from_slice(&1u16.to_le_bytes());
866 f.extend_from_slice(&0u16.to_le_bytes());
867 let mut vd = [0u8; SECTION_DESCRIPTOR_SIZE];
868 vd[..6].copy_from_slice(b"volume");
869 vd[16..24].copy_from_slice(&9_999_999u64.to_le_bytes()); vd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 94).to_le_bytes());
871 f.extend_from_slice(&vd);
872 let (sections, trunc) = walk_sections(&f);
873 assert_eq!(sections.len(), 1);
874 assert_eq!(trunc, Some(9_999_999));
875 }
876
877 #[test]
878 fn decode_uncompressed_bad_crc_still_emits() {
879 let mut raw = vec![0xABu8; CHUNK_SIZE];
881 raw.extend_from_slice(&0xDEAD_BEEFu32.to_le_bytes()); let (bytes, crc_ok) = decode_chunk(&raw, false, CHUNK_SIZE).unwrap();
883 assert_eq!(bytes.len(), CHUNK_SIZE);
884 assert!(!crc_ok);
885 }
886
887 #[test]
888 fn decode_uncompressed_good_crc_ok() {
889 let sectors = vec![0x5Au8; CHUNK_SIZE];
890 let crc = adler32(§ors);
891 let mut raw = sectors.clone();
892 raw.extend_from_slice(&crc.to_le_bytes());
893 let (bytes, crc_ok) = decode_chunk(&raw, false, CHUNK_SIZE).unwrap();
894 assert_eq!(bytes, sectors);
895 assert!(crc_ok);
896 }
897
898 #[test]
899 fn decode_uncompressed_short_final_chunk() {
900 let raw = vec![0x11u8; 100];
903 let (bytes, crc_ok) = decode_chunk(&raw, false, CHUNK_SIZE).unwrap();
904 assert_eq!(bytes.len(), 100);
905 assert!(crc_ok);
906 }
907
908 #[test]
909 fn locate_chunk_spans_segments() {
910 let counts = [3usize, 2, 4];
911 assert_eq!(locate_chunk(&counts, 0), Some((0, 0)));
912 assert_eq!(locate_chunk(&counts, 2), Some((0, 2)));
913 assert_eq!(locate_chunk(&counts, 3), Some((1, 0)));
914 assert_eq!(locate_chunk(&counts, 4), Some((1, 1)));
915 assert_eq!(locate_chunk(&counts, 5), Some((2, 0)));
916 assert_eq!(locate_chunk(&counts, 8), Some((2, 3)));
917 assert_eq!(locate_chunk(&counts, 9), None);
918 }
919
920 #[test]
921 fn discover_segments_non_ewf_extension_single() {
922 let p = Path::new("/tmp/whatever.bin");
923 assert_eq!(discover_segments(p), vec![p.to_path_buf()]);
924 }
925
926 #[test]
927 fn discover_segments_no_extension_single() {
928 let p = Path::new("/tmp/noext");
929 assert_eq!(discover_segments(p), vec![p.to_path_buf()]);
930 }
931
932 #[test]
933 fn discover_segments_lowercase_e01_single_when_no_siblings() {
934 let dir = tempfile::tempdir().unwrap();
935 let p = dir.path().join("img.e01");
936 std::fs::write(&p, b"x").unwrap();
937 assert_eq!(discover_segments(&p), vec![p]);
939 }
940
941 #[test]
944 fn read_geometry_rejects_zero_geometry() {
945 let mut f = Vec::new();
947 f.extend_from_slice(&EVF_SIGNATURE);
948 f.push(0x01);
949 f.extend_from_slice(&1u16.to_le_bytes());
950 f.extend_from_slice(&0u16.to_le_bytes());
951 let mut vd = [0u8; SECTION_DESCRIPTOR_SIZE];
952 vd[..6].copy_from_slice(b"volume");
953 let next = FILE_HEADER_SIZE as u64 + SECTION_DESCRIPTOR_SIZE as u64 + 94;
954 vd[16..24].copy_from_slice(&next.to_le_bytes());
955 vd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 94).to_le_bytes());
956 f.extend_from_slice(&vd);
957 let mut vb = [0u8; 94];
958 vb[0..4].copy_from_slice(&1u32.to_le_bytes());
959 vb[4..8].copy_from_slice(&1u32.to_le_bytes());
960 vb[12..16].copy_from_slice(&BYTES_PER_SECTOR.to_le_bytes());
962 f.extend_from_slice(&vb);
963 let (sections, _) = walk_sections(&f);
964 assert!(read_geometry(&f, §ions).is_none());
965 }
966
967 #[test]
968 fn chunk_range_out_of_bounds_is_none() {
969 let data = vec![0u8; 200];
971 let t = TableRef {
972 entry_count: 1,
973 base_offset: 10_000, entries_file_offset: 0,
975 };
976 let mut data = data;
978 data[0..4].copy_from_slice(&0x8000_0000u32.to_le_bytes());
979 assert!(chunk_range(&data, &t, 0, Some(200)).is_none());
980 }
981
982 #[test]
983 fn decode_compressed_empty_output_is_none() {
984 let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
986 enc.write_all(b"").unwrap();
987 let empty_stream = enc.finish().unwrap();
988 assert!(decode_chunk(&empty_stream, true, CHUNK_SIZE).is_none());
989 }
990
991 fn build_uncompressed_table2_good(chunk_count: u32, sector_count: u64) -> Vec<u8> {
996 let sectors = vec![0x7Eu8; CHUNK_SIZE];
997 let crc = adler32(§ors);
998
999 let mut f = Vec::new();
1000 f.extend_from_slice(&EVF_SIGNATURE);
1001 f.push(0x01);
1002 f.extend_from_slice(&1u16.to_le_bytes());
1003 f.extend_from_slice(&0u16.to_le_bytes());
1004
1005 let vol_desc = FILE_HEADER_SIZE as u64;
1006 let vol_data = vol_desc + SECTION_DESCRIPTOR_SIZE as u64;
1007 let tbl_desc = vol_data + 94;
1008 let tbl2_desc = tbl_desc + SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4;
1009 let sec_desc = tbl2_desc + SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4;
1010 let sec_data = sec_desc + SECTION_DESCRIPTOR_SIZE as u64;
1011 let chunk_len = CHUNK_SIZE as u64 + 4; let done_desc = sec_data + chunk_len;
1013
1014 let mut vd = [0u8; SECTION_DESCRIPTOR_SIZE];
1016 vd[..6].copy_from_slice(b"volume");
1017 vd[16..24].copy_from_slice(&tbl_desc.to_le_bytes());
1018 vd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 94).to_le_bytes());
1019 f.extend_from_slice(&vd);
1020 let mut vb = [0u8; 94];
1021 vb[0..4].copy_from_slice(&1u32.to_le_bytes());
1022 vb[4..8].copy_from_slice(&chunk_count.to_le_bytes());
1023 vb[8..12].copy_from_slice(&SECTORS_PER_CHUNK.to_le_bytes());
1024 vb[12..16].copy_from_slice(&BYTES_PER_SECTOR.to_le_bytes());
1025 vb[16..24].copy_from_slice(§or_count.to_le_bytes());
1026 f.extend_from_slice(&vb);
1027
1028 let emit = |f: &mut Vec<u8>, name: &[u8], next: u64, base: u64| {
1030 let mut td = [0u8; SECTION_DESCRIPTOR_SIZE];
1031 td[..name.len()].copy_from_slice(name);
1032 td[16..24].copy_from_slice(&next.to_le_bytes());
1033 td[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4).to_le_bytes());
1034 f.extend_from_slice(&td);
1035 let mut th = [0u8; 24];
1036 th[0..4].copy_from_slice(&1u32.to_le_bytes());
1037 th[8..16].copy_from_slice(&base.to_le_bytes());
1038 f.extend_from_slice(&th);
1039 f.extend_from_slice(&0u32.to_le_bytes()); };
1041 emit(&mut f, b"table", tbl2_desc, 9_000_000); emit(&mut f, b"table2", sec_desc, sec_data); let mut sd = [0u8; SECTION_DESCRIPTOR_SIZE];
1046 sd[..7].copy_from_slice(b"sectors");
1047 sd[16..24].copy_from_slice(&done_desc.to_le_bytes());
1048 sd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + chunk_len).to_le_bytes());
1049 f.extend_from_slice(&sd);
1050 f.extend_from_slice(§ors);
1051 f.extend_from_slice(&crc.to_le_bytes());
1052
1053 let mut dd = [0u8; SECTION_DESCRIPTOR_SIZE];
1054 dd[..4].copy_from_slice(b"done");
1055 dd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64).to_le_bytes());
1056 f.extend_from_slice(&dd);
1057 f
1058 }
1059
1060 #[test]
1061 fn table2_recovers_good_data_when_primary_out_of_range() {
1062 let img = build_uncompressed_table2_good(1, u64::from(SECTORS_PER_CHUNK));
1064 let (report, raw) = recover_bytes(&img);
1065 assert_eq!(report.chunks_recovered_table2, 1, "table2 must rescue");
1066 assert_eq!(report.chunks_recovered_primary, 0);
1067 assert_eq!(report.chunks_zero_filled, 0);
1068 assert_eq!(raw.len(), CHUNK_SIZE);
1069 assert!(raw.iter().all(|&b| b == 0x7E));
1070 }
1071
1072 #[test]
1073 fn table2_crc_flagged_when_primary_absent() {
1074 let mut img = build_uncompressed_table2_good(1, u64::from(SECTORS_PER_CHUNK));
1078 let crc_pos = img.len() - SECTION_DESCRIPTOR_SIZE - 4;
1081 for b in &mut img[crc_pos..crc_pos + 4] {
1082 *b ^= 0xFF;
1083 }
1084 let (report, raw) = recover_bytes(&img);
1085 assert_eq!(
1086 report.chunks_recovered_table2, 1,
1087 "table2 still supplies data"
1088 );
1089 assert_eq!(report.chunks_recovered_primary, 0);
1090 assert_eq!(
1091 report.chunks_zero_filled, 0,
1092 "present data is not zero-filled"
1093 );
1094 assert_eq!(
1095 report.chunks_crc_flagged, 1,
1096 "table2 data flagged CRC-suspect"
1097 );
1098 assert_eq!(report.crc_flagged_chunks, vec![0]);
1099 assert!(raw.iter().all(|&b| b == 0x7E));
1100 }
1101
1102 #[test]
1103 fn geometry_undercover_zero_fills_tail() {
1104 let img = build_uncompressed_table2_good(1, u64::from(SECTORS_PER_CHUNK) * 2);
1107 let (report, raw) = recover_bytes(&img);
1108 assert_eq!(report.image_size, (CHUNK_SIZE * 2) as u64);
1109 assert_eq!(raw.len(), CHUNK_SIZE * 2);
1110 assert!(raw[..CHUNK_SIZE].iter().all(|&b| b == 0x7E));
1112 assert!(raw[CHUNK_SIZE..].iter().all(|&b| b == 0));
1113 assert!(report.bytes_zero_filled >= CHUNK_SIZE as u64);
1114 }
1115
1116 #[test]
1117 fn walk_sections_breaks_on_next_zero_nonterminal() {
1118 let mut f = Vec::new();
1121 f.extend_from_slice(&EVF_SIGNATURE);
1122 f.push(0x01);
1123 f.extend_from_slice(&1u16.to_le_bytes());
1124 f.extend_from_slice(&0u16.to_le_bytes());
1125 let mut vd = [0u8; SECTION_DESCRIPTOR_SIZE];
1126 vd[..6].copy_from_slice(b"volume");
1127 vd[16..24].copy_from_slice(&0u64.to_le_bytes()); vd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 94).to_le_bytes());
1129 f.extend_from_slice(&vd);
1130 f.extend_from_slice(&[0u8; 94]);
1131 let (sections, trunc) = walk_sections(&f);
1132 assert_eq!(sections.len(), 1);
1133 assert_eq!(trunc, None, "next==0 ends the chain, not a truncation");
1134 }
1135
1136 fn build_uncompressed_sized(chunk_body: &[u8], sector_count: u64) -> Vec<u8> {
1140 let mut f = Vec::new();
1141 f.extend_from_slice(&EVF_SIGNATURE);
1142 f.push(0x01);
1143 f.extend_from_slice(&1u16.to_le_bytes());
1144 f.extend_from_slice(&0u16.to_le_bytes());
1145
1146 let vol_desc = FILE_HEADER_SIZE as u64;
1147 let vol_data = vol_desc + SECTION_DESCRIPTOR_SIZE as u64;
1148 let tbl_desc = vol_data + 94;
1149 let sec_desc = tbl_desc + SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4;
1150 let sec_data = sec_desc + SECTION_DESCRIPTOR_SIZE as u64;
1151 let done_desc = sec_data + chunk_body.len() as u64;
1152
1153 let mut vd = [0u8; SECTION_DESCRIPTOR_SIZE];
1154 vd[..6].copy_from_slice(b"volume");
1155 vd[16..24].copy_from_slice(&tbl_desc.to_le_bytes());
1156 vd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 94).to_le_bytes());
1157 f.extend_from_slice(&vd);
1158 let mut vb = [0u8; 94];
1159 vb[0..4].copy_from_slice(&1u32.to_le_bytes());
1160 vb[4..8].copy_from_slice(&1u32.to_le_bytes()); vb[8..12].copy_from_slice(&SECTORS_PER_CHUNK.to_le_bytes());
1162 vb[12..16].copy_from_slice(&BYTES_PER_SECTOR.to_le_bytes());
1163 vb[16..24].copy_from_slice(§or_count.to_le_bytes());
1164 f.extend_from_slice(&vb);
1165
1166 let mut td = [0u8; SECTION_DESCRIPTOR_SIZE];
1167 td[..5].copy_from_slice(b"table");
1168 td[16..24].copy_from_slice(&sec_desc.to_le_bytes());
1169 td[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4).to_le_bytes());
1170 f.extend_from_slice(&td);
1171 let mut th = [0u8; 24];
1172 th[0..4].copy_from_slice(&1u32.to_le_bytes());
1173 th[8..16].copy_from_slice(&sec_data.to_le_bytes());
1174 f.extend_from_slice(&th);
1175 f.extend_from_slice(&0u32.to_le_bytes()); let mut sd = [0u8; SECTION_DESCRIPTOR_SIZE];
1178 sd[..7].copy_from_slice(b"sectors");
1179 sd[16..24].copy_from_slice(&done_desc.to_le_bytes());
1180 sd[24..32].copy_from_slice(
1181 &(SECTION_DESCRIPTOR_SIZE as u64 + chunk_body.len() as u64).to_le_bytes(),
1182 );
1183 f.extend_from_slice(&sd);
1184 f.extend_from_slice(chunk_body);
1185
1186 let mut dd = [0u8; SECTION_DESCRIPTOR_SIZE];
1187 dd[..4].copy_from_slice(b"done");
1188 dd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64).to_le_bytes());
1189 f.extend_from_slice(&dd);
1190 f
1191 }
1192
1193 #[test]
1194 fn chunk_longer_than_logical_is_truncated() {
1195 let body = vec![0x42u8; CHUNK_SIZE];
1199 let img = build_uncompressed_sized(&body, u64::from(SECTORS_PER_CHUNK) / 2);
1200 let (report, raw) = recover_bytes(&img);
1201 assert_eq!(report.image_size, (CHUNK_SIZE / 2) as u64);
1202 assert_eq!(raw.len(), CHUNK_SIZE / 2);
1203 assert!(raw.iter().all(|&b| b == 0x42));
1204 }
1205
1206 #[test]
1207 fn chunk_shorter_than_logical_is_padded() {
1208 let body = vec![0x24u8; 100];
1212 let img = build_uncompressed_sized(&body, u64::from(SECTORS_PER_CHUNK));
1213 let (report, raw) = recover_bytes(&img);
1214 assert_eq!(report.image_size, CHUNK_SIZE as u64);
1215 assert_eq!(raw.len(), CHUNK_SIZE);
1216 assert!(raw[..100].iter().all(|&b| b == 0x24));
1217 assert!(
1218 raw[100..].iter().all(|&b| b == 0),
1219 "short chunk zero-padded"
1220 );
1221 }
1222
1223 #[test]
1224 fn geometry_overcover_stops_at_image_size() {
1225 let img = build_uncompressed_table2_good(2, u64::from(SECTORS_PER_CHUNK));
1228 let (report, raw) = recover_bytes(&img);
1229 assert_eq!(report.image_size, CHUNK_SIZE as u64);
1230 assert_eq!(raw.len(), CHUNK_SIZE);
1231 assert_eq!(
1233 report.chunks_recovered_primary
1234 + report.chunks_recovered_table2
1235 + report.chunks_zero_filled,
1236 1
1237 );
1238 }
1239}