use std::fs::File;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use ewf::sections::{
adler32, EwfVolume, SectionDescriptor, TableEntry, TableHeader, EVF_SIGNATURE,
FILE_HEADER_SIZE, SECTION_DESCRIPTOR_SIZE, TABLE_HEADER_SIZE,
};
use flate2::read::ZlibDecoder;
use memmap2::Mmap;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct RecoveryReport {
pub image_size: u64,
pub chunk_size: u64,
pub chunks_total: usize,
pub chunks_recovered_primary: usize,
pub chunks_recovered_table2: usize,
pub chunks_zero_filled: usize,
pub chunks_crc_flagged: usize,
pub bytes_recovered: u64,
pub bytes_zero_filled: u64,
pub truncation_offset: Option<u64>,
pub lost_chunks: Vec<usize>,
pub crc_flagged_chunks: Vec<usize>,
}
pub struct EwfRecover {
segment_paths: Vec<PathBuf>,
}
impl EwfRecover {
#[must_use]
pub fn from_path(path: impl AsRef<Path>) -> Self {
Self {
segment_paths: discover_segments(path.as_ref()),
}
}
#[must_use]
pub fn from_paths(paths: &[impl AsRef<Path>]) -> Self {
Self {
segment_paths: paths.iter().map(|p| p.as_ref().to_path_buf()).collect(),
}
}
pub fn recover_to_raw(&self, out_path: impl AsRef<Path>) -> io::Result<RecoveryReport> {
if self.segment_paths.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"no EWF segments to recover",
));
}
let mmaps = self
.segment_paths
.iter()
.map(|p| {
let file = File::open(p)?;
#[allow(unsafe_code)]
unsafe {
Mmap::map(&file)
}
})
.collect::<io::Result<Vec<Mmap>>>()?;
let segments: Vec<&[u8]> = mmaps.iter().map(std::convert::AsRef::as_ref).collect();
recover_segments(&segments, out_path.as_ref())
}
}
struct Section {
type_name: String,
offset: u64,
size: u64,
}
struct Geometry {
chunk_count: u32,
sectors_per_chunk: u32,
bytes_per_sector: u32,
sector_count: u64,
}
fn walk_sections(data: &[u8]) -> (Vec<Section>, Option<u64>) {
let file_size = data.len() as u64;
let mut sections = Vec::new();
let mut pos = FILE_HEADER_SIZE as u64;
let mut truncation: Option<u64> = None;
loop {
let off = pos as usize;
if off.saturating_add(SECTION_DESCRIPTOR_SIZE) > data.len() {
if pos < file_size {
truncation = Some(pos);
}
break;
}
let raw = &data[off..off.saturating_add(SECTION_DESCRIPTOR_SIZE)];
let Ok(desc) = SectionDescriptor::parse(raw, pos) else {
truncation = Some(pos);
break;
};
let next = desc.next;
let section_size = desc.section_size;
let type_name = desc.section_type;
sections.push(Section {
type_name: type_name.clone(),
offset: pos,
size: section_size,
});
if type_name == "done" || type_name == "next" {
break;
}
if next == 0 || next <= pos {
break;
}
if next > file_size {
truncation = Some(next);
break;
}
pos = next;
}
(sections, truncation)
}
fn read_geometry(data: &[u8], sections: &[Section]) -> Option<Geometry> {
let vol = sections
.iter()
.find(|s| s.type_name == "volume" || s.type_name == "disk")?;
let data_start = (vol.offset as usize).saturating_add(SECTION_DESCRIPTOR_SIZE);
let body_len = (vol.size as usize).saturating_sub(SECTION_DESCRIPTOR_SIZE);
let vol_end = data_start.saturating_add(body_len).min(data.len());
let body = data.get(data_start..vol_end)?;
let parsed = EwfVolume::parse(body).ok()?;
if parsed.sectors_per_chunk == 0 || parsed.bytes_per_sector == 0 {
return None;
}
Some(Geometry {
chunk_count: parsed.chunk_count,
sectors_per_chunk: parsed.sectors_per_chunk,
bytes_per_sector: parsed.bytes_per_sector,
sector_count: parsed.sector_count,
})
}
struct TableRef {
entry_count: usize,
base_offset: u64,
entries_file_offset: usize,
}
fn table_ref(data: &[u8], sections: &[Section], name: &str) -> Option<TableRef> {
let sec = sections.iter().find(|s| s.type_name == name)?;
let hdr_start = (sec.offset as usize).saturating_add(SECTION_DESCRIPTOR_SIZE);
let hdr = data.get(hdr_start..hdr_start.saturating_add(TABLE_HEADER_SIZE))?;
let header = TableHeader::parse(hdr).ok()?;
Some(TableRef {
entry_count: header.entry_count as usize,
base_offset: header.base_offset,
entries_file_offset: hdr_start.saturating_add(TABLE_HEADER_SIZE),
})
}
fn sectors_data_end(sections: &[Section], data_len: usize) -> Option<usize> {
let sec = sections.iter().find(|s| s.type_name == "sectors")?;
Some((sec.offset.saturating_add(sec.size) as usize).min(data_len))
}
fn entry_at(data: &[u8], t: &TableRef, i: usize) -> Option<(bool, u64)> {
let off = t.entries_file_offset.saturating_add(i.saturating_mul(4));
let bytes = data.get(off..off.saturating_add(4))?;
let e = TableEntry::parse(bytes).ok()?;
Some((
e.compressed,
t.base_offset.saturating_add(u64::from(e.chunk_offset)),
))
}
fn chunk_range(
data: &[u8],
t: &TableRef,
i: usize,
sectors_end: Option<usize>,
) -> Option<(usize, usize, bool)> {
let (compressed, abs) = entry_at(data, t, i)?;
let start = abs as usize;
let end = if i.saturating_add(1) < t.entry_count {
let (_, next_abs) = entry_at(data, t, i.saturating_add(1))?;
next_abs as usize
} else {
sectors_end.unwrap_or(data.len())
};
if start >= end || end > data.len() {
return None;
}
Some((start, end, compressed))
}
fn decode_chunk(raw: &[u8], compressed: bool, chunk_size: usize) -> Option<(Vec<u8>, bool)> {
if compressed {
let mut out = Vec::with_capacity(chunk_size.min(raw.len().saturating_mul(4).max(1)));
let limit = (chunk_size as u64).saturating_add(1);
ZlibDecoder::new(raw)
.take(limit)
.read_to_end(&mut out)
.ok()?;
if out.is_empty() {
return None;
}
out.truncate(chunk_size);
Some((out, true))
} else {
let has_trailing_crc = raw.len() >= chunk_size.saturating_add(4);
let crc_ok = if has_trailing_crc {
let stored = u32::from_le_bytes([
raw[chunk_size],
raw[chunk_size + 1],
raw[chunk_size + 2],
raw[chunk_size + 3],
]);
adler32(&raw[..chunk_size]) == stored
} else {
true
};
let take = raw.len().min(chunk_size);
Some((raw[..take].to_vec(), crc_ok))
}
}
fn locate_chunk(seg_entry_counts: &[usize], idx: usize) -> Option<(usize, usize)> {
let mut running = 0usize;
for (seg_idx, &count) in seg_entry_counts.iter().enumerate() {
if idx < running.saturating_add(count) {
return Some((seg_idx, idx.saturating_sub(running)));
}
running = running.saturating_add(count);
}
None
}
fn recover_segments(segments: &[&[u8]], out_path: &Path) -> io::Result<RecoveryReport> {
let first = segments.first().copied().unwrap_or(&[]);
if first.len() < FILE_HEADER_SIZE || !first.starts_with(&EVF_SIGNATURE) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"not an EWF v1 image: first segment is {} bytes, signature {:02x?}",
first.len(),
first
.get(..FILE_HEADER_SIZE.min(first.len()))
.unwrap_or(&[])
),
));
}
let mut all_sections: Vec<Vec<Section>> = Vec::with_capacity(segments.len());
let mut truncation_offset: Option<u64> = None;
for seg in segments {
let (sections, trunc) = walk_sections(seg);
if truncation_offset.is_none() {
truncation_offset = trunc;
}
all_sections.push(sections);
}
let geom = read_geometry(first, &all_sections[0]).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"no parseable volume/disk section: cannot establish image geometry",
)
})?;
let chunk_size =
u64::from(geom.sectors_per_chunk).saturating_mul(u64::from(geom.bytes_per_sector));
let image_size = geom
.sector_count
.saturating_mul(u64::from(geom.bytes_per_sector));
let chunk_size_usize = chunk_size as usize;
let total_chunks = geom.chunk_count as usize;
let mut primary: Vec<Option<TableRef>> = Vec::with_capacity(segments.len());
let mut fallback: Vec<Option<TableRef>> = Vec::with_capacity(segments.len());
let mut sec_ends: Vec<Option<usize>> = Vec::with_capacity(segments.len());
let mut seg_entry_counts: Vec<usize> = Vec::with_capacity(segments.len());
for (seg, sections) in segments.iter().zip(all_sections.iter()) {
let p = table_ref(seg, sections, "table");
let f = table_ref(seg, sections, "table2");
let count = p.as_ref().or(f.as_ref()).map_or(0, |t| t.entry_count);
seg_entry_counts.push(count);
primary.push(p);
fallback.push(f);
sec_ends.push(sectors_data_end(sections, seg.len()));
}
let mut out = io::BufWriter::new(File::create(out_path)?);
let mut recovered_primary = 0usize;
let mut recovered_table2 = 0usize;
let mut zero_filled = 0usize;
let mut crc_flagged = 0usize;
let mut bytes_recovered = 0u64;
let mut bytes_zero_filled = 0u64;
let mut lost_chunks: Vec<usize> = Vec::new();
let mut crc_flagged_chunks: Vec<usize> = Vec::new();
let mut bytes_remaining = image_size;
let decode_from = |table: Option<&TableRef>, seg_idx: usize, local: usize| {
let seg = segments[seg_idx];
let sec_end = sec_ends[seg_idx];
table.and_then(|t| {
chunk_range(seg, t, local, sec_end)
.and_then(|(s, e, c)| decode_chunk(&seg[s..e], c, chunk_size_usize))
})
};
for idx in 0..total_chunks {
if bytes_remaining == 0 {
break;
}
let logical = bytes_remaining.min(chunk_size) as usize;
let decoded: Option<(Vec<u8>, bool, bool)> = match locate_chunk(&seg_entry_counts, idx) {
Some((seg_idx, local)) => {
let from_primary = decode_from(primary[seg_idx].as_ref(), seg_idx, local);
match from_primary {
Some((bytes, true)) => Some((bytes, false, true)),
other => {
let from_t2 = decode_from(fallback[seg_idx].as_ref(), seg_idx, local);
match (other, from_t2) {
(_, Some((bytes, true))) => Some((bytes, true, true)),
(Some((bytes, _)), _) => Some((bytes, false, false)),
(None, Some((bytes, false))) => Some((bytes, true, false)),
(None, None) => None,
}
}
}
}
None => None,
};
if let Some((mut bytes, via_table2, crc_ok)) = decoded {
if bytes.len() > logical {
bytes.truncate(logical);
} else if bytes.len() < logical {
bytes.resize(logical, 0);
}
out.write_all(&bytes)?;
bytes_recovered = bytes_recovered.saturating_add(logical as u64);
if via_table2 {
recovered_table2 = recovered_table2.saturating_add(1);
} else {
recovered_primary = recovered_primary.saturating_add(1);
}
if !crc_ok {
crc_flagged = crc_flagged.saturating_add(1);
crc_flagged_chunks.push(idx);
}
} else {
write_zeros(&mut out, logical)?;
zero_filled = zero_filled.saturating_add(1);
bytes_zero_filled = bytes_zero_filled.saturating_add(logical as u64);
lost_chunks.push(idx);
}
bytes_remaining = bytes_remaining.saturating_sub(logical as u64);
}
while bytes_remaining > 0 {
let logical = bytes_remaining.min(chunk_size) as usize;
write_zeros(&mut out, logical)?;
bytes_zero_filled = bytes_zero_filled.saturating_add(logical as u64);
bytes_remaining = bytes_remaining.saturating_sub(logical as u64);
}
out.flush()?;
Ok(RecoveryReport {
image_size,
chunk_size,
chunks_total: total_chunks,
chunks_recovered_primary: recovered_primary,
chunks_recovered_table2: recovered_table2,
chunks_zero_filled: zero_filled,
chunks_crc_flagged: crc_flagged,
bytes_recovered,
bytes_zero_filled,
truncation_offset,
lost_chunks,
crc_flagged_chunks,
})
}
fn write_zeros(w: &mut impl Write, n: usize) -> io::Result<()> {
const BLOCK: usize = 8 * 1024;
let zeros = [0u8; BLOCK];
let mut left = n;
while left > 0 {
let take = left.min(BLOCK);
w.write_all(&zeros[..take])?;
left = left.saturating_sub(take);
}
Ok(())
}
fn discover_segments(base: &Path) -> Vec<PathBuf> {
let Some(ext) = base.extension().and_then(|e| e.to_str()) else {
return vec![base.to_path_buf()];
};
let lower = ext.to_ascii_lowercase();
if lower.len() != 3
|| !lower.starts_with('e')
|| !lower[1..].chars().all(|c| c.is_ascii_digit())
{
return vec![base.to_path_buf()];
}
let upper = ext.chars().next().is_some_and(|c| c.is_ascii_uppercase());
let mut out = vec![base.to_path_buf()];
let mut n = 2u32;
loop {
let e = if upper {
format!("E{n:02}")
} else {
format!("e{n:02}")
};
let candidate = base.with_extension(&e);
if candidate.exists() {
out.push(candidate);
n = n.saturating_add(1);
} else {
break;
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use flate2::write::ZlibEncoder;
use flate2::Compression;
const CHUNK_SIZE: usize = 32768;
const SECTORS_PER_CHUNK: u32 = 64;
const BYTES_PER_SECTOR: u32 = 512;
fn build_compressed_e01(data: &[u8], corrupt_stream: bool, add_table2: bool) -> Vec<u8> {
let mut padded = data.to_vec();
padded.resize(CHUNK_SIZE, 0);
let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
enc.write_all(&padded).unwrap();
let mut compressed = enc.finish().unwrap();
if corrupt_stream {
let mid = compressed.len() / 2;
compressed[mid] ^= 0xFF;
}
let sector_count = u64::from(CHUNK_SIZE as u32 / BYTES_PER_SECTOR);
let mut f = Vec::new();
f.extend_from_slice(&EVF_SIGNATURE);
f.push(0x01);
f.extend_from_slice(&1u16.to_le_bytes());
f.extend_from_slice(&0u16.to_le_bytes());
let vol_desc = FILE_HEADER_SIZE as u64;
let vol_data = vol_desc + SECTION_DESCRIPTOR_SIZE as u64;
let tbl_desc = vol_data + 94;
let tbl_hdr = tbl_desc + SECTION_DESCRIPTOR_SIZE as u64;
let tbl_entries = tbl_hdr + 24;
let after_tbl = tbl_entries + 4;
let (tbl2_desc, tbl2_hdr, tbl2_entries, after_tbl2) = if add_table2 {
let d = after_tbl;
let h = d + SECTION_DESCRIPTOR_SIZE as u64;
let e = h + 24;
(Some(d), h, e, e + 4)
} else {
(None, 0, 0, after_tbl)
};
let sec_desc = after_tbl2;
let sec_data = sec_desc + SECTION_DESCRIPTOR_SIZE as u64;
let done_desc = sec_data + compressed.len() as u64;
let mut vd = [0u8; SECTION_DESCRIPTOR_SIZE];
vd[..6].copy_from_slice(b"volume");
vd[16..24].copy_from_slice(&tbl_desc.to_le_bytes());
vd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 94).to_le_bytes());
f.extend_from_slice(&vd);
let mut vb = [0u8; 94];
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());
vb[12..16].copy_from_slice(&BYTES_PER_SECTOR.to_le_bytes());
vb[16..24].copy_from_slice(§or_count.to_le_bytes());
f.extend_from_slice(&vb);
let emit_table = |f: &mut Vec<u8>, name: &[u8], next: u64| {
let mut td = [0u8; SECTION_DESCRIPTOR_SIZE];
td[..name.len()].copy_from_slice(name);
td[16..24].copy_from_slice(&next.to_le_bytes());
td[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4).to_le_bytes());
f.extend_from_slice(&td);
let mut th = [0u8; 24];
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);
f.extend_from_slice(&0x8000_0000u32.to_le_bytes()); };
emit_table(&mut f, b"table", tbl2_desc.unwrap_or(sec_desc));
if let Some(_d) = tbl2_desc {
emit_table(&mut f, b"table2", sec_desc);
}
let mut sd = [0u8; SECTION_DESCRIPTOR_SIZE];
sd[..7].copy_from_slice(b"sectors");
sd[16..24].copy_from_slice(&done_desc.to_le_bytes());
sd[24..32].copy_from_slice(
&(SECTION_DESCRIPTOR_SIZE as u64 + compressed.len() as u64).to_le_bytes(),
);
f.extend_from_slice(&sd);
f.extend_from_slice(&compressed);
let mut dd = [0u8; SECTION_DESCRIPTOR_SIZE];
dd[..4].copy_from_slice(b"done");
dd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64).to_le_bytes());
f.extend_from_slice(&dd);
let _ = (tbl2_hdr, tbl2_entries, after_tbl2);
f
}
fn recover_bytes(image: &[u8]) -> (RecoveryReport, Vec<u8>) {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("img.E01");
std::fs::write(&src, image).unwrap();
let out = dir.path().join("out.raw");
let report = EwfRecover::from_path(&src).recover_to_raw(&out).unwrap();
let raw = std::fs::read(&out).unwrap();
(report, raw)
}
#[test]
fn compressed_chunk_recovers() {
let img = build_compressed_e01(b"hello compressed world", false, false);
let (report, raw) = recover_bytes(&img);
assert_eq!(report.chunks_total, 1);
assert_eq!(report.chunks_recovered_primary, 1);
assert_eq!(report.chunks_zero_filled, 0);
assert_eq!(raw.len(), CHUNK_SIZE);
assert_eq!(&raw[..22], b"hello compressed world");
}
#[test]
fn corrupt_compressed_chunk_zero_fills() {
let img = build_compressed_e01(b"data that becomes garbage", true, false);
let (report, raw) = recover_bytes(&img);
assert_eq!(report.chunks_zero_filled, 1, "broken zlib must zero-fill");
assert_eq!(report.lost_chunks, vec![0]);
assert_eq!(raw.len(), CHUNK_SIZE);
assert!(raw.iter().all(|&b| b == 0), "lost chunk is all zeros");
}
#[test]
fn table2_recovers_when_primary_stream_broken() {
let img = build_compressed_e01(b"x", true, true);
let (report, _raw) = recover_bytes(&img);
assert_eq!(report.chunks_zero_filled, 1);
}
#[test]
fn table2_present_clean_recovers_from_primary() {
let img = build_compressed_e01(b"good data via primary", false, true);
let (report, raw) = recover_bytes(&img);
assert_eq!(report.chunks_recovered_primary, 1);
assert_eq!(report.chunks_recovered_table2, 0);
assert_eq!(&raw[..21], b"good data via primary");
}
#[test]
fn from_paths_and_empty_error() {
let img = build_compressed_e01(b"z", false, false);
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("explicit.E01");
std::fs::write(&p, &img).unwrap();
let out = dir.path().join("o.raw");
let r = EwfRecover::from_paths(&[&p]).recover_to_raw(&out).unwrap();
assert_eq!(r.chunks_total, 1);
let empty: [&Path; 0] = [];
let err = EwfRecover::from_paths(&empty)
.recover_to_raw(dir.path().join("none.raw"))
.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
}
#[test]
fn not_an_ewf_image_errors() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("garbage.bin");
std::fs::write(&p, b"not an ewf file at all").unwrap();
let err = EwfRecover::from_paths(&[&p])
.recover_to_raw(dir.path().join("o.raw"))
.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
#[test]
fn valid_signature_but_no_volume_errors() {
let mut f = Vec::new();
f.extend_from_slice(&EVF_SIGNATURE);
f.push(0x01);
f.extend_from_slice(&1u16.to_le_bytes());
f.extend_from_slice(&0u16.to_le_bytes());
let mut dd = [0u8; SECTION_DESCRIPTOR_SIZE];
dd[..4].copy_from_slice(b"done");
dd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64).to_le_bytes());
f.extend_from_slice(&dd);
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("novol.E01");
std::fs::write(&p, &f).unwrap();
let err = EwfRecover::from_paths(&[&p])
.recover_to_raw(dir.path().join("o.raw"))
.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
#[test]
fn walk_sections_flags_short_descriptor_truncation() {
let mut f = Vec::new();
f.extend_from_slice(&EVF_SIGNATURE);
f.push(0x01);
f.extend_from_slice(&1u16.to_le_bytes());
f.extend_from_slice(&0u16.to_le_bytes());
f.extend_from_slice(&[0u8; 10]); let (sections, trunc) = walk_sections(&f);
assert!(sections.is_empty());
assert_eq!(trunc, Some(FILE_HEADER_SIZE as u64));
}
#[test]
fn walk_sections_flags_next_past_eof() {
let mut f = Vec::new();
f.extend_from_slice(&EVF_SIGNATURE);
f.push(0x01);
f.extend_from_slice(&1u16.to_le_bytes());
f.extend_from_slice(&0u16.to_le_bytes());
let mut vd = [0u8; SECTION_DESCRIPTOR_SIZE];
vd[..6].copy_from_slice(b"volume");
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());
f.extend_from_slice(&vd);
let (sections, trunc) = walk_sections(&f);
assert_eq!(sections.len(), 1);
assert_eq!(trunc, Some(9_999_999));
}
#[test]
fn decode_uncompressed_bad_crc_still_emits() {
let mut raw = vec![0xABu8; CHUNK_SIZE];
raw.extend_from_slice(&0xDEAD_BEEFu32.to_le_bytes()); let (bytes, crc_ok) = decode_chunk(&raw, false, CHUNK_SIZE).unwrap();
assert_eq!(bytes.len(), CHUNK_SIZE);
assert!(!crc_ok);
}
#[test]
fn decode_uncompressed_good_crc_ok() {
let sectors = vec![0x5Au8; CHUNK_SIZE];
let crc = adler32(§ors);
let mut raw = sectors.clone();
raw.extend_from_slice(&crc.to_le_bytes());
let (bytes, crc_ok) = decode_chunk(&raw, false, CHUNK_SIZE).unwrap();
assert_eq!(bytes, sectors);
assert!(crc_ok);
}
#[test]
fn decode_uncompressed_short_final_chunk() {
let raw = vec![0x11u8; 100];
let (bytes, crc_ok) = decode_chunk(&raw, false, CHUNK_SIZE).unwrap();
assert_eq!(bytes.len(), 100);
assert!(crc_ok);
}
#[test]
fn locate_chunk_spans_segments() {
let counts = [3usize, 2, 4];
assert_eq!(locate_chunk(&counts, 0), Some((0, 0)));
assert_eq!(locate_chunk(&counts, 2), Some((0, 2)));
assert_eq!(locate_chunk(&counts, 3), Some((1, 0)));
assert_eq!(locate_chunk(&counts, 4), Some((1, 1)));
assert_eq!(locate_chunk(&counts, 5), Some((2, 0)));
assert_eq!(locate_chunk(&counts, 8), Some((2, 3)));
assert_eq!(locate_chunk(&counts, 9), None);
}
#[test]
fn discover_segments_non_ewf_extension_single() {
let p = Path::new("/tmp/whatever.bin");
assert_eq!(discover_segments(p), vec![p.to_path_buf()]);
}
#[test]
fn discover_segments_no_extension_single() {
let p = Path::new("/tmp/noext");
assert_eq!(discover_segments(p), vec![p.to_path_buf()]);
}
#[test]
fn discover_segments_lowercase_e01_single_when_no_siblings() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("img.e01");
std::fs::write(&p, b"x").unwrap();
assert_eq!(discover_segments(&p), vec![p]);
}
#[test]
fn read_geometry_rejects_zero_geometry() {
let mut f = Vec::new();
f.extend_from_slice(&EVF_SIGNATURE);
f.push(0x01);
f.extend_from_slice(&1u16.to_le_bytes());
f.extend_from_slice(&0u16.to_le_bytes());
let mut vd = [0u8; SECTION_DESCRIPTOR_SIZE];
vd[..6].copy_from_slice(b"volume");
let next = FILE_HEADER_SIZE as u64 + SECTION_DESCRIPTOR_SIZE as u64 + 94;
vd[16..24].copy_from_slice(&next.to_le_bytes());
vd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 94).to_le_bytes());
f.extend_from_slice(&vd);
let mut vb = [0u8; 94];
vb[0..4].copy_from_slice(&1u32.to_le_bytes());
vb[4..8].copy_from_slice(&1u32.to_le_bytes());
vb[12..16].copy_from_slice(&BYTES_PER_SECTOR.to_le_bytes());
f.extend_from_slice(&vb);
let (sections, _) = walk_sections(&f);
assert!(read_geometry(&f, §ions).is_none());
}
#[test]
fn chunk_range_out_of_bounds_is_none() {
let data = vec![0u8; 200];
let t = TableRef {
entry_count: 1,
base_offset: 10_000, entries_file_offset: 0,
};
let mut data = data;
data[0..4].copy_from_slice(&0x8000_0000u32.to_le_bytes());
assert!(chunk_range(&data, &t, 0, Some(200)).is_none());
}
#[test]
fn decode_compressed_empty_output_is_none() {
let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
enc.write_all(b"").unwrap();
let empty_stream = enc.finish().unwrap();
assert!(decode_chunk(&empty_stream, true, CHUNK_SIZE).is_none());
}
fn build_uncompressed_table2_good(chunk_count: u32, sector_count: u64) -> Vec<u8> {
let sectors = vec![0x7Eu8; CHUNK_SIZE];
let crc = adler32(§ors);
let mut f = Vec::new();
f.extend_from_slice(&EVF_SIGNATURE);
f.push(0x01);
f.extend_from_slice(&1u16.to_le_bytes());
f.extend_from_slice(&0u16.to_le_bytes());
let vol_desc = FILE_HEADER_SIZE as u64;
let vol_data = vol_desc + SECTION_DESCRIPTOR_SIZE as u64;
let tbl_desc = vol_data + 94;
let tbl2_desc = tbl_desc + SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4;
let sec_desc = tbl2_desc + SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4;
let sec_data = sec_desc + SECTION_DESCRIPTOR_SIZE as u64;
let chunk_len = CHUNK_SIZE as u64 + 4; let done_desc = sec_data + chunk_len;
let mut vd = [0u8; SECTION_DESCRIPTOR_SIZE];
vd[..6].copy_from_slice(b"volume");
vd[16..24].copy_from_slice(&tbl_desc.to_le_bytes());
vd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 94).to_le_bytes());
f.extend_from_slice(&vd);
let mut vb = [0u8; 94];
vb[0..4].copy_from_slice(&1u32.to_le_bytes());
vb[4..8].copy_from_slice(&chunk_count.to_le_bytes());
vb[8..12].copy_from_slice(&SECTORS_PER_CHUNK.to_le_bytes());
vb[12..16].copy_from_slice(&BYTES_PER_SECTOR.to_le_bytes());
vb[16..24].copy_from_slice(§or_count.to_le_bytes());
f.extend_from_slice(&vb);
let emit = |f: &mut Vec<u8>, name: &[u8], next: u64, base: u64| {
let mut td = [0u8; SECTION_DESCRIPTOR_SIZE];
td[..name.len()].copy_from_slice(name);
td[16..24].copy_from_slice(&next.to_le_bytes());
td[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4).to_le_bytes());
f.extend_from_slice(&td);
let mut th = [0u8; 24];
th[0..4].copy_from_slice(&1u32.to_le_bytes());
th[8..16].copy_from_slice(&base.to_le_bytes());
f.extend_from_slice(&th);
f.extend_from_slice(&0u32.to_le_bytes()); };
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];
sd[..7].copy_from_slice(b"sectors");
sd[16..24].copy_from_slice(&done_desc.to_le_bytes());
sd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + chunk_len).to_le_bytes());
f.extend_from_slice(&sd);
f.extend_from_slice(§ors);
f.extend_from_slice(&crc.to_le_bytes());
let mut dd = [0u8; SECTION_DESCRIPTOR_SIZE];
dd[..4].copy_from_slice(b"done");
dd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64).to_le_bytes());
f.extend_from_slice(&dd);
f
}
#[test]
fn table2_recovers_good_data_when_primary_out_of_range() {
let img = build_uncompressed_table2_good(1, u64::from(SECTORS_PER_CHUNK));
let (report, raw) = recover_bytes(&img);
assert_eq!(report.chunks_recovered_table2, 1, "table2 must rescue");
assert_eq!(report.chunks_recovered_primary, 0);
assert_eq!(report.chunks_zero_filled, 0);
assert_eq!(raw.len(), CHUNK_SIZE);
assert!(raw.iter().all(|&b| b == 0x7E));
}
#[test]
fn table2_crc_flagged_when_primary_absent() {
let mut img = build_uncompressed_table2_good(1, u64::from(SECTORS_PER_CHUNK));
let crc_pos = img.len() - SECTION_DESCRIPTOR_SIZE - 4;
for b in &mut img[crc_pos..crc_pos + 4] {
*b ^= 0xFF;
}
let (report, raw) = recover_bytes(&img);
assert_eq!(
report.chunks_recovered_table2, 1,
"table2 still supplies data"
);
assert_eq!(report.chunks_recovered_primary, 0);
assert_eq!(
report.chunks_zero_filled, 0,
"present data is not zero-filled"
);
assert_eq!(
report.chunks_crc_flagged, 1,
"table2 data flagged CRC-suspect"
);
assert_eq!(report.crc_flagged_chunks, vec![0]);
assert!(raw.iter().all(|&b| b == 0x7E));
}
#[test]
fn geometry_undercover_zero_fills_tail() {
let img = build_uncompressed_table2_good(1, u64::from(SECTORS_PER_CHUNK) * 2);
let (report, raw) = recover_bytes(&img);
assert_eq!(report.image_size, (CHUNK_SIZE * 2) as u64);
assert_eq!(raw.len(), CHUNK_SIZE * 2);
assert!(raw[..CHUNK_SIZE].iter().all(|&b| b == 0x7E));
assert!(raw[CHUNK_SIZE..].iter().all(|&b| b == 0));
assert!(report.bytes_zero_filled >= CHUNK_SIZE as u64);
}
#[test]
fn walk_sections_breaks_on_next_zero_nonterminal() {
let mut f = Vec::new();
f.extend_from_slice(&EVF_SIGNATURE);
f.push(0x01);
f.extend_from_slice(&1u16.to_le_bytes());
f.extend_from_slice(&0u16.to_le_bytes());
let mut vd = [0u8; SECTION_DESCRIPTOR_SIZE];
vd[..6].copy_from_slice(b"volume");
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());
f.extend_from_slice(&vd);
f.extend_from_slice(&[0u8; 94]);
let (sections, trunc) = walk_sections(&f);
assert_eq!(sections.len(), 1);
assert_eq!(trunc, None, "next==0 ends the chain, not a truncation");
}
fn build_uncompressed_sized(chunk_body: &[u8], sector_count: u64) -> Vec<u8> {
let mut f = Vec::new();
f.extend_from_slice(&EVF_SIGNATURE);
f.push(0x01);
f.extend_from_slice(&1u16.to_le_bytes());
f.extend_from_slice(&0u16.to_le_bytes());
let vol_desc = FILE_HEADER_SIZE as u64;
let vol_data = vol_desc + SECTION_DESCRIPTOR_SIZE as u64;
let tbl_desc = vol_data + 94;
let sec_desc = tbl_desc + SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4;
let sec_data = sec_desc + SECTION_DESCRIPTOR_SIZE as u64;
let done_desc = sec_data + chunk_body.len() as u64;
let mut vd = [0u8; SECTION_DESCRIPTOR_SIZE];
vd[..6].copy_from_slice(b"volume");
vd[16..24].copy_from_slice(&tbl_desc.to_le_bytes());
vd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 94).to_le_bytes());
f.extend_from_slice(&vd);
let mut vb = [0u8; 94];
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());
vb[12..16].copy_from_slice(&BYTES_PER_SECTOR.to_le_bytes());
vb[16..24].copy_from_slice(§or_count.to_le_bytes());
f.extend_from_slice(&vb);
let mut td = [0u8; SECTION_DESCRIPTOR_SIZE];
td[..5].copy_from_slice(b"table");
td[16..24].copy_from_slice(&sec_desc.to_le_bytes());
td[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4).to_le_bytes());
f.extend_from_slice(&td);
let mut th = [0u8; 24];
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);
f.extend_from_slice(&0u32.to_le_bytes());
let mut sd = [0u8; SECTION_DESCRIPTOR_SIZE];
sd[..7].copy_from_slice(b"sectors");
sd[16..24].copy_from_slice(&done_desc.to_le_bytes());
sd[24..32].copy_from_slice(
&(SECTION_DESCRIPTOR_SIZE as u64 + chunk_body.len() as u64).to_le_bytes(),
);
f.extend_from_slice(&sd);
f.extend_from_slice(chunk_body);
let mut dd = [0u8; SECTION_DESCRIPTOR_SIZE];
dd[..4].copy_from_slice(b"done");
dd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64).to_le_bytes());
f.extend_from_slice(&dd);
f
}
#[test]
fn chunk_longer_than_logical_is_truncated() {
let body = vec![0x42u8; CHUNK_SIZE];
let img = build_uncompressed_sized(&body, u64::from(SECTORS_PER_CHUNK) / 2);
let (report, raw) = recover_bytes(&img);
assert_eq!(report.image_size, (CHUNK_SIZE / 2) as u64);
assert_eq!(raw.len(), CHUNK_SIZE / 2);
assert!(raw.iter().all(|&b| b == 0x42));
}
#[test]
fn chunk_shorter_than_logical_is_padded() {
let body = vec![0x24u8; 100];
let img = build_uncompressed_sized(&body, u64::from(SECTORS_PER_CHUNK));
let (report, raw) = recover_bytes(&img);
assert_eq!(report.image_size, CHUNK_SIZE as u64);
assert_eq!(raw.len(), CHUNK_SIZE);
assert!(raw[..100].iter().all(|&b| b == 0x24));
assert!(
raw[100..].iter().all(|&b| b == 0),
"short chunk zero-padded"
);
}
#[test]
fn geometry_overcover_stops_at_image_size() {
let img = build_uncompressed_table2_good(2, u64::from(SECTORS_PER_CHUNK));
let (report, raw) = recover_bytes(&img);
assert_eq!(report.image_size, CHUNK_SIZE as u64);
assert_eq!(raw.len(), CHUNK_SIZE);
assert_eq!(
report.chunks_recovered_primary
+ report.chunks_recovered_table2
+ report.chunks_zero_filled,
1
);
}
}