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
314fn decode_chunk(raw: &[u8], compressed: bool, chunk_size: usize) -> Option<(Vec<u8>, bool)> {
325 if compressed {
326 let mut out = Vec::with_capacity(chunk_size.min(raw.len().saturating_mul(4).max(1)));
327 let limit = (chunk_size as u64).saturating_add(1);
332 ZlibDecoder::new(raw)
333 .take(limit)
334 .read_to_end(&mut out)
335 .ok()?;
336 if out.is_empty() {
337 return None;
338 }
339 out.truncate(chunk_size);
340 Some((out, true))
341 } else {
342 let has_trailing_crc = raw.len() >= chunk_size.saturating_add(4);
346 let crc_ok = if has_trailing_crc {
347 let stored = u32::from_le_bytes([
348 raw[chunk_size],
349 raw[chunk_size + 1],
350 raw[chunk_size + 2],
351 raw[chunk_size + 3],
352 ]);
353 adler32(&raw[..chunk_size]) == stored
354 } else {
355 true
357 };
358 let take = raw.len().min(chunk_size);
359 Some((raw[..take].to_vec(), crc_ok))
360 }
361}
362
363fn locate_chunk(seg_entry_counts: &[usize], idx: usize) -> Option<(usize, usize)> {
366 let mut running = 0usize;
367 for (seg_idx, &count) in seg_entry_counts.iter().enumerate() {
368 if idx < running.saturating_add(count) {
369 return Some((seg_idx, idx.saturating_sub(running)));
370 }
371 running = running.saturating_add(count);
372 }
373 None
374}
375
376fn recover_segments(segments: &[&[u8]], out_path: &Path) -> io::Result<RecoveryReport> {
378 let first = segments.first().copied().unwrap_or(&[]);
381 if first.len() < FILE_HEADER_SIZE || !first.starts_with(&EVF_SIGNATURE) {
382 return Err(io::Error::new(
383 io::ErrorKind::InvalidData,
384 format!(
385 "not an EWF v1 image: first segment is {} bytes, signature {:02x?}",
386 first.len(),
387 first
388 .get(..FILE_HEADER_SIZE.min(first.len()))
389 .unwrap_or(&[])
390 ),
391 ));
392 }
393
394 let mut all_sections: Vec<Vec<Section>> = Vec::with_capacity(segments.len());
396 let mut truncation_offset: Option<u64> = None;
397 for seg in segments {
398 let (sections, trunc) = walk_sections(seg);
399 if truncation_offset.is_none() {
400 truncation_offset = trunc;
401 }
402 all_sections.push(sections);
403 }
404
405 let geom = read_geometry(first, &all_sections[0]).ok_or_else(|| {
408 io::Error::new(
409 io::ErrorKind::InvalidData,
410 "no parseable volume/disk section: cannot establish image geometry",
411 )
412 })?;
413
414 let chunk_size =
415 u64::from(geom.sectors_per_chunk).saturating_mul(u64::from(geom.bytes_per_sector));
416 let image_size = geom
417 .sector_count
418 .saturating_mul(u64::from(geom.bytes_per_sector));
419 let chunk_size_usize = chunk_size as usize;
420 let total_chunks = geom.chunk_count as usize;
421
422 let mut primary: Vec<Option<TableRef>> = Vec::with_capacity(segments.len());
424 let mut fallback: Vec<Option<TableRef>> = Vec::with_capacity(segments.len());
425 let mut sec_ends: Vec<Option<usize>> = Vec::with_capacity(segments.len());
426 let mut seg_entry_counts: Vec<usize> = Vec::with_capacity(segments.len());
427 for (seg, sections) in segments.iter().zip(all_sections.iter()) {
428 let p = table_ref(seg, sections, "table");
429 let f = table_ref(seg, sections, "table2");
430 let count = p.as_ref().or(f.as_ref()).map_or(0, |t| t.entry_count);
434 seg_entry_counts.push(count);
435 primary.push(p);
436 fallback.push(f);
437 sec_ends.push(sectors_data_end(sections, seg.len()));
438 }
439
440 let mut out = io::BufWriter::new(File::create(out_path)?);
441
442 let mut recovered_primary = 0usize;
443 let mut recovered_table2 = 0usize;
444 let mut zero_filled = 0usize;
445 let mut crc_flagged = 0usize;
446 let mut bytes_recovered = 0u64;
447 let mut bytes_zero_filled = 0u64;
448 let mut lost_chunks: Vec<usize> = Vec::new();
449 let mut crc_flagged_chunks: Vec<usize> = Vec::new();
450
451 let mut bytes_remaining = image_size;
452
453 let decode_from = |table: Option<&TableRef>, seg_idx: usize, local: usize| {
455 let seg = segments[seg_idx];
456 let sec_end = sec_ends[seg_idx];
457 table.and_then(|t| {
458 chunk_range(seg, t, local, sec_end)
459 .and_then(|(s, e, c)| decode_chunk(&seg[s..e], c, chunk_size_usize))
460 })
461 };
462
463 for idx in 0..total_chunks {
464 if bytes_remaining == 0 {
465 break;
466 }
467 let logical = bytes_remaining.min(chunk_size) as usize;
468
469 let decoded: Option<(Vec<u8>, bool, bool)> = match locate_chunk(&seg_entry_counts, idx) {
472 Some((seg_idx, local)) => {
473 let from_primary = decode_from(primary[seg_idx].as_ref(), seg_idx, local);
474 match from_primary {
475 Some((bytes, true)) => Some((bytes, false, true)),
477 other => {
479 let from_t2 = decode_from(fallback[seg_idx].as_ref(), seg_idx, local);
480 match (other, from_t2) {
484 (_, Some((bytes, true))) => Some((bytes, true, true)),
486 (Some((bytes, _)), _) => Some((bytes, false, false)),
488 (None, Some((bytes, false))) => Some((bytes, true, false)),
490 (None, None) => None,
492 }
493 }
494 }
495 }
496 None => None,
497 };
498
499 if let Some((mut bytes, via_table2, crc_ok)) = decoded {
500 if bytes.len() > logical {
502 bytes.truncate(logical);
503 } else if bytes.len() < logical {
504 bytes.resize(logical, 0);
505 }
506 out.write_all(&bytes)?;
507 bytes_recovered = bytes_recovered.saturating_add(logical as u64);
508 if via_table2 {
509 recovered_table2 = recovered_table2.saturating_add(1);
510 } else {
511 recovered_primary = recovered_primary.saturating_add(1);
512 }
513 if !crc_ok {
514 crc_flagged = crc_flagged.saturating_add(1);
515 crc_flagged_chunks.push(idx);
516 }
517 } else {
518 write_zeros(&mut out, logical)?;
520 zero_filled = zero_filled.saturating_add(1);
521 bytes_zero_filled = bytes_zero_filled.saturating_add(logical as u64);
522 lost_chunks.push(idx);
523 }
524 bytes_remaining = bytes_remaining.saturating_sub(logical as u64);
525 }
526
527 while bytes_remaining > 0 {
531 let logical = bytes_remaining.min(chunk_size) as usize;
532 write_zeros(&mut out, logical)?;
533 bytes_zero_filled = bytes_zero_filled.saturating_add(logical as u64);
534 bytes_remaining = bytes_remaining.saturating_sub(logical as u64);
535 }
536
537 out.flush()?;
538
539 Ok(RecoveryReport {
540 image_size,
541 chunk_size,
542 chunks_total: total_chunks,
543 chunks_recovered_primary: recovered_primary,
544 chunks_recovered_table2: recovered_table2,
545 chunks_zero_filled: zero_filled,
546 chunks_crc_flagged: crc_flagged,
547 bytes_recovered,
548 bytes_zero_filled,
549 truncation_offset,
550 lost_chunks,
551 crc_flagged_chunks,
552 })
553}
554
555fn write_zeros(w: &mut impl Write, n: usize) -> io::Result<()> {
557 const BLOCK: usize = 8 * 1024;
558 let zeros = [0u8; BLOCK];
559 let mut left = n;
560 while left > 0 {
561 let take = left.min(BLOCK);
562 w.write_all(&zeros[..take])?;
563 left = left.saturating_sub(take);
564 }
565 Ok(())
566}
567
568fn discover_segments(base: &Path) -> Vec<PathBuf> {
573 let Some(ext) = base.extension().and_then(|e| e.to_str()) else {
574 return vec![base.to_path_buf()];
575 };
576 let lower = ext.to_ascii_lowercase();
579 if lower.len() != 3
580 || !lower.starts_with('e')
581 || !lower[1..].chars().all(|c| c.is_ascii_digit())
582 {
583 return vec![base.to_path_buf()];
584 }
585 let upper = ext.chars().next().is_some_and(|c| c.is_ascii_uppercase());
586 let mut out = vec![base.to_path_buf()];
587 let mut n = 2u32;
588 loop {
589 let e = if upper {
590 format!("E{n:02}")
591 } else {
592 format!("e{n:02}")
593 };
594 let candidate = base.with_extension(&e);
595 if candidate.exists() {
596 out.push(candidate);
597 n = n.saturating_add(1);
598 } else {
599 break;
600 }
601 }
602 out
603}
604
605#[cfg(test)]
606mod tests {
607 use super::*;
608 use flate2::write::ZlibEncoder;
609 use flate2::Compression;
610
611 const CHUNK_SIZE: usize = 32768;
612 const SECTORS_PER_CHUNK: u32 = 64;
613 const BYTES_PER_SECTOR: u32 = 512;
614
615 fn build_compressed_e01(data: &[u8], corrupt_stream: bool, add_table2: bool) -> Vec<u8> {
620 let mut padded = data.to_vec();
621 padded.resize(CHUNK_SIZE, 0);
622 let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
623 enc.write_all(&padded).unwrap();
624 let mut compressed = enc.finish().unwrap();
625 if corrupt_stream {
626 let mid = compressed.len() / 2;
629 compressed[mid] ^= 0xFF;
630 }
631
632 let sector_count = u64::from(CHUNK_SIZE as u32 / BYTES_PER_SECTOR);
633 let mut f = Vec::new();
634
635 f.extend_from_slice(&EVF_SIGNATURE);
637 f.push(0x01);
638 f.extend_from_slice(&1u16.to_le_bytes());
639 f.extend_from_slice(&0u16.to_le_bytes());
640
641 let vol_desc = FILE_HEADER_SIZE as u64;
643 let vol_data = vol_desc + SECTION_DESCRIPTOR_SIZE as u64;
644 let tbl_desc = vol_data + 94;
645 let tbl_hdr = tbl_desc + SECTION_DESCRIPTOR_SIZE as u64;
646 let tbl_entries = tbl_hdr + 24;
647 let after_tbl = tbl_entries + 4;
648 let (tbl2_desc, tbl2_hdr, tbl2_entries, after_tbl2) = if add_table2 {
650 let d = after_tbl;
651 let h = d + SECTION_DESCRIPTOR_SIZE as u64;
652 let e = h + 24;
653 (Some(d), h, e, e + 4)
654 } else {
655 (None, 0, 0, after_tbl)
656 };
657 let sec_desc = after_tbl2;
658 let sec_data = sec_desc + SECTION_DESCRIPTOR_SIZE as u64;
659 let done_desc = sec_data + compressed.len() as u64;
660
661 let mut vd = [0u8; SECTION_DESCRIPTOR_SIZE];
663 vd[..6].copy_from_slice(b"volume");
664 vd[16..24].copy_from_slice(&tbl_desc.to_le_bytes());
665 vd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 94).to_le_bytes());
666 f.extend_from_slice(&vd);
667 let mut vb = [0u8; 94];
668 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());
671 vb[12..16].copy_from_slice(&BYTES_PER_SECTOR.to_le_bytes());
672 vb[16..24].copy_from_slice(§or_count.to_le_bytes());
673 f.extend_from_slice(&vb);
674
675 let emit_table = |f: &mut Vec<u8>, name: &[u8], next: u64| {
677 let mut td = [0u8; SECTION_DESCRIPTOR_SIZE];
678 td[..name.len()].copy_from_slice(name);
679 td[16..24].copy_from_slice(&next.to_le_bytes());
680 td[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4).to_le_bytes());
681 f.extend_from_slice(&td);
682 let mut th = [0u8; 24];
683 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);
686 f.extend_from_slice(&0x8000_0000u32.to_le_bytes()); };
688 emit_table(&mut f, b"table", tbl2_desc.unwrap_or(sec_desc));
689 if let Some(_d) = tbl2_desc {
690 emit_table(&mut f, b"table2", sec_desc);
691 }
692
693 let mut sd = [0u8; SECTION_DESCRIPTOR_SIZE];
695 sd[..7].copy_from_slice(b"sectors");
696 sd[16..24].copy_from_slice(&done_desc.to_le_bytes());
697 sd[24..32].copy_from_slice(
698 &(SECTION_DESCRIPTOR_SIZE as u64 + compressed.len() as u64).to_le_bytes(),
699 );
700 f.extend_from_slice(&sd);
701 f.extend_from_slice(&compressed);
702
703 let mut dd = [0u8; SECTION_DESCRIPTOR_SIZE];
705 dd[..4].copy_from_slice(b"done");
706 dd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64).to_le_bytes());
707 f.extend_from_slice(&dd);
708
709 let _ = (tbl2_hdr, tbl2_entries, after_tbl2);
711 f
712 }
713
714 fn recover_bytes(image: &[u8]) -> (RecoveryReport, Vec<u8>) {
715 let dir = tempfile::tempdir().unwrap();
716 let src = dir.path().join("img.E01");
717 std::fs::write(&src, image).unwrap();
718 let out = dir.path().join("out.raw");
719 let report = EwfRecover::from_path(&src).recover_to_raw(&out).unwrap();
720 let raw = std::fs::read(&out).unwrap();
721 (report, raw)
722 }
723
724 #[test]
725 fn compressed_chunk_recovers() {
726 let img = build_compressed_e01(b"hello compressed world", false, false);
727 let (report, raw) = recover_bytes(&img);
728 assert_eq!(report.chunks_total, 1);
729 assert_eq!(report.chunks_recovered_primary, 1);
730 assert_eq!(report.chunks_zero_filled, 0);
731 assert_eq!(raw.len(), CHUNK_SIZE);
732 assert_eq!(&raw[..22], b"hello compressed world");
733 }
734
735 #[test]
736 fn corrupt_compressed_chunk_zero_fills() {
737 let img = build_compressed_e01(b"data that becomes garbage", true, false);
741 let (report, raw) = recover_bytes(&img);
742 assert_eq!(report.chunks_zero_filled, 1, "broken zlib must zero-fill");
743 assert_eq!(report.lost_chunks, vec![0]);
744 assert_eq!(raw.len(), CHUNK_SIZE);
745 assert!(raw.iter().all(|&b| b == 0), "lost chunk is all zeros");
746 }
747
748 #[test]
749 fn table2_recovers_when_primary_stream_broken() {
750 let img = build_compressed_e01(b"x", true, true);
754 let (report, _raw) = recover_bytes(&img);
755 assert_eq!(report.chunks_zero_filled, 1);
756 }
757
758 #[test]
759 fn table2_present_clean_recovers_from_primary() {
760 let img = build_compressed_e01(b"good data via primary", false, true);
761 let (report, raw) = recover_bytes(&img);
762 assert_eq!(report.chunks_recovered_primary, 1);
763 assert_eq!(report.chunks_recovered_table2, 0);
764 assert_eq!(&raw[..21], b"good data via primary");
765 }
766
767 #[test]
768 fn from_paths_and_empty_error() {
769 let img = build_compressed_e01(b"z", false, false);
771 let dir = tempfile::tempdir().unwrap();
772 let p = dir.path().join("explicit.E01");
773 std::fs::write(&p, &img).unwrap();
774 let out = dir.path().join("o.raw");
775 let r = EwfRecover::from_paths(&[&p]).recover_to_raw(&out).unwrap();
776 assert_eq!(r.chunks_total, 1);
777
778 let empty: [&Path; 0] = [];
780 let err = EwfRecover::from_paths(&empty)
781 .recover_to_raw(dir.path().join("none.raw"))
782 .unwrap_err();
783 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
784 }
785
786 #[test]
787 fn not_an_ewf_image_errors() {
788 let dir = tempfile::tempdir().unwrap();
789 let p = dir.path().join("garbage.bin");
790 std::fs::write(&p, b"not an ewf file at all").unwrap();
791 let err = EwfRecover::from_paths(&[&p])
792 .recover_to_raw(dir.path().join("o.raw"))
793 .unwrap_err();
794 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
795 }
796
797 #[test]
798 fn valid_signature_but_no_volume_errors() {
799 let mut f = Vec::new();
802 f.extend_from_slice(&EVF_SIGNATURE);
803 f.push(0x01);
804 f.extend_from_slice(&1u16.to_le_bytes());
805 f.extend_from_slice(&0u16.to_le_bytes());
806 let mut dd = [0u8; SECTION_DESCRIPTOR_SIZE];
808 dd[..4].copy_from_slice(b"done");
809 dd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64).to_le_bytes());
810 f.extend_from_slice(&dd);
811 let dir = tempfile::tempdir().unwrap();
812 let p = dir.path().join("novol.E01");
813 std::fs::write(&p, &f).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 walk_sections_flags_short_descriptor_truncation() {
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 f.extend_from_slice(&[0u8; 10]); let (sections, trunc) = walk_sections(&f);
831 assert!(sections.is_empty());
832 assert_eq!(trunc, Some(FILE_HEADER_SIZE as u64));
833 }
834
835 #[test]
836 fn walk_sections_flags_next_past_eof() {
837 let mut f = Vec::new();
840 f.extend_from_slice(&EVF_SIGNATURE);
841 f.push(0x01);
842 f.extend_from_slice(&1u16.to_le_bytes());
843 f.extend_from_slice(&0u16.to_le_bytes());
844 let mut vd = [0u8; SECTION_DESCRIPTOR_SIZE];
845 vd[..6].copy_from_slice(b"volume");
846 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());
848 f.extend_from_slice(&vd);
849 let (sections, trunc) = walk_sections(&f);
850 assert_eq!(sections.len(), 1);
851 assert_eq!(trunc, Some(9_999_999));
852 }
853
854 #[test]
855 fn decode_uncompressed_bad_crc_still_emits() {
856 let mut raw = vec![0xABu8; CHUNK_SIZE];
858 raw.extend_from_slice(&0xDEAD_BEEFu32.to_le_bytes()); let (bytes, crc_ok) = decode_chunk(&raw, false, CHUNK_SIZE).unwrap();
860 assert_eq!(bytes.len(), CHUNK_SIZE);
861 assert!(!crc_ok);
862 }
863
864 #[test]
865 fn decode_uncompressed_good_crc_ok() {
866 let sectors = vec![0x5Au8; CHUNK_SIZE];
867 let crc = adler32(§ors);
868 let mut raw = sectors.clone();
869 raw.extend_from_slice(&crc.to_le_bytes());
870 let (bytes, crc_ok) = decode_chunk(&raw, false, CHUNK_SIZE).unwrap();
871 assert_eq!(bytes, sectors);
872 assert!(crc_ok);
873 }
874
875 #[test]
876 fn decode_uncompressed_short_final_chunk() {
877 let raw = vec![0x11u8; 100];
880 let (bytes, crc_ok) = decode_chunk(&raw, false, CHUNK_SIZE).unwrap();
881 assert_eq!(bytes.len(), 100);
882 assert!(crc_ok);
883 }
884
885 #[test]
886 fn locate_chunk_spans_segments() {
887 let counts = [3usize, 2, 4];
888 assert_eq!(locate_chunk(&counts, 0), Some((0, 0)));
889 assert_eq!(locate_chunk(&counts, 2), Some((0, 2)));
890 assert_eq!(locate_chunk(&counts, 3), Some((1, 0)));
891 assert_eq!(locate_chunk(&counts, 4), Some((1, 1)));
892 assert_eq!(locate_chunk(&counts, 5), Some((2, 0)));
893 assert_eq!(locate_chunk(&counts, 8), Some((2, 3)));
894 assert_eq!(locate_chunk(&counts, 9), None);
895 }
896
897 #[test]
898 fn discover_segments_non_ewf_extension_single() {
899 let p = Path::new("/tmp/whatever.bin");
900 assert_eq!(discover_segments(p), vec![p.to_path_buf()]);
901 }
902
903 #[test]
904 fn discover_segments_no_extension_single() {
905 let p = Path::new("/tmp/noext");
906 assert_eq!(discover_segments(p), vec![p.to_path_buf()]);
907 }
908
909 #[test]
910 fn discover_segments_lowercase_e01_single_when_no_siblings() {
911 let dir = tempfile::tempdir().unwrap();
912 let p = dir.path().join("img.e01");
913 std::fs::write(&p, b"x").unwrap();
914 assert_eq!(discover_segments(&p), vec![p]);
916 }
917
918 #[test]
921 fn read_geometry_rejects_zero_geometry() {
922 let mut f = Vec::new();
924 f.extend_from_slice(&EVF_SIGNATURE);
925 f.push(0x01);
926 f.extend_from_slice(&1u16.to_le_bytes());
927 f.extend_from_slice(&0u16.to_le_bytes());
928 let mut vd = [0u8; SECTION_DESCRIPTOR_SIZE];
929 vd[..6].copy_from_slice(b"volume");
930 let next = FILE_HEADER_SIZE as u64 + SECTION_DESCRIPTOR_SIZE as u64 + 94;
931 vd[16..24].copy_from_slice(&next.to_le_bytes());
932 vd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 94).to_le_bytes());
933 f.extend_from_slice(&vd);
934 let mut vb = [0u8; 94];
935 vb[0..4].copy_from_slice(&1u32.to_le_bytes());
936 vb[4..8].copy_from_slice(&1u32.to_le_bytes());
937 vb[12..16].copy_from_slice(&BYTES_PER_SECTOR.to_le_bytes());
939 f.extend_from_slice(&vb);
940 let (sections, _) = walk_sections(&f);
941 assert!(read_geometry(&f, §ions).is_none());
942 }
943
944 #[test]
945 fn chunk_range_out_of_bounds_is_none() {
946 let data = vec![0u8; 200];
948 let t = TableRef {
949 entry_count: 1,
950 base_offset: 10_000, entries_file_offset: 0,
952 };
953 let mut data = data;
955 data[0..4].copy_from_slice(&0x8000_0000u32.to_le_bytes());
956 assert!(chunk_range(&data, &t, 0, Some(200)).is_none());
957 }
958
959 #[test]
960 fn decode_compressed_empty_output_is_none() {
961 let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
963 enc.write_all(b"").unwrap();
964 let empty_stream = enc.finish().unwrap();
965 assert!(decode_chunk(&empty_stream, true, CHUNK_SIZE).is_none());
966 }
967
968 fn build_uncompressed_table2_good(chunk_count: u32, sector_count: u64) -> Vec<u8> {
973 let sectors = vec![0x7Eu8; CHUNK_SIZE];
974 let crc = adler32(§ors);
975
976 let mut f = Vec::new();
977 f.extend_from_slice(&EVF_SIGNATURE);
978 f.push(0x01);
979 f.extend_from_slice(&1u16.to_le_bytes());
980 f.extend_from_slice(&0u16.to_le_bytes());
981
982 let vol_desc = FILE_HEADER_SIZE as u64;
983 let vol_data = vol_desc + SECTION_DESCRIPTOR_SIZE as u64;
984 let tbl_desc = vol_data + 94;
985 let tbl2_desc = tbl_desc + SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4;
986 let sec_desc = tbl2_desc + SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4;
987 let sec_data = sec_desc + SECTION_DESCRIPTOR_SIZE as u64;
988 let chunk_len = CHUNK_SIZE as u64 + 4; let done_desc = sec_data + chunk_len;
990
991 let mut vd = [0u8; SECTION_DESCRIPTOR_SIZE];
993 vd[..6].copy_from_slice(b"volume");
994 vd[16..24].copy_from_slice(&tbl_desc.to_le_bytes());
995 vd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 94).to_le_bytes());
996 f.extend_from_slice(&vd);
997 let mut vb = [0u8; 94];
998 vb[0..4].copy_from_slice(&1u32.to_le_bytes());
999 vb[4..8].copy_from_slice(&chunk_count.to_le_bytes());
1000 vb[8..12].copy_from_slice(&SECTORS_PER_CHUNK.to_le_bytes());
1001 vb[12..16].copy_from_slice(&BYTES_PER_SECTOR.to_le_bytes());
1002 vb[16..24].copy_from_slice(§or_count.to_le_bytes());
1003 f.extend_from_slice(&vb);
1004
1005 let emit = |f: &mut Vec<u8>, name: &[u8], next: u64, base: u64| {
1007 let mut td = [0u8; SECTION_DESCRIPTOR_SIZE];
1008 td[..name.len()].copy_from_slice(name);
1009 td[16..24].copy_from_slice(&next.to_le_bytes());
1010 td[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4).to_le_bytes());
1011 f.extend_from_slice(&td);
1012 let mut th = [0u8; 24];
1013 th[0..4].copy_from_slice(&1u32.to_le_bytes());
1014 th[8..16].copy_from_slice(&base.to_le_bytes());
1015 f.extend_from_slice(&th);
1016 f.extend_from_slice(&0u32.to_le_bytes()); };
1018 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];
1023 sd[..7].copy_from_slice(b"sectors");
1024 sd[16..24].copy_from_slice(&done_desc.to_le_bytes());
1025 sd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + chunk_len).to_le_bytes());
1026 f.extend_from_slice(&sd);
1027 f.extend_from_slice(§ors);
1028 f.extend_from_slice(&crc.to_le_bytes());
1029
1030 let mut dd = [0u8; SECTION_DESCRIPTOR_SIZE];
1031 dd[..4].copy_from_slice(b"done");
1032 dd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64).to_le_bytes());
1033 f.extend_from_slice(&dd);
1034 f
1035 }
1036
1037 #[test]
1038 fn table2_recovers_good_data_when_primary_out_of_range() {
1039 let img = build_uncompressed_table2_good(1, u64::from(SECTORS_PER_CHUNK));
1041 let (report, raw) = recover_bytes(&img);
1042 assert_eq!(report.chunks_recovered_table2, 1, "table2 must rescue");
1043 assert_eq!(report.chunks_recovered_primary, 0);
1044 assert_eq!(report.chunks_zero_filled, 0);
1045 assert_eq!(raw.len(), CHUNK_SIZE);
1046 assert!(raw.iter().all(|&b| b == 0x7E));
1047 }
1048
1049 #[test]
1050 fn table2_crc_flagged_when_primary_absent() {
1051 let mut img = build_uncompressed_table2_good(1, u64::from(SECTORS_PER_CHUNK));
1055 let crc_pos = img.len() - SECTION_DESCRIPTOR_SIZE - 4;
1058 for b in &mut img[crc_pos..crc_pos + 4] {
1059 *b ^= 0xFF;
1060 }
1061 let (report, raw) = recover_bytes(&img);
1062 assert_eq!(
1063 report.chunks_recovered_table2, 1,
1064 "table2 still supplies data"
1065 );
1066 assert_eq!(report.chunks_recovered_primary, 0);
1067 assert_eq!(
1068 report.chunks_zero_filled, 0,
1069 "present data is not zero-filled"
1070 );
1071 assert_eq!(
1072 report.chunks_crc_flagged, 1,
1073 "table2 data flagged CRC-suspect"
1074 );
1075 assert_eq!(report.crc_flagged_chunks, vec![0]);
1076 assert!(raw.iter().all(|&b| b == 0x7E));
1077 }
1078
1079 #[test]
1080 fn geometry_undercover_zero_fills_tail() {
1081 let img = build_uncompressed_table2_good(1, u64::from(SECTORS_PER_CHUNK) * 2);
1084 let (report, raw) = recover_bytes(&img);
1085 assert_eq!(report.image_size, (CHUNK_SIZE * 2) as u64);
1086 assert_eq!(raw.len(), CHUNK_SIZE * 2);
1087 assert!(raw[..CHUNK_SIZE].iter().all(|&b| b == 0x7E));
1089 assert!(raw[CHUNK_SIZE..].iter().all(|&b| b == 0));
1090 assert!(report.bytes_zero_filled >= CHUNK_SIZE as u64);
1091 }
1092
1093 #[test]
1094 fn walk_sections_breaks_on_next_zero_nonterminal() {
1095 let mut f = Vec::new();
1098 f.extend_from_slice(&EVF_SIGNATURE);
1099 f.push(0x01);
1100 f.extend_from_slice(&1u16.to_le_bytes());
1101 f.extend_from_slice(&0u16.to_le_bytes());
1102 let mut vd = [0u8; SECTION_DESCRIPTOR_SIZE];
1103 vd[..6].copy_from_slice(b"volume");
1104 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());
1106 f.extend_from_slice(&vd);
1107 f.extend_from_slice(&[0u8; 94]);
1108 let (sections, trunc) = walk_sections(&f);
1109 assert_eq!(sections.len(), 1);
1110 assert_eq!(trunc, None, "next==0 ends the chain, not a truncation");
1111 }
1112
1113 fn build_uncompressed_sized(chunk_body: &[u8], sector_count: u64) -> Vec<u8> {
1117 let mut f = Vec::new();
1118 f.extend_from_slice(&EVF_SIGNATURE);
1119 f.push(0x01);
1120 f.extend_from_slice(&1u16.to_le_bytes());
1121 f.extend_from_slice(&0u16.to_le_bytes());
1122
1123 let vol_desc = FILE_HEADER_SIZE as u64;
1124 let vol_data = vol_desc + SECTION_DESCRIPTOR_SIZE as u64;
1125 let tbl_desc = vol_data + 94;
1126 let sec_desc = tbl_desc + SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4;
1127 let sec_data = sec_desc + SECTION_DESCRIPTOR_SIZE as u64;
1128 let done_desc = sec_data + chunk_body.len() as u64;
1129
1130 let mut vd = [0u8; SECTION_DESCRIPTOR_SIZE];
1131 vd[..6].copy_from_slice(b"volume");
1132 vd[16..24].copy_from_slice(&tbl_desc.to_le_bytes());
1133 vd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 94).to_le_bytes());
1134 f.extend_from_slice(&vd);
1135 let mut vb = [0u8; 94];
1136 vb[0..4].copy_from_slice(&1u32.to_le_bytes());
1137 vb[4..8].copy_from_slice(&1u32.to_le_bytes()); vb[8..12].copy_from_slice(&SECTORS_PER_CHUNK.to_le_bytes());
1139 vb[12..16].copy_from_slice(&BYTES_PER_SECTOR.to_le_bytes());
1140 vb[16..24].copy_from_slice(§or_count.to_le_bytes());
1141 f.extend_from_slice(&vb);
1142
1143 let mut td = [0u8; SECTION_DESCRIPTOR_SIZE];
1144 td[..5].copy_from_slice(b"table");
1145 td[16..24].copy_from_slice(&sec_desc.to_le_bytes());
1146 td[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4).to_le_bytes());
1147 f.extend_from_slice(&td);
1148 let mut th = [0u8; 24];
1149 th[0..4].copy_from_slice(&1u32.to_le_bytes());
1150 th[8..16].copy_from_slice(&sec_data.to_le_bytes());
1151 f.extend_from_slice(&th);
1152 f.extend_from_slice(&0u32.to_le_bytes()); let mut sd = [0u8; SECTION_DESCRIPTOR_SIZE];
1155 sd[..7].copy_from_slice(b"sectors");
1156 sd[16..24].copy_from_slice(&done_desc.to_le_bytes());
1157 sd[24..32].copy_from_slice(
1158 &(SECTION_DESCRIPTOR_SIZE as u64 + chunk_body.len() as u64).to_le_bytes(),
1159 );
1160 f.extend_from_slice(&sd);
1161 f.extend_from_slice(chunk_body);
1162
1163 let mut dd = [0u8; SECTION_DESCRIPTOR_SIZE];
1164 dd[..4].copy_from_slice(b"done");
1165 dd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64).to_le_bytes());
1166 f.extend_from_slice(&dd);
1167 f
1168 }
1169
1170 #[test]
1171 fn chunk_longer_than_logical_is_truncated() {
1172 let body = vec![0x42u8; CHUNK_SIZE];
1176 let img = build_uncompressed_sized(&body, u64::from(SECTORS_PER_CHUNK) / 2);
1177 let (report, raw) = recover_bytes(&img);
1178 assert_eq!(report.image_size, (CHUNK_SIZE / 2) as u64);
1179 assert_eq!(raw.len(), CHUNK_SIZE / 2);
1180 assert!(raw.iter().all(|&b| b == 0x42));
1181 }
1182
1183 #[test]
1184 fn chunk_shorter_than_logical_is_padded() {
1185 let body = vec![0x24u8; 100];
1189 let img = build_uncompressed_sized(&body, u64::from(SECTORS_PER_CHUNK));
1190 let (report, raw) = recover_bytes(&img);
1191 assert_eq!(report.image_size, CHUNK_SIZE as u64);
1192 assert_eq!(raw.len(), CHUNK_SIZE);
1193 assert!(raw[..100].iter().all(|&b| b == 0x24));
1194 assert!(
1195 raw[100..].iter().all(|&b| b == 0),
1196 "short chunk zero-padded"
1197 );
1198 }
1199
1200 #[test]
1201 fn geometry_overcover_stops_at_image_size() {
1202 let img = build_uncompressed_table2_good(2, u64::from(SECTORS_PER_CHUNK));
1205 let (report, raw) = recover_bytes(&img);
1206 assert_eq!(report.image_size, CHUNK_SIZE as u64);
1207 assert_eq!(raw.len(), CHUNK_SIZE);
1208 assert_eq!(
1210 report.chunks_recovered_primary
1211 + report.chunks_recovered_table2
1212 + report.chunks_zero_filled,
1213 1
1214 );
1215 }
1216}