mod error;
pub(crate) mod ewf2;
mod parse;
mod reader;
mod sections;
mod types;
pub use error::{EwfError, Result};
pub use parse::parse_error2_data;
pub use reader::EwfReader;
pub use sections::{EVF_SIGNATURE, EwfFileHeader, EwfVolume, SectionDescriptor, TableEntry};
pub use types::{AcquisitionError, EwfMetadata, StoredHashes};
#[cfg(feature = "verify")]
pub use types::VerifyResult;
#[cfg(test)]
mod tests {
use super::*;
use sections::{FILE_HEADER_SIZE, SECTION_DESCRIPTOR_SIZE};
use std::io::{Read, Seek, SeekFrom, Write};
use tempfile::NamedTempFile;
fn make_file_header(segment_number: u16) -> [u8; 13] {
let mut buf = [0u8; FILE_HEADER_SIZE];
buf[0..8].copy_from_slice(&EVF_SIGNATURE);
buf[8] = 0x01; buf[9..11].copy_from_slice(&segment_number.to_le_bytes());
buf[11] = 0x00; buf[12] = 0x00; buf
}
#[test]
fn parse_file_header_segment_1() {
let buf = make_file_header(1);
let header = EwfFileHeader::parse(&buf).unwrap();
assert_eq!(header.segment_number, 1);
}
#[test]
fn parse_file_header_segment_42() {
let buf = make_file_header(42);
let header = EwfFileHeader::parse(&buf).unwrap();
assert_eq!(header.segment_number, 42);
}
#[test]
fn parse_file_header_rejects_invalid_signature() {
let buf = [0u8; 13];
let result = EwfFileHeader::parse(&buf);
assert!(matches!(result, Err(EwfError::InvalidSignature)));
}
#[test]
fn parse_file_header_rejects_short_buffer() {
let buf = [0u8; 5];
let result = EwfFileHeader::parse(&buf);
assert!(matches!(result, Err(EwfError::BufferTooShort { .. })));
}
fn make_section_descriptor(section_type: &str, next: u64, section_size: u64) -> [u8; 76] {
let mut buf = [0u8; SECTION_DESCRIPTOR_SIZE];
let type_bytes = section_type.as_bytes();
buf[..type_bytes.len()].copy_from_slice(type_bytes);
buf[16..24].copy_from_slice(&next.to_le_bytes());
buf[24..32].copy_from_slice(§ion_size.to_le_bytes());
buf
}
#[test]
fn parse_section_descriptor_volume() {
let buf = make_section_descriptor("volume", 1000, 170);
let desc = SectionDescriptor::parse(&buf, 13).unwrap();
assert_eq!(desc.section_type, "volume");
assert_eq!(desc.next, 1000);
assert_eq!(desc.section_size, 170);
assert_eq!(desc.offset, 13);
}
#[test]
fn parse_section_descriptor_table() {
let buf = make_section_descriptor("table", 50000, 4096);
let desc = SectionDescriptor::parse(&buf, 200).unwrap();
assert_eq!(desc.section_type, "table");
assert_eq!(desc.next, 50000);
assert_eq!(desc.section_size, 4096);
}
#[test]
fn parse_section_descriptor_done() {
let buf = make_section_descriptor("done", 0, 76);
let desc = SectionDescriptor::parse(&buf, 9999).unwrap();
assert_eq!(desc.section_type, "done");
assert_eq!(desc.next, 0);
}
#[test]
fn parse_section_descriptor_rejects_short_buffer() {
let buf = [0u8; 10];
let result = SectionDescriptor::parse(&buf, 0);
assert!(matches!(result, Err(EwfError::BufferTooShort { .. })));
}
fn make_volume_data(
chunk_count: u32,
sectors_per_chunk: u32,
bytes_per_sector: u32,
sector_count: u64,
) -> [u8; 94] {
let mut buf = [0u8; 94];
buf[4..8].copy_from_slice(&chunk_count.to_le_bytes());
buf[8..12].copy_from_slice(§ors_per_chunk.to_le_bytes());
buf[12..16].copy_from_slice(&bytes_per_sector.to_le_bytes());
buf[16..24].copy_from_slice(§or_count.to_le_bytes());
buf
}
#[test]
fn parse_volume_typical() {
let buf = make_volume_data(1000, 64, 512, 64000);
let vol = EwfVolume::parse(&buf).unwrap();
assert_eq!(vol.chunk_count, 1000);
assert_eq!(vol.sectors_per_chunk, 64);
assert_eq!(vol.bytes_per_sector, 512);
assert_eq!(vol.sector_count, 64000);
assert_eq!(vol.chunk_size(), 32768); assert_eq!(vol.total_size(), 512 * 64000);
}
#[test]
fn parse_volume_rejects_short_buffer() {
let buf = [0u8; 10];
let result = EwfVolume::parse(&buf);
assert!(matches!(result, Err(EwfError::BufferTooShort { .. })));
}
#[test]
fn parse_table_entry_compressed() {
let val: u32 = 0x8000_1000;
let buf = val.to_le_bytes();
let entry = TableEntry::parse(&buf).unwrap();
assert!(entry.compressed);
assert_eq!(entry.chunk_offset, 0x1000);
}
#[test]
fn parse_table_entry_uncompressed() {
let val: u32 = 0x0000_2000;
let buf = val.to_le_bytes();
let entry = TableEntry::parse(&buf).unwrap();
assert!(!entry.compressed);
assert_eq!(entry.chunk_offset, 0x2000);
}
#[test]
fn parse_table_entry_rejects_short_buffer() {
let buf = [0u8; 2];
let result = TableEntry::parse(&buf);
assert!(matches!(result, Err(EwfError::BufferTooShort { .. })));
}
fn build_synthetic_e01(data: &[u8]) -> NamedTempFile {
use flate2::write::ZlibEncoder;
use flate2::Compression;
let chunk_size: u32 = 32768; let sectors_per_chunk: u32 = 64;
let bytes_per_sector: u32 = 512;
let mut padded = data.to_vec();
padded.resize(chunk_size as usize, 0);
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&padded).unwrap();
let compressed = encoder.finish().unwrap();
let sector_count = (chunk_size / bytes_per_sector) as u64;
let vol_desc_offset: u64 = FILE_HEADER_SIZE as u64; let vol_data_offset: u64 = vol_desc_offset + SECTION_DESCRIPTOR_SIZE as u64; let tbl_desc_offset: u64 = vol_data_offset + 94; let tbl_hdr_offset: u64 = tbl_desc_offset + SECTION_DESCRIPTOR_SIZE as u64; let tbl_entries_offset: u64 = tbl_hdr_offset + 24; let sectors_desc_offset: u64 = tbl_entries_offset + 4; let sectors_data_offset: u64 = sectors_desc_offset + SECTION_DESCRIPTOR_SIZE as u64; let done_desc_offset: u64 = sectors_data_offset + compressed.len() as u64;
let mut file_data = Vec::new();
file_data.extend_from_slice(&EVF_SIGNATURE);
file_data.push(0x01); file_data.extend_from_slice(&1u16.to_le_bytes()); file_data.extend_from_slice(&0u16.to_le_bytes());
let mut vol_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
vol_desc[..6].copy_from_slice(b"volume");
vol_desc[16..24].copy_from_slice(&tbl_desc_offset.to_le_bytes()); vol_desc[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 94).to_le_bytes()); file_data.extend_from_slice(&vol_desc);
let mut vol_data = [0u8; 94];
vol_data[0..4].copy_from_slice(&1u32.to_le_bytes());
vol_data[4..8].copy_from_slice(&1u32.to_le_bytes()); vol_data[8..12].copy_from_slice(§ors_per_chunk.to_le_bytes());
vol_data[12..16].copy_from_slice(&bytes_per_sector.to_le_bytes());
vol_data[16..24].copy_from_slice(§or_count.to_le_bytes());
file_data.extend_from_slice(&vol_data);
let mut tbl_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
tbl_desc[..5].copy_from_slice(b"table");
tbl_desc[16..24].copy_from_slice(§ors_desc_offset.to_le_bytes()); let tbl_section_size = SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4;
tbl_desc[24..32].copy_from_slice(&tbl_section_size.to_le_bytes());
file_data.extend_from_slice(&tbl_desc);
let mut tbl_hdr = [0u8; 24];
tbl_hdr[0..4].copy_from_slice(&1u32.to_le_bytes()); tbl_hdr[8..16].copy_from_slice(§ors_data_offset.to_le_bytes()); file_data.extend_from_slice(&tbl_hdr);
let entry: u32 = 0x8000_0000; file_data.extend_from_slice(&entry.to_le_bytes());
let mut sec_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
sec_desc[..7].copy_from_slice(b"sectors");
sec_desc[16..24].copy_from_slice(&done_desc_offset.to_le_bytes()); let sec_section_size = SECTION_DESCRIPTOR_SIZE as u64 + compressed.len() as u64;
sec_desc[24..32].copy_from_slice(&sec_section_size.to_le_bytes());
file_data.extend_from_slice(&sec_desc);
file_data.extend_from_slice(&compressed);
let mut done_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
done_desc[..4].copy_from_slice(b"done");
done_desc[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64).to_le_bytes());
file_data.extend_from_slice(&done_desc);
let mut tmp = tempfile::Builder::new().suffix(".E01").tempfile().unwrap();
tmp.write_all(&file_data).unwrap();
tmp.flush().unwrap();
tmp
}
#[test]
fn ewf_reader_opens_synthetic_e01() {
let data = b"Hello, forensic world!";
let tmp = build_synthetic_e01(data);
let reader = EwfReader::open(tmp.path()).unwrap();
assert_eq!(reader.chunk_size(), 32768);
assert_eq!(reader.chunk_count(), 1);
assert!(reader.total_size() > 0);
}
#[test]
fn ewf_reader_reads_first_bytes() {
let data = b"DEADBEEF_CAFEBABE_1234567890";
let tmp = build_synthetic_e01(data);
let mut reader = EwfReader::open(tmp.path()).unwrap();
let mut buf = vec![0u8; data.len()];
reader.read_exact(&mut buf).unwrap();
assert_eq!(&buf, data);
}
#[test]
fn ewf_reader_seek_and_read() {
let mut test_data = vec![0u8; 1024];
test_data[512..520].copy_from_slice(b"SEEKTEST");
let tmp = build_synthetic_e01(&test_data);
let mut reader = EwfReader::open(tmp.path()).unwrap();
reader.seek(SeekFrom::Start(512)).unwrap();
let mut buf = [0u8; 8];
reader.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"SEEKTEST");
}
#[test]
fn ewf_reader_seek_from_end() {
let data = b"test";
let tmp = build_synthetic_e01(data);
let mut reader = EwfReader::open(tmp.path()).unwrap();
let size = reader.total_size();
let pos = reader.seek(SeekFrom::End(-4)).unwrap();
assert_eq!(pos, size - 4);
}
#[test]
fn ewf_reader_read_returns_zero_at_eof() {
let data = b"short";
let tmp = build_synthetic_e01(data);
let mut reader = EwfReader::open(tmp.path()).unwrap();
reader.seek(SeekFrom::Start(reader.total_size())).unwrap();
let mut buf = [0u8; 10];
let n = reader.read(&mut buf).unwrap();
assert_eq!(n, 0);
}
fn build_synthetic_e01_with_table2(data: &[u8]) -> NamedTempFile {
use flate2::write::ZlibEncoder;
use flate2::Compression;
let chunk_size: u32 = 32768;
let sectors_per_chunk: u32 = 64;
let bytes_per_sector: u32 = 512;
let mut padded = data.to_vec();
padded.resize(chunk_size as usize, 0);
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&padded).unwrap();
let compressed = encoder.finish().unwrap();
let sector_count = (chunk_size / bytes_per_sector) as u64;
let vol_desc_offset: u64 = FILE_HEADER_SIZE as u64;
let vol_data_offset: u64 = vol_desc_offset + SECTION_DESCRIPTOR_SIZE as u64;
let tbl_desc_offset: u64 = vol_data_offset + 94;
let tbl_hdr_offset: u64 = tbl_desc_offset + SECTION_DESCRIPTOR_SIZE as u64;
let tbl_entries_offset: u64 = tbl_hdr_offset + 24;
let sectors_desc_offset: u64 = tbl_entries_offset + 4;
let sectors_data_offset: u64 = sectors_desc_offset + SECTION_DESCRIPTOR_SIZE as u64;
let done_desc_offset: u64 = sectors_data_offset + compressed.len() as u64;
let mut file_data = Vec::new();
file_data.extend_from_slice(&EVF_SIGNATURE);
file_data.push(0x01);
file_data.extend_from_slice(&1u16.to_le_bytes());
file_data.extend_from_slice(&0u16.to_le_bytes());
let mut vol_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
vol_desc[..6].copy_from_slice(b"volume");
vol_desc[16..24].copy_from_slice(&tbl_desc_offset.to_le_bytes());
vol_desc[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 94).to_le_bytes());
file_data.extend_from_slice(&vol_desc);
let mut vol_data = [0u8; 94];
vol_data[0..4].copy_from_slice(&1u32.to_le_bytes());
vol_data[4..8].copy_from_slice(&1u32.to_le_bytes());
vol_data[8..12].copy_from_slice(§ors_per_chunk.to_le_bytes());
vol_data[12..16].copy_from_slice(&bytes_per_sector.to_le_bytes());
vol_data[16..24].copy_from_slice(§or_count.to_le_bytes());
file_data.extend_from_slice(&vol_data);
let mut tbl_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
tbl_desc[..6].copy_from_slice(b"table2");
tbl_desc[16..24].copy_from_slice(§ors_desc_offset.to_le_bytes());
let tbl_section_size = SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4;
tbl_desc[24..32].copy_from_slice(&tbl_section_size.to_le_bytes());
file_data.extend_from_slice(&tbl_desc);
let mut tbl_hdr = [0u8; 24];
tbl_hdr[0..4].copy_from_slice(&1u32.to_le_bytes()); tbl_hdr[8..16].copy_from_slice(§ors_data_offset.to_le_bytes());
file_data.extend_from_slice(&tbl_hdr);
let entry: u32 = 0x8000_0000;
file_data.extend_from_slice(&entry.to_le_bytes());
let mut sec_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
sec_desc[..7].copy_from_slice(b"sectors");
sec_desc[16..24].copy_from_slice(&done_desc_offset.to_le_bytes());
let sec_section_size = SECTION_DESCRIPTOR_SIZE as u64 + compressed.len() as u64;
sec_desc[24..32].copy_from_slice(&sec_section_size.to_le_bytes());
file_data.extend_from_slice(&sec_desc);
file_data.extend_from_slice(&compressed);
let mut done_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
done_desc[..4].copy_from_slice(b"done");
done_desc[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64).to_le_bytes());
file_data.extend_from_slice(&done_desc);
let mut tmp = tempfile::Builder::new().suffix(".E01").tempfile().unwrap();
tmp.write_all(&file_data).unwrap();
tmp.flush().unwrap();
tmp
}
#[test]
fn ewf_reader_handles_table2_sections() {
let data = b"table2 section test data!";
let tmp = build_synthetic_e01_with_table2(data);
let mut reader = EwfReader::open(tmp.path()).unwrap();
assert_eq!(reader.chunk_count(), 1);
let mut buf = vec![0u8; data.len()];
reader.read_exact(&mut buf).unwrap();
assert_eq!(&buf, data);
}
#[test]
fn ewf_reader_skips_duplicate_table2() {
use flate2::write::ZlibEncoder;
use flate2::Compression;
let chunk_size: u32 = 32768;
let sectors_per_chunk: u32 = 64;
let bytes_per_sector: u32 = 512;
let mut padded = b"dedup test".to_vec();
padded.resize(chunk_size as usize, 0);
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&padded).unwrap();
let compressed = encoder.finish().unwrap();
let sector_count = (chunk_size / bytes_per_sector) as u64;
let vol_desc_off: u64 = 13;
let vol_data_off: u64 = vol_desc_off + 76;
let tbl_desc_off: u64 = vol_data_off + 94;
let tbl_hdr_off: u64 = tbl_desc_off + 76;
let tbl_entry_off: u64 = tbl_hdr_off + 24;
let tbl2_desc_off: u64 = tbl_entry_off + 4;
let tbl2_hdr_off: u64 = tbl2_desc_off + 76;
let tbl2_entry_off: u64 = tbl2_hdr_off + 24;
let sec_desc_off: u64 = tbl2_entry_off + 4;
let sec_data_off: u64 = sec_desc_off + 76;
let done_desc_off: u64 = sec_data_off + compressed.len() as u64;
let mut d = Vec::new();
d.extend_from_slice(&EVF_SIGNATURE);
d.push(0x01);
d.extend_from_slice(&1u16.to_le_bytes());
d.extend_from_slice(&0u16.to_le_bytes());
let mut vd = [0u8; 76];
vd[..6].copy_from_slice(b"volume");
vd[16..24].copy_from_slice(&tbl_desc_off.to_le_bytes());
vd[24..32].copy_from_slice(&(76u64 + 94).to_le_bytes());
d.extend_from_slice(&vd);
let mut vdata = [0u8; 94];
vdata[0..4].copy_from_slice(&1u32.to_le_bytes());
vdata[4..8].copy_from_slice(&1u32.to_le_bytes()); vdata[8..12].copy_from_slice(§ors_per_chunk.to_le_bytes());
vdata[12..16].copy_from_slice(&bytes_per_sector.to_le_bytes());
vdata[16..24].copy_from_slice(§or_count.to_le_bytes());
d.extend_from_slice(&vdata);
let mut td = [0u8; 76];
td[..5].copy_from_slice(b"table");
td[16..24].copy_from_slice(&tbl2_desc_off.to_le_bytes());
td[24..32].copy_from_slice(&(76u64 + 24 + 4).to_le_bytes());
d.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_off.to_le_bytes());
d.extend_from_slice(&th);
d.extend_from_slice(&0x8000_0000u32.to_le_bytes());
let mut td2 = [0u8; 76];
td2[..6].copy_from_slice(b"table2");
td2[16..24].copy_from_slice(&sec_desc_off.to_le_bytes());
td2[24..32].copy_from_slice(&(76u64 + 24 + 4).to_le_bytes());
d.extend_from_slice(&td2);
d.extend_from_slice(&th);
d.extend_from_slice(&0x8000_0000u32.to_le_bytes());
let mut sd = [0u8; 76];
sd[..7].copy_from_slice(b"sectors");
sd[16..24].copy_from_slice(&done_desc_off.to_le_bytes());
sd[24..32].copy_from_slice(&(76u64 + compressed.len() as u64).to_le_bytes());
d.extend_from_slice(&sd);
d.extend_from_slice(&compressed);
let mut dd = [0u8; 76];
dd[..4].copy_from_slice(b"done");
dd[24..32].copy_from_slice(&76u64.to_le_bytes());
d.extend_from_slice(&dd);
let mut tmp = tempfile::Builder::new().suffix(".E01").tempfile().unwrap();
tmp.write_all(&d).unwrap();
tmp.flush().unwrap();
let reader = EwfReader::open(tmp.path()).unwrap();
assert_eq!(reader.chunk_count(), 1, "table2 caused duplicate chunks");
}
fn build_synthetic_e01_with_nonzero_table_padding(data: &[u8]) -> NamedTempFile {
use flate2::write::ZlibEncoder;
use flate2::Compression;
let chunk_size: u32 = 32768;
let bytes_per_sector: u32 = 512;
let sectors_per_chunk: u32 = chunk_size / bytes_per_sector;
let sector_count = (chunk_size / bytes_per_sector) as u64;
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(data).unwrap();
encoder
.write_all(&vec![0u8; chunk_size as usize - data.len()])
.unwrap();
let compressed = encoder.finish().unwrap();
let vol_desc_off: u64 = FILE_HEADER_SIZE as u64;
let vol_data_off: u64 = vol_desc_off + SECTION_DESCRIPTOR_SIZE as u64;
let tbl_desc_off: u64 = vol_data_off + 94;
let tbl_hdr_off: u64 = tbl_desc_off + SECTION_DESCRIPTOR_SIZE as u64;
let tbl_entry_off: u64 = tbl_hdr_off + 24;
let sectors_desc_offset: u64 = tbl_entry_off + 4;
let sectors_data_offset: u64 = sectors_desc_offset + SECTION_DESCRIPTOR_SIZE as u64;
let done_desc_offset: u64 = sectors_data_offset + compressed.len() as u64;
let mut file_data = Vec::new();
let mut hdr = [0u8; FILE_HEADER_SIZE];
hdr[0..8].copy_from_slice(&EVF_SIGNATURE);
hdr[9..11].copy_from_slice(&1u16.to_le_bytes());
file_data.extend_from_slice(&hdr);
let mut vol_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
vol_desc[..6].copy_from_slice(b"volume");
vol_desc[16..24].copy_from_slice(&tbl_desc_off.to_le_bytes());
let vol_section_size = SECTION_DESCRIPTOR_SIZE as u64 + 94;
vol_desc[24..32].copy_from_slice(&vol_section_size.to_le_bytes());
file_data.extend_from_slice(&vol_desc);
let mut vol_data = [0u8; 94];
vol_data[4..8].copy_from_slice(&1u32.to_le_bytes());
vol_data[8..12].copy_from_slice(§ors_per_chunk.to_le_bytes());
vol_data[12..16].copy_from_slice(&bytes_per_sector.to_le_bytes());
vol_data[16..24].copy_from_slice(§or_count.to_le_bytes());
file_data.extend_from_slice(&vol_data);
let mut tbl_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
tbl_desc[..5].copy_from_slice(b"table");
tbl_desc[16..24].copy_from_slice(§ors_desc_offset.to_le_bytes());
let tbl_section_size = SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4;
tbl_desc[24..32].copy_from_slice(&tbl_section_size.to_le_bytes());
file_data.extend_from_slice(&tbl_desc);
let mut tbl_hdr = [0u8; 24];
tbl_hdr[0..4].copy_from_slice(&1u32.to_le_bytes()); tbl_hdr[4..8].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]); tbl_hdr[8..16].copy_from_slice(§ors_data_offset.to_le_bytes()); file_data.extend_from_slice(&tbl_hdr);
let entry: u32 = 0x8000_0000;
file_data.extend_from_slice(&entry.to_le_bytes());
let mut sec_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
sec_desc[..7].copy_from_slice(b"sectors");
sec_desc[16..24].copy_from_slice(&done_desc_offset.to_le_bytes());
let sec_section_size = SECTION_DESCRIPTOR_SIZE as u64 + compressed.len() as u64;
sec_desc[24..32].copy_from_slice(&sec_section_size.to_le_bytes());
file_data.extend_from_slice(&sec_desc);
file_data.extend_from_slice(&compressed);
let mut done_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
done_desc[..4].copy_from_slice(b"done");
file_data.extend_from_slice(&done_desc);
let mut tmp = tempfile::Builder::new().suffix(".E01").tempfile().unwrap();
tmp.write_all(&file_data).unwrap();
tmp.flush().unwrap();
tmp
}
#[test]
fn ewf_reader_handles_nonzero_table_padding() {
let data = b"nonzero padding in table header";
let tmp = build_synthetic_e01_with_nonzero_table_padding(data);
let mut reader = EwfReader::open(tmp.path()).unwrap();
assert_eq!(reader.chunk_count(), 1);
let mut buf = vec![0u8; data.len()];
reader.read_exact(&mut buf).unwrap();
assert_eq!(&buf, data);
}
fn build_synthetic_e01_truncated_chain(data: &[u8]) -> NamedTempFile {
use flate2::write::ZlibEncoder;
use flate2::Compression;
let chunk_size: u32 = 32768;
let bytes_per_sector: u32 = 512;
let sectors_per_chunk: u32 = chunk_size / bytes_per_sector;
let sector_count = (chunk_size / bytes_per_sector) as u64;
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(data).unwrap();
encoder
.write_all(&vec![0u8; chunk_size as usize - data.len()])
.unwrap();
let compressed = encoder.finish().unwrap();
let vol_desc_off: u64 = FILE_HEADER_SIZE as u64;
let vol_data_off: u64 = vol_desc_off + SECTION_DESCRIPTOR_SIZE as u64;
let tbl_desc_off: u64 = vol_data_off + 94;
let tbl_hdr_off: u64 = tbl_desc_off + SECTION_DESCRIPTOR_SIZE as u64;
let tbl_entry_off: u64 = tbl_hdr_off + 24;
let sectors_desc_offset: u64 = tbl_entry_off + 4;
let sectors_data_offset: u64 = sectors_desc_offset + SECTION_DESCRIPTOR_SIZE as u64;
let mut file_data = Vec::new();
let mut hdr = [0u8; FILE_HEADER_SIZE];
hdr[0..8].copy_from_slice(&EVF_SIGNATURE);
hdr[9..11].copy_from_slice(&1u16.to_le_bytes());
file_data.extend_from_slice(&hdr);
let mut vol_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
vol_desc[..6].copy_from_slice(b"volume");
vol_desc[16..24].copy_from_slice(&tbl_desc_off.to_le_bytes());
let vol_section_size = SECTION_DESCRIPTOR_SIZE as u64 + 94;
vol_desc[24..32].copy_from_slice(&vol_section_size.to_le_bytes());
file_data.extend_from_slice(&vol_desc);
let mut vol_data = [0u8; 94];
vol_data[4..8].copy_from_slice(&1u32.to_le_bytes());
vol_data[8..12].copy_from_slice(§ors_per_chunk.to_le_bytes());
vol_data[12..16].copy_from_slice(&bytes_per_sector.to_le_bytes());
vol_data[16..24].copy_from_slice(§or_count.to_le_bytes());
file_data.extend_from_slice(&vol_data);
let mut tbl_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
tbl_desc[..5].copy_from_slice(b"table");
tbl_desc[16..24].copy_from_slice(§ors_desc_offset.to_le_bytes());
let tbl_section_size = SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4;
tbl_desc[24..32].copy_from_slice(&tbl_section_size.to_le_bytes());
file_data.extend_from_slice(&tbl_desc);
let mut tbl_hdr = [0u8; 24];
tbl_hdr[0..4].copy_from_slice(&1u32.to_le_bytes());
tbl_hdr[8..16].copy_from_slice(§ors_data_offset.to_le_bytes());
file_data.extend_from_slice(&tbl_hdr);
let entry: u32 = 0x8000_0000;
file_data.extend_from_slice(&entry.to_le_bytes());
let mut sec_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
sec_desc[..7].copy_from_slice(b"sectors");
let bogus_next: u64 = 999_999_999; sec_desc[16..24].copy_from_slice(&bogus_next.to_le_bytes());
let sec_section_size = SECTION_DESCRIPTOR_SIZE as u64 + compressed.len() as u64;
sec_desc[24..32].copy_from_slice(&sec_section_size.to_le_bytes());
file_data.extend_from_slice(&sec_desc);
file_data.extend_from_slice(&compressed);
let mut tmp = tempfile::Builder::new().suffix(".E01").tempfile().unwrap();
tmp.write_all(&file_data).unwrap();
tmp.flush().unwrap();
tmp
}
#[test]
fn ewf_reader_handles_truncated_section_chain() {
let data = b"truncated chain test data!!!";
let tmp = build_synthetic_e01_truncated_chain(data);
let mut reader = EwfReader::open(tmp.path()).unwrap();
assert_eq!(reader.chunk_count(), 1);
let mut buf = vec![0u8; data.len()];
reader.read_exact(&mut buf).unwrap();
assert_eq!(&buf, data);
}
#[test]
fn ewf_reader_open_with_cache_size() {
let data = b"cache size test";
let tmp = build_synthetic_e01(data);
let reader = EwfReader::open_with_cache_size(tmp.path(), 10).unwrap();
assert_eq!(reader.chunk_count(), 1);
}
#[test]
#[ignore]
fn ewf_reader_opens_real_e01() {
let path = std::path::Path::new(
"../usnjrnl-forensic/tests/data/20200918_0417_DESKTOP-SDN1RPT.E01",
);
if !path.exists() {
panic!("Test image not found at {}", path.display());
}
let mut reader = EwfReader::open(path).unwrap();
assert!(reader.total_size() > 0);
eprintln!(
"Image size: {} bytes ({:.2} GB)",
reader.total_size(),
reader.total_size() as f64 / 1_073_741_824.0
);
eprintln!("Chunk size: {} bytes", reader.chunk_size());
eprintln!("Chunk count: {}", reader.chunk_count());
let mut sector = [0u8; 512];
reader.read_exact(&mut sector).unwrap();
assert_eq!(sector[510], 0x55);
assert_eq!(sector[511], 0xAA);
eprintln!("MBR signature verified: 0x55AA");
}
#[test]
fn discover_segments_no_segments_found() {
let result = EwfReader::open("/tmp/nonexistent_ewf_xyzzy.E01");
assert!(result.is_err());
match result.unwrap_err() {
EwfError::NoSegments(_) => {}
other => panic!("expected NoSegments, got {other:?}"),
}
}
#[test]
fn open_segments_empty_path_list() {
let result = EwfReader::open_segments(&[]);
assert!(matches!(result, Err(EwfError::NoSegments(ref msg)) if msg == "empty path list"));
}
#[test]
fn open_segments_segment_gap() {
let data = b"test data";
let tmp1 = build_synthetic_e01(data);
let mut file_data = std::fs::read(tmp1.path()).unwrap();
file_data[9..11].copy_from_slice(&3u16.to_le_bytes()); let mut tmp3 = tempfile::Builder::new().suffix(".E01").tempfile().unwrap();
tmp3.write_all(&file_data).unwrap();
tmp3.flush().unwrap();
let result = EwfReader::open_segments(&[tmp1.path().into(), tmp3.path().into()]);
assert!(matches!(
result,
Err(EwfError::SegmentGap {
expected: 2,
got: 3
})
));
}
#[test]
fn open_missing_volume_section() {
let mut file_data = Vec::new();
file_data.extend_from_slice(&EVF_SIGNATURE);
file_data.push(0x01);
file_data.extend_from_slice(&1u16.to_le_bytes());
file_data.extend_from_slice(&0u16.to_le_bytes());
let mut done_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
done_desc[..4].copy_from_slice(b"done");
done_desc[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64).to_le_bytes());
file_data.extend_from_slice(&done_desc);
let mut tmp = tempfile::Builder::new().suffix(".E01").tempfile().unwrap();
tmp.write_all(&file_data).unwrap();
tmp.flush().unwrap();
let result = EwfReader::open(tmp.path());
assert!(matches!(result, Err(EwfError::MissingVolume)));
}
#[test]
fn ewf_reader_seek_from_current() {
let data = b"ABCDEFGHIJKLMNOP";
let tmp = build_synthetic_e01(data);
let mut reader = EwfReader::open(tmp.path()).unwrap();
reader.seek(SeekFrom::Start(4)).unwrap();
let pos = reader.seek(SeekFrom::Current(4)).unwrap();
assert_eq!(pos, 8);
let mut buf = [0u8; 4];
reader.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"IJKL");
}
#[test]
fn ewf_reader_seek_negative_position() {
let data = b"test";
let tmp = build_synthetic_e01(data);
let mut reader = EwfReader::open(tmp.path()).unwrap();
let result = reader.seek(SeekFrom::Current(-1));
assert!(result.is_err());
}
#[test]
fn ewf_reader_cache_hit() {
let data = b"cached data test";
let tmp = build_synthetic_e01(data);
let mut reader = EwfReader::open(tmp.path()).unwrap();
let mut buf1 = [0u8; 16];
reader.read_exact(&mut buf1).unwrap();
reader.seek(SeekFrom::Start(0)).unwrap();
let mut buf2 = [0u8; 16];
reader.read_exact(&mut buf2).unwrap();
assert_eq!(buf1, buf2);
assert_eq!(&buf1[..16], b"cached data test");
}
#[test]
fn ewf_reader_uncompressed_chunk() {
let chunk_size: u32 = 32768;
let sectors_per_chunk: u32 = 64;
let bytes_per_sector: u32 = 512;
let mut padded = b"uncompressed chunk data".to_vec();
padded.resize(chunk_size as usize, 0);
let sector_count = (chunk_size / bytes_per_sector) as u64;
let vol_desc_offset: u64 = FILE_HEADER_SIZE as u64;
let vol_data_offset: u64 = vol_desc_offset + SECTION_DESCRIPTOR_SIZE as u64;
let tbl_desc_offset: u64 = vol_data_offset + 94;
let tbl_hdr_offset: u64 = tbl_desc_offset + SECTION_DESCRIPTOR_SIZE as u64;
let tbl_entries_offset: u64 = tbl_hdr_offset + 24;
let sectors_desc_offset: u64 = tbl_entries_offset + 4;
let sectors_data_offset: u64 = sectors_desc_offset + SECTION_DESCRIPTOR_SIZE as u64;
let done_desc_offset: u64 = sectors_data_offset + chunk_size as u64;
let mut file_data = Vec::new();
file_data.extend_from_slice(&EVF_SIGNATURE);
file_data.push(0x01);
file_data.extend_from_slice(&1u16.to_le_bytes());
file_data.extend_from_slice(&0u16.to_le_bytes());
let mut vol_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
vol_desc[..6].copy_from_slice(b"volume");
vol_desc[16..24].copy_from_slice(&tbl_desc_offset.to_le_bytes());
vol_desc[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 94).to_le_bytes());
file_data.extend_from_slice(&vol_desc);
let mut vol_data = [0u8; 94];
vol_data[0..4].copy_from_slice(&1u32.to_le_bytes());
vol_data[4..8].copy_from_slice(&1u32.to_le_bytes());
vol_data[8..12].copy_from_slice(§ors_per_chunk.to_le_bytes());
vol_data[12..16].copy_from_slice(&bytes_per_sector.to_le_bytes());
vol_data[16..24].copy_from_slice(§or_count.to_le_bytes());
file_data.extend_from_slice(&vol_data);
let mut tbl_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
tbl_desc[..5].copy_from_slice(b"table");
tbl_desc[16..24].copy_from_slice(§ors_desc_offset.to_le_bytes());
tbl_desc[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4).to_le_bytes());
file_data.extend_from_slice(&tbl_desc);
let mut tbl_hdr = [0u8; 24];
tbl_hdr[0..4].copy_from_slice(&1u32.to_le_bytes());
tbl_hdr[8..16].copy_from_slice(§ors_data_offset.to_le_bytes());
file_data.extend_from_slice(&tbl_hdr);
let entry: u32 = 0x0000_0000; file_data.extend_from_slice(&entry.to_le_bytes());
let mut sec_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
sec_desc[..7].copy_from_slice(b"sectors");
sec_desc[16..24].copy_from_slice(&done_desc_offset.to_le_bytes());
sec_desc[24..32]
.copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + chunk_size as u64).to_le_bytes());
file_data.extend_from_slice(&sec_desc);
file_data.extend_from_slice(&padded);
let mut done_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
done_desc[..4].copy_from_slice(b"done");
done_desc[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64).to_le_bytes());
file_data.extend_from_slice(&done_desc);
let mut tmp = tempfile::Builder::new().suffix(".E01").tempfile().unwrap();
tmp.write_all(&file_data).unwrap();
tmp.flush().unwrap();
let mut reader = EwfReader::open(tmp.path()).unwrap();
let expected = b"uncompressed chunk data";
let mut buf = vec![0u8; expected.len()];
reader.read_exact(&mut buf).unwrap();
assert_eq!(buf, expected);
}
#[test]
fn ewf_reader_decompression_error() {
let chunk_size: u32 = 32768;
let sectors_per_chunk: u32 = 64;
let bytes_per_sector: u32 = 512;
let garbage = b"THIS IS NOT VALID ZLIB DATA!!!!";
let sector_count = (chunk_size / bytes_per_sector) as u64;
let vol_desc_offset: u64 = FILE_HEADER_SIZE as u64;
let vol_data_offset: u64 = vol_desc_offset + SECTION_DESCRIPTOR_SIZE as u64;
let tbl_desc_offset: u64 = vol_data_offset + 94;
let tbl_hdr_offset: u64 = tbl_desc_offset + SECTION_DESCRIPTOR_SIZE as u64;
let tbl_entries_offset: u64 = tbl_hdr_offset + 24;
let sectors_desc_offset: u64 = tbl_entries_offset + 4;
let sectors_data_offset: u64 = sectors_desc_offset + SECTION_DESCRIPTOR_SIZE as u64;
let done_desc_offset: u64 = sectors_data_offset + garbage.len() as u64;
let mut file_data = Vec::new();
file_data.extend_from_slice(&EVF_SIGNATURE);
file_data.push(0x01);
file_data.extend_from_slice(&1u16.to_le_bytes());
file_data.extend_from_slice(&0u16.to_le_bytes());
let mut vol_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
vol_desc[..6].copy_from_slice(b"volume");
vol_desc[16..24].copy_from_slice(&tbl_desc_offset.to_le_bytes());
vol_desc[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 94).to_le_bytes());
file_data.extend_from_slice(&vol_desc);
let mut vol_data = [0u8; 94];
vol_data[0..4].copy_from_slice(&1u32.to_le_bytes());
vol_data[4..8].copy_from_slice(&1u32.to_le_bytes());
vol_data[8..12].copy_from_slice(§ors_per_chunk.to_le_bytes());
vol_data[12..16].copy_from_slice(&bytes_per_sector.to_le_bytes());
vol_data[16..24].copy_from_slice(§or_count.to_le_bytes());
file_data.extend_from_slice(&vol_data);
let mut tbl_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
tbl_desc[..5].copy_from_slice(b"table");
tbl_desc[16..24].copy_from_slice(§ors_desc_offset.to_le_bytes());
tbl_desc[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4).to_le_bytes());
file_data.extend_from_slice(&tbl_desc);
let mut tbl_hdr = [0u8; 24];
tbl_hdr[0..4].copy_from_slice(&1u32.to_le_bytes());
tbl_hdr[8..16].copy_from_slice(§ors_data_offset.to_le_bytes());
file_data.extend_from_slice(&tbl_hdr);
let entry: u32 = 0x8000_0000;
file_data.extend_from_slice(&entry.to_le_bytes());
let mut sec_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
sec_desc[..7].copy_from_slice(b"sectors");
sec_desc[16..24].copy_from_slice(&done_desc_offset.to_le_bytes());
sec_desc[24..32].copy_from_slice(
&(SECTION_DESCRIPTOR_SIZE as u64 + garbage.len() as u64).to_le_bytes(),
);
file_data.extend_from_slice(&sec_desc);
file_data.extend_from_slice(garbage);
let mut done_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
done_desc[..4].copy_from_slice(b"done");
done_desc[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64).to_le_bytes());
file_data.extend_from_slice(&done_desc);
let mut tmp = tempfile::Builder::new().suffix(".E01").tempfile().unwrap();
tmp.write_all(&file_data).unwrap();
tmp.flush().unwrap();
let mut reader = EwfReader::open(tmp.path()).unwrap();
let mut buf = [0u8; 512];
let result = reader.read(&mut buf);
assert!(
result.is_err() || {
false
}
);
}
#[test]
fn ewf_reader_volume_with_zero_total_size() {
use flate2::write::ZlibEncoder;
use flate2::Compression;
let chunk_size: u32 = 32768;
let sectors_per_chunk: u32 = 64;
let bytes_per_sector: u32 = 512;
let mut padded = b"zero total size test".to_vec();
padded.resize(chunk_size as usize, 0);
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&padded).unwrap();
let compressed = encoder.finish().unwrap();
let vol_desc_offset: u64 = FILE_HEADER_SIZE as u64;
let vol_data_offset: u64 = vol_desc_offset + SECTION_DESCRIPTOR_SIZE as u64;
let tbl_desc_offset: u64 = vol_data_offset + 94;
let tbl_hdr_offset: u64 = tbl_desc_offset + SECTION_DESCRIPTOR_SIZE as u64;
let tbl_entries_offset: u64 = tbl_hdr_offset + 24;
let sectors_desc_offset: u64 = tbl_entries_offset + 4;
let sectors_data_offset: u64 = sectors_desc_offset + SECTION_DESCRIPTOR_SIZE as u64;
let done_desc_offset: u64 = sectors_data_offset + compressed.len() as u64;
let mut file_data = Vec::new();
file_data.extend_from_slice(&EVF_SIGNATURE);
file_data.push(0x01);
file_data.extend_from_slice(&1u16.to_le_bytes());
file_data.extend_from_slice(&0u16.to_le_bytes());
let mut vol_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
vol_desc[..6].copy_from_slice(b"volume");
vol_desc[16..24].copy_from_slice(&tbl_desc_offset.to_le_bytes());
vol_desc[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 94).to_le_bytes());
file_data.extend_from_slice(&vol_desc);
let mut vol_data = [0u8; 94];
vol_data[0..4].copy_from_slice(&1u32.to_le_bytes()); vol_data[4..8].copy_from_slice(&1u32.to_le_bytes()); vol_data[8..12].copy_from_slice(§ors_per_chunk.to_le_bytes());
vol_data[12..16].copy_from_slice(&bytes_per_sector.to_le_bytes());
vol_data[16..24].copy_from_slice(&0u64.to_le_bytes()); file_data.extend_from_slice(&vol_data);
let mut tbl_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
tbl_desc[..5].copy_from_slice(b"table");
tbl_desc[16..24].copy_from_slice(§ors_desc_offset.to_le_bytes());
tbl_desc[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4).to_le_bytes());
file_data.extend_from_slice(&tbl_desc);
let mut tbl_hdr = [0u8; 24];
tbl_hdr[0..4].copy_from_slice(&1u32.to_le_bytes());
tbl_hdr[8..16].copy_from_slice(§ors_data_offset.to_le_bytes());
file_data.extend_from_slice(&tbl_hdr);
let entry: u32 = 0x8000_0000;
file_data.extend_from_slice(&entry.to_le_bytes());
let mut sec_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
sec_desc[..7].copy_from_slice(b"sectors");
sec_desc[16..24].copy_from_slice(&done_desc_offset.to_le_bytes());
sec_desc[24..32].copy_from_slice(
&(SECTION_DESCRIPTOR_SIZE as u64 + compressed.len() as u64).to_le_bytes(),
);
file_data.extend_from_slice(&sec_desc);
file_data.extend_from_slice(&compressed);
let mut done_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
done_desc[..4].copy_from_slice(b"done");
done_desc[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64).to_le_bytes());
file_data.extend_from_slice(&done_desc);
let mut tmp = tempfile::Builder::new().suffix(".E01").tempfile().unwrap();
tmp.write_all(&file_data).unwrap();
tmp.flush().unwrap();
let reader = EwfReader::open(tmp.path()).unwrap();
assert_eq!(reader.total_size(), 32768);
}
#[test]
fn ewf_reader_read_at_eof_returns_zero() {
let data = b"edge case";
let tmp = build_synthetic_e01(data);
let mut reader = EwfReader::open(tmp.path()).unwrap();
reader.seek(SeekFrom::Start(reader.total_size())).unwrap();
let mut buf = [0u8; 16];
let n = reader.read(&mut buf).unwrap();
assert_eq!(n, 0);
}
fn build_synthetic_e01_two_chunks(data1: &[u8], data2: &[u8]) -> NamedTempFile {
use flate2::write::ZlibEncoder;
use flate2::Compression;
let chunk_size: u32 = 32768;
let sectors_per_chunk: u32 = 64;
let bytes_per_sector: u32 = 512;
let mut padded1 = data1.to_vec();
padded1.resize(chunk_size as usize, 0);
let mut enc1 = ZlibEncoder::new(Vec::new(), Compression::default());
enc1.write_all(&padded1).unwrap();
let compressed1 = enc1.finish().unwrap();
let mut padded2 = data2.to_vec();
padded2.resize(chunk_size as usize, 0);
let mut enc2 = ZlibEncoder::new(Vec::new(), Compression::default());
enc2.write_all(&padded2).unwrap();
let compressed2 = enc2.finish().unwrap();
let total_compressed = compressed1.len() + compressed2.len();
let sector_count = (chunk_size as u64 * 2) / bytes_per_sector as u64;
let vol_desc_offset: u64 = FILE_HEADER_SIZE as u64;
let vol_data_offset: u64 = vol_desc_offset + SECTION_DESCRIPTOR_SIZE as u64;
let tbl_desc_offset: u64 = vol_data_offset + 94;
let tbl_hdr_offset: u64 = tbl_desc_offset + SECTION_DESCRIPTOR_SIZE as u64;
let tbl_entries_offset: u64 = tbl_hdr_offset + 24;
let sectors_desc_offset: u64 = tbl_entries_offset + 8; let sectors_data_offset: u64 = sectors_desc_offset + SECTION_DESCRIPTOR_SIZE as u64;
let done_desc_offset: u64 = sectors_data_offset + total_compressed as u64;
let mut file_data = Vec::new();
file_data.extend_from_slice(&EVF_SIGNATURE);
file_data.push(0x01);
file_data.extend_from_slice(&1u16.to_le_bytes());
file_data.extend_from_slice(&0u16.to_le_bytes());
let mut vol_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
vol_desc[..6].copy_from_slice(b"volume");
vol_desc[16..24].copy_from_slice(&tbl_desc_offset.to_le_bytes());
vol_desc[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 94).to_le_bytes());
file_data.extend_from_slice(&vol_desc);
let mut vol_data = [0u8; 94];
vol_data[0..4].copy_from_slice(&1u32.to_le_bytes());
vol_data[4..8].copy_from_slice(&2u32.to_le_bytes()); vol_data[8..12].copy_from_slice(§ors_per_chunk.to_le_bytes());
vol_data[12..16].copy_from_slice(&bytes_per_sector.to_le_bytes());
vol_data[16..24].copy_from_slice(§or_count.to_le_bytes());
file_data.extend_from_slice(&vol_data);
let mut tbl_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
tbl_desc[..5].copy_from_slice(b"table");
tbl_desc[16..24].copy_from_slice(§ors_desc_offset.to_le_bytes());
tbl_desc[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 24 + 8).to_le_bytes());
file_data.extend_from_slice(&tbl_desc);
let mut tbl_hdr = [0u8; 24];
tbl_hdr[0..4].copy_from_slice(&2u32.to_le_bytes());
tbl_hdr[8..16].copy_from_slice(§ors_data_offset.to_le_bytes());
file_data.extend_from_slice(&tbl_hdr);
let entry1: u32 = 0x8000_0000;
file_data.extend_from_slice(&entry1.to_le_bytes());
let entry2: u32 = 0x8000_0000 | compressed1.len() as u32;
file_data.extend_from_slice(&entry2.to_le_bytes());
let mut sec_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
sec_desc[..7].copy_from_slice(b"sectors");
sec_desc[16..24].copy_from_slice(&done_desc_offset.to_le_bytes());
sec_desc[24..32].copy_from_slice(
&(SECTION_DESCRIPTOR_SIZE as u64 + total_compressed as u64).to_le_bytes(),
);
file_data.extend_from_slice(&sec_desc);
file_data.extend_from_slice(&compressed1);
file_data.extend_from_slice(&compressed2);
let mut done_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
done_desc[..4].copy_from_slice(b"done");
done_desc[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64).to_le_bytes());
file_data.extend_from_slice(&done_desc);
let mut tmp = tempfile::Builder::new().suffix(".E01").tempfile().unwrap();
tmp.write_all(&file_data).unwrap();
tmp.flush().unwrap();
tmp
}
#[test]
fn ewf_reader_two_compressed_chunks() {
let data1 = b"first chunk data here!";
let data2 = b"second chunk is different";
let tmp = build_synthetic_e01_two_chunks(data1, data2);
let mut reader = EwfReader::open(tmp.path()).unwrap();
assert_eq!(reader.total_size(), 32768 * 2);
assert_eq!(reader.chunk_count(), 2);
let mut buf = vec![0u8; data1.len()];
reader.read_exact(&mut buf).unwrap();
assert_eq!(buf, data1);
reader.seek(SeekFrom::Start(32768)).unwrap();
let mut buf2 = vec![0u8; data2.len()];
reader.read_exact(&mut buf2).unwrap();
assert_eq!(buf2, data2);
}
#[test]
fn discover_segments_sorts_by_extension() {
let dir = tempfile::tempdir().unwrap();
let path_e02 = dir.path().join("test.E02");
let path_e01 = dir.path().join("test.E01");
let mut hdr1 = [0u8; FILE_HEADER_SIZE];
hdr1[0..8].copy_from_slice(&EVF_SIGNATURE);
hdr1[8] = 0x01;
hdr1[9..11].copy_from_slice(&1u16.to_le_bytes());
let mut hdr2 = [0u8; FILE_HEADER_SIZE];
hdr2[0..8].copy_from_slice(&EVF_SIGNATURE);
hdr2[8] = 0x01;
hdr2[9..11].copy_from_slice(&2u16.to_le_bytes());
std::fs::write(&path_e01, hdr1).unwrap();
std::fs::write(&path_e02, hdr2).unwrap();
let result = EwfReader::open(&path_e01);
assert!(result.is_err());
}
fn make_ewf2_file_header(is_physical: bool, segment: u32, compression: u16) -> [u8; 32] {
let mut buf = [0u8; 32];
let sig = if is_physical { ewf2::EVF2_SIGNATURE } else { ewf2::LEF2_SIGNATURE };
buf[0..8].copy_from_slice(&sig);
buf[8] = 0x02;
buf[9] = 0x01;
buf[10..12].copy_from_slice(&compression.to_le_bytes());
buf[12..16].copy_from_slice(&segment.to_le_bytes());
buf
}
#[test]
fn ewf2_parse_ex01_header() {
let buf = make_ewf2_file_header(true, 1, 1);
let header = ewf2::Ewf2FileHeader::parse(&buf).unwrap();
assert!(header.is_physical, "Ex01 should be physical");
assert_eq!(header.major_version, 2);
assert_eq!(header.minor_version, 1);
assert_eq!(header.compression_method, ewf2::CompressionMethod::Zlib);
assert_eq!(header.segment_number, 1);
}
#[test]
fn ewf2_parse_lx01_header() {
let buf = make_ewf2_file_header(false, 3, 2);
let header = ewf2::Ewf2FileHeader::parse(&buf).unwrap();
assert!(!header.is_physical, "Lx01 should not be physical");
assert_eq!(header.compression_method, ewf2::CompressionMethod::Bzip2);
assert_eq!(header.segment_number, 3);
}
#[test]
fn ewf2_header_rejects_v1_signature() {
let v1_buf = make_file_header(1);
let mut buf = [0u8; 32];
buf[..13].copy_from_slice(&v1_buf);
assert!(ewf2::Ewf2FileHeader::parse(&buf).is_err());
}
#[test]
fn ewf2_header_rejects_short_buffer() {
assert!(ewf2::Ewf2FileHeader::parse(&[0u8; 10]).is_err());
}
fn make_ewf2_section_descriptor(
section_type: u32, data_flags: u32, prev_offset: u64, data_size: u64,
) -> [u8; 64] {
let mut buf = [0u8; 64];
buf[0..4].copy_from_slice(§ion_type.to_le_bytes());
buf[4..8].copy_from_slice(&data_flags.to_le_bytes());
buf[8..16].copy_from_slice(&prev_offset.to_le_bytes());
buf[16..24].copy_from_slice(&data_size.to_le_bytes());
buf[24..28].copy_from_slice(&64u32.to_le_bytes());
buf
}
#[test]
fn ewf2_parse_section_descriptor() {
let buf = make_ewf2_section_descriptor(0x03, 0x01, 100, 65536);
let desc = ewf2::Ewf2SectionDescriptor::parse(&buf, 200).unwrap();
assert_eq!(desc.section_type, ewf2::Ewf2SectionType::SectorData);
assert!(desc.is_md5_hashed());
assert!(!desc.is_encrypted());
assert_eq!(desc.previous_offset, 100);
assert_eq!(desc.data_size, 65536);
assert_eq!(desc.offset, 200);
}
#[test]
fn ewf2_section_descriptor_encrypted_flag() {
let buf = make_ewf2_section_descriptor(0x08, 0x03, 0, 20);
let desc = ewf2::Ewf2SectionDescriptor::parse(&buf, 0).unwrap();
assert_eq!(desc.section_type, ewf2::Ewf2SectionType::Md5Hash);
assert!(desc.is_encrypted());
}
#[test]
fn ewf2_section_type_names() {
assert_eq!(ewf2::Ewf2SectionType::SectorData.name(), "sector_data");
assert_eq!(ewf2::Ewf2SectionType::Done.name(), "done");
assert_eq!(ewf2::Ewf2SectionType::Unknown(0xFF).name(), "unknown");
}
fn make_ewf2_table_entry(offset: u64, size: u32, flags: u32) -> [u8; 16] {
let mut buf = [0u8; 16];
buf[0..8].copy_from_slice(&offset.to_le_bytes());
buf[8..12].copy_from_slice(&size.to_le_bytes());
buf[12..16].copy_from_slice(&flags.to_le_bytes());
buf
}
#[test]
fn ewf2_parse_compressed_table_entry() {
let buf = make_ewf2_table_entry(4096, 30000, 0x01);
let entry = ewf2::Ewf2TableEntry::parse(&buf).unwrap();
assert_eq!(entry.chunk_data_offset, 4096);
assert_eq!(entry.chunk_data_size, 30000);
assert!(entry.is_compressed());
assert!(!entry.is_checksumed());
assert!(!entry.is_pattern_fill());
}
#[test]
fn ewf2_parse_uncompressed_table_entry() {
let buf = make_ewf2_table_entry(8192, 32768, 0x02);
let entry = ewf2::Ewf2TableEntry::parse(&buf).unwrap();
assert!(!entry.is_compressed());
assert!(entry.is_checksumed());
}
#[test]
fn ewf2_parse_pattern_fill_entry() {
let buf = make_ewf2_table_entry(0, 0, 0x05);
let entry = ewf2::Ewf2TableEntry::parse(&buf).unwrap();
assert!(entry.is_pattern_fill());
assert_eq!(entry.chunk_data_size, 0);
}
#[test]
fn ewf2_parse_table_header() {
let mut buf = [0u8; 20];
buf[0..8].copy_from_slice(&0u64.to_le_bytes());
buf[8..12].copy_from_slice(&128u32.to_le_bytes());
let header = ewf2::Ewf2TableHeader::parse(&buf).unwrap();
assert_eq!(header.first_chunk, 0);
assert_eq!(header.entry_count, 128);
}
fn build_synthetic_ex01(data: &[u8]) -> NamedTempFile {
use flate2::write::ZlibEncoder;
use flate2::Compression;
let chunk_size: u32 = 32768;
let bytes_per_sector: u32 = 512;
let sectors_per_chunk: u32 = chunk_size / bytes_per_sector; let total_sectors: u64 = sectors_per_chunk as u64;
let mut padded = data.to_vec();
padded.resize(chunk_size as usize, 0);
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&padded).unwrap();
let compressed = encoder.finish().unwrap();
let device_info_text = format!(
"2\nmain\nb\tsc\tts\n{}\t{}\t{}\n\n",
bytes_per_sector, sectors_per_chunk, total_sectors
);
let device_info_utf16: Vec<u8> = device_info_text
.encode_utf16()
.flat_map(|c| c.to_le_bytes())
.collect();
let devinfo_data_size = device_info_utf16.len();
let table_data_size = 20 + 16;
let devinfo_desc_off: usize = 32; let devinfo_data_off: usize = devinfo_desc_off + 64;
let table_desc_off: usize = devinfo_data_off + devinfo_data_size;
let table_data_off: usize = table_desc_off + 64;
let sectors_desc_off: usize = table_data_off + table_data_size;
let sectors_data_off: usize = sectors_desc_off + 64;
let done_desc_off: usize = sectors_data_off + compressed.len();
fn make_v2_desc(
section_type: u32, data_size: u64, previous_offset: u64,
) -> [u8; 64] {
let mut desc = [0u8; 64];
desc[0..4].copy_from_slice(§ion_type.to_le_bytes());
desc[8..16].copy_from_slice(&previous_offset.to_le_bytes());
desc[16..24].copy_from_slice(&data_size.to_le_bytes());
desc[24..28].copy_from_slice(&64u32.to_le_bytes()); desc
}
let mut file_data = Vec::new();
file_data.extend_from_slice(&ewf2::EVF2_SIGNATURE);
file_data.push(2); file_data.push(1); file_data.extend_from_slice(&1u16.to_le_bytes()); file_data.extend_from_slice(&1u32.to_le_bytes()); file_data.extend_from_slice(&[0u8; 16]); assert_eq!(file_data.len(), 32);
file_data.extend_from_slice(&make_v2_desc(
0x01, devinfo_data_size as u64, 0,
));
file_data.extend_from_slice(&device_info_utf16);
file_data.extend_from_slice(&make_v2_desc(
0x04, table_data_size as u64, devinfo_desc_off as u64,
));
let mut tbl_hdr = [0u8; 20];
tbl_hdr[0..8].copy_from_slice(&0u64.to_le_bytes()); tbl_hdr[8..12].copy_from_slice(&1u32.to_le_bytes()); file_data.extend_from_slice(&tbl_hdr);
let mut entry = [0u8; 16];
entry[0..8].copy_from_slice(&(sectors_data_off as u64).to_le_bytes());
entry[8..12].copy_from_slice(&(compressed.len() as u32).to_le_bytes());
entry[12..16].copy_from_slice(&ewf2::CHUNK_FLAG_COMPRESSED.to_le_bytes());
file_data.extend_from_slice(&entry);
file_data.extend_from_slice(&make_v2_desc(
0x03, compressed.len() as u64, table_desc_off as u64,
));
file_data.extend_from_slice(&compressed);
file_data.extend_from_slice(&make_v2_desc(
0x0F, 0, sectors_desc_off as u64,
));
assert_eq!(file_data.len(), done_desc_off + 64);
let mut tmp = tempfile::Builder::new().suffix(".Ex01").tempfile().unwrap();
tmp.write_all(&file_data).unwrap();
tmp.flush().unwrap();
tmp
}
#[test]
fn ewf2_section_descriptor_rejects_short_buffer() {
assert!(ewf2::Ewf2SectionDescriptor::parse(&[0u8; 10], 0).is_err());
}
#[test]
fn ewf2_table_entry_rejects_short_buffer() {
assert!(ewf2::Ewf2TableEntry::parse(&[0u8; 4]).is_err());
}
#[test]
fn ewf2_table_header_rejects_short_buffer() {
assert!(ewf2::Ewf2TableHeader::parse(&[0u8; 8]).is_err());
}
#[test]
fn ewf2_compression_none_and_unknown() {
assert_eq!(ewf2::CompressionMethod::from_u16(0).unwrap(), ewf2::CompressionMethod::None);
assert!(ewf2::CompressionMethod::from_u16(99).is_err());
}
#[test]
fn ewf2_section_type_from_u32_all_variants() {
let cases: &[(u32, &str)] = &[
(0x01, "device_info"),
(0x02, "case_data"),
(0x03, "sector_data"),
(0x04, "sector_table"),
(0x05, "error_table"),
(0x06, "session_table"),
(0x07, "increment_data"),
(0x08, "md5_hash"),
(0x09, "sha1_hash"),
(0x0A, "restart_data"),
(0x0B, "encryption_keys"),
(0x0C, "memory_extents"),
(0x0D, "next"),
(0x0E, "final_info"),
(0x0F, "done"),
(0x10, "analytical_data"),
(0x20, "single_files_data"),
(0xFF, "unknown"),
];
for &(val, expected_name) in cases {
let st = ewf2::Ewf2SectionType::from_u32(val);
assert_eq!(st.name(), expected_name, "from_u32({val:#x}) name mismatch");
}
}
#[test]
fn ewf2_reader_rejects_encrypted() {
let mut file_data = Vec::new();
file_data.extend_from_slice(&ewf2::EVF2_SIGNATURE);
file_data.push(2); file_data.push(1);
file_data.extend_from_slice(&1u16.to_le_bytes());
file_data.extend_from_slice(&1u32.to_le_bytes());
file_data.extend_from_slice(&[0u8; 16]);
let mut desc = [0u8; 64];
desc[0..4].copy_from_slice(&0x01u32.to_le_bytes()); desc[4..8].copy_from_slice(&0x02u32.to_le_bytes()); desc[16..24].copy_from_slice(&100u64.to_le_bytes()); desc[24..28].copy_from_slice(&64u32.to_le_bytes());
file_data.extend_from_slice(&desc);
file_data.extend_from_slice(&[0u8; 100]);
let mut tmp = tempfile::Builder::new().suffix(".Ex01").tempfile().unwrap();
tmp.write_all(&file_data).unwrap();
tmp.flush().unwrap();
let result = EwfReader::open(tmp.path());
assert!(result.is_err(), "Encrypted EWF2 should be rejected");
assert!(
matches!(result.unwrap_err(), EwfError::EncryptedNotSupported),
"Should be EncryptedNotSupported error"
);
}
fn build_synthetic_ex01_with_md5_no_devinfo(data: &[u8]) -> (NamedTempFile, [u8; 16]) {
use flate2::write::ZlibEncoder;
use flate2::Compression;
let chunk_size: u32 = 32768;
let mut padded = data.to_vec();
padded.resize(chunk_size as usize, 0);
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&padded).unwrap();
let compressed = encoder.finish().unwrap();
let fake_md5: [u8; 16] = [
0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
];
fn make_v2_desc(section_type: u32, data_size: u64, prev: u64) -> [u8; 64] {
let mut d = [0u8; 64];
d[0..4].copy_from_slice(§ion_type.to_le_bytes());
d[8..16].copy_from_slice(&prev.to_le_bytes());
d[16..24].copy_from_slice(&data_size.to_le_bytes());
d[24..28].copy_from_slice(&64u32.to_le_bytes());
d
}
let tbl_desc_off = 32usize;
let tbl_data_size = 20 + 16; let md5_desc_off = tbl_desc_off + 64 + tbl_data_size;
let md5_data_size = 16usize;
let sectors_desc_off = md5_desc_off + 64 + md5_data_size;
let sectors_data_off = sectors_desc_off + 64;
let done_desc_off = sectors_data_off + compressed.len();
let mut file_data = Vec::new();
file_data.extend_from_slice(&ewf2::EVF2_SIGNATURE);
file_data.push(2); file_data.push(1);
file_data.extend_from_slice(&1u16.to_le_bytes());
file_data.extend_from_slice(&1u32.to_le_bytes());
file_data.extend_from_slice(&[0u8; 16]);
file_data.extend_from_slice(&make_v2_desc(0x04, tbl_data_size as u64, 0));
let mut tbl_hdr = [0u8; 20];
tbl_hdr[8..12].copy_from_slice(&1u32.to_le_bytes());
file_data.extend_from_slice(&tbl_hdr);
let mut entry = [0u8; 16];
entry[0..8].copy_from_slice(&(sectors_data_off as u64).to_le_bytes());
entry[8..12].copy_from_slice(&(compressed.len() as u32).to_le_bytes());
entry[12..16].copy_from_slice(&ewf2::CHUNK_FLAG_COMPRESSED.to_le_bytes());
file_data.extend_from_slice(&entry);
file_data.extend_from_slice(&make_v2_desc(0x08, md5_data_size as u64, tbl_desc_off as u64));
file_data.extend_from_slice(&fake_md5);
file_data.extend_from_slice(&make_v2_desc(0x03, compressed.len() as u64, md5_desc_off as u64));
file_data.extend_from_slice(&compressed);
file_data.extend_from_slice(&make_v2_desc(0x0F, 0, sectors_desc_off as u64));
assert_eq!(file_data.len(), done_desc_off + 64);
let mut tmp = tempfile::Builder::new().suffix(".Ex01").tempfile().unwrap();
tmp.write_all(&file_data).unwrap();
tmp.flush().unwrap();
(tmp, fake_md5)
}
#[test]
fn ewf2_reader_parses_md5_hash_section() {
let (tmp, expected_md5) = build_synthetic_ex01_with_md5_no_devinfo(b"md5 test");
let result = EwfReader::open(tmp.path());
assert!(result.is_ok(), "Should open Ex01 with md5_hash: {:?}", result.err());
let reader = result.unwrap();
let hashes = reader.stored_hashes();
assert_eq!(hashes.md5, Some(expected_md5));
}
#[test]
fn ewf2_reader_defaults_chunk_size_without_device_info() {
let (tmp, _) = build_synthetic_ex01_with_md5_no_devinfo(b"no devinfo");
let reader = EwfReader::open(tmp.path()).unwrap();
assert_eq!(reader.chunk_size(), 32768);
assert_eq!(reader.total_size(), 32768);
}
#[test]
fn ewf2_reader_reads_data_without_device_info() {
let data = b"read without devinfo";
let (tmp, _) = build_synthetic_ex01_with_md5_no_devinfo(data);
let mut reader = EwfReader::open(tmp.path()).unwrap();
let mut buf = vec![0u8; data.len()];
reader.read_exact(&mut buf).unwrap();
assert_eq!(&buf, data);
}
fn build_synthetic_ex01_with_md5_and_sha1(data: &[u8]) -> (NamedTempFile, [u8; 16], [u8; 20]) {
use flate2::write::ZlibEncoder;
use flate2::Compression;
let chunk_size: u32 = 32768;
let mut padded = data.to_vec();
padded.resize(chunk_size as usize, 0);
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&padded).unwrap();
let compressed = encoder.finish().unwrap();
let fake_md5: [u8; 16] = [
0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
];
let fake_sha1: [u8; 20] = [
0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9,
0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9,
];
fn make_v2_desc(section_type: u32, data_size: u64, prev: u64) -> [u8; 64] {
let mut d = [0u8; 64];
d[0..4].copy_from_slice(§ion_type.to_le_bytes());
d[8..16].copy_from_slice(&prev.to_le_bytes());
d[16..24].copy_from_slice(&data_size.to_le_bytes());
d[24..28].copy_from_slice(&64u32.to_le_bytes());
d
}
let tbl_off = 32usize;
let tbl_data = 20 + 16; let md5_off = tbl_off + 64 + tbl_data;
let sha1_off = md5_off + 64 + 16;
let sec_off = sha1_off + 64 + 20;
let sec_data_off = sec_off + 64;
let done_off = sec_data_off + compressed.len();
let mut f = Vec::new();
f.extend_from_slice(&ewf2::EVF2_SIGNATURE);
f.push(2); f.push(1);
f.extend_from_slice(&1u16.to_le_bytes());
f.extend_from_slice(&1u32.to_le_bytes());
f.extend_from_slice(&[0u8; 16]);
f.extend_from_slice(&make_v2_desc(0x04, tbl_data as u64, 0));
let mut tbl_hdr = [0u8; 20];
tbl_hdr[8..12].copy_from_slice(&1u32.to_le_bytes());
f.extend_from_slice(&tbl_hdr);
let mut entry = [0u8; 16];
entry[0..8].copy_from_slice(&(sec_data_off as u64).to_le_bytes());
entry[8..12].copy_from_slice(&(compressed.len() as u32).to_le_bytes());
entry[12..16].copy_from_slice(&ewf2::CHUNK_FLAG_COMPRESSED.to_le_bytes());
f.extend_from_slice(&entry);
f.extend_from_slice(&make_v2_desc(0x08, 16, tbl_off as u64));
f.extend_from_slice(&fake_md5);
f.extend_from_slice(&make_v2_desc(0x09, 20, md5_off as u64));
f.extend_from_slice(&fake_sha1);
f.extend_from_slice(&make_v2_desc(0x03, compressed.len() as u64, sha1_off as u64));
f.extend_from_slice(&compressed);
f.extend_from_slice(&make_v2_desc(0x0F, 0, sec_off as u64));
assert_eq!(f.len(), done_off + 64);
let mut tmp = tempfile::Builder::new().suffix(".Ex01").tempfile().unwrap();
tmp.write_all(&f).unwrap();
tmp.flush().unwrap();
(tmp, fake_md5, fake_sha1)
}
#[test]
fn ewf2_reader_parses_sha1_hash_section() {
let (tmp, expected_md5, expected_sha1) = build_synthetic_ex01_with_md5_and_sha1(b"sha1 test");
let reader = EwfReader::open(tmp.path()).unwrap();
let hashes = reader.stored_hashes();
assert_eq!(hashes.md5, Some(expected_md5), "V2 MD5 should be parsed");
assert_eq!(hashes.sha1, Some(expected_sha1), "V2 SHA-1 should be parsed");
}
fn build_synthetic_ex01_with_case_data() -> NamedTempFile {
use flate2::write::ZlibEncoder;
use flate2::Compression;
let chunk_size: u32 = 32768;
let mut padded = b"case data test".to_vec();
padded.resize(chunk_size as usize, 0);
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&padded).unwrap();
let compressed = encoder.finish().unwrap();
let case_text = "2\nmain\ncn\ten\tex\tde\tnt\tav\tov\tad\tsd\nCASE-42\tEV-7\tJane Doe\tTest image\tForensic notes\tEnCase 8.0\tWindows 11\t2025-01-15\t2025-01-14\n";
let case_utf16: Vec<u8> = case_text.encode_utf16().flat_map(|c| c.to_le_bytes()).collect();
fn make_v2_desc(section_type: u32, data_size: u64, prev: u64) -> [u8; 64] {
let mut d = [0u8; 64];
d[0..4].copy_from_slice(§ion_type.to_le_bytes());
d[8..16].copy_from_slice(&prev.to_le_bytes());
d[16..24].copy_from_slice(&data_size.to_le_bytes());
d[24..28].copy_from_slice(&64u32.to_le_bytes());
d
}
let case_off = 32usize;
let case_data_size = case_utf16.len();
let tbl_off = case_off + 64 + case_data_size;
let tbl_data = 20 + 16;
let sec_off = tbl_off + 64 + tbl_data;
let sec_data_off = sec_off + 64;
let done_off = sec_data_off + compressed.len();
let mut f = Vec::new();
f.extend_from_slice(&ewf2::EVF2_SIGNATURE);
f.push(2); f.push(1);
f.extend_from_slice(&1u16.to_le_bytes());
f.extend_from_slice(&1u32.to_le_bytes());
f.extend_from_slice(&[0u8; 16]);
f.extend_from_slice(&make_v2_desc(0x02, case_data_size as u64, 0));
f.extend_from_slice(&case_utf16);
f.extend_from_slice(&make_v2_desc(0x04, tbl_data as u64, case_off as u64));
let mut tbl_hdr = [0u8; 20];
tbl_hdr[8..12].copy_from_slice(&1u32.to_le_bytes());
f.extend_from_slice(&tbl_hdr);
let mut entry = [0u8; 16];
entry[0..8].copy_from_slice(&(sec_data_off as u64).to_le_bytes());
entry[8..12].copy_from_slice(&(compressed.len() as u32).to_le_bytes());
entry[12..16].copy_from_slice(&ewf2::CHUNK_FLAG_COMPRESSED.to_le_bytes());
f.extend_from_slice(&entry);
f.extend_from_slice(&make_v2_desc(0x03, compressed.len() as u64, tbl_off as u64));
f.extend_from_slice(&compressed);
f.extend_from_slice(&make_v2_desc(0x0F, 0, sec_off as u64));
assert_eq!(f.len(), done_off + 64);
let mut tmp = tempfile::Builder::new().suffix(".Ex01").tempfile().unwrap();
tmp.write_all(&f).unwrap();
tmp.flush().unwrap();
tmp
}
#[test]
fn ewf2_reader_parses_case_data_metadata() {
let tmp = build_synthetic_ex01_with_case_data();
let reader = EwfReader::open(tmp.path()).unwrap();
let meta = reader.metadata();
assert_eq!(meta.case_number.as_deref(), Some("CASE-42"));
assert_eq!(meta.evidence_number.as_deref(), Some("EV-7"));
assert_eq!(meta.examiner.as_deref(), Some("Jane Doe"));
assert_eq!(meta.description.as_deref(), Some("Test image"));
assert_eq!(meta.notes.as_deref(), Some("Forensic notes"));
assert_eq!(meta.acquiry_software.as_deref(), Some("EnCase 8.0"));
assert_eq!(meta.os_version.as_deref(), Some("Windows 11"));
assert_eq!(meta.acquiry_date.as_deref(), Some("2025-01-15"));
assert_eq!(meta.system_date.as_deref(), Some("2025-01-14"));
}
#[test]
fn ewf_reader_debug_format() {
let data = b"debug test";
let tmp = build_synthetic_e01(data);
let reader = EwfReader::open(tmp.path()).unwrap();
let debug = format!("{:?}", reader);
assert!(debug.contains("EwfReader"));
assert!(debug.contains("chunk_size"));
assert!(debug.contains("total_size"));
}
#[test]
fn parse_header_text_short_input() {
use crate::parse::parse_header_text;
let mut meta = EwfMetadata::default();
parse_header_text("line1\nline2\n", &mut meta);
assert!(meta.case_number.is_none());
}
#[test]
fn parse_error2_data_short_input() {
let errors = parse_error2_data(&[0u8; 4]);
assert!(errors.is_empty());
}
#[test]
fn parse_error2_data_truncated_entries() {
let mut data = Vec::new();
data.extend_from_slice(&2u32.to_le_bytes()); data.extend_from_slice(&[0u8; 4]); data.extend_from_slice(&100u32.to_le_bytes()); data.extend_from_slice(&5u32.to_le_bytes()); let errors = parse_error2_data(&data);
assert_eq!(errors.len(), 1);
}
fn build_synthetic_e01_with_error2() -> NamedTempFile {
use flate2::write::ZlibEncoder;
use flate2::Compression;
let chunk_size: u32 = 32768;
let sectors_per_chunk: u32 = 64;
let bytes_per_sector: u32 = 512;
let sector_count = (chunk_size / bytes_per_sector) as u64;
let data = b"error2 test data";
let mut padded = data.to_vec();
padded.resize(chunk_size as usize, 0);
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&padded).unwrap();
let compressed = encoder.finish().unwrap();
let mut error2_data = Vec::new();
error2_data.extend_from_slice(&1u32.to_le_bytes()); error2_data.extend_from_slice(&[0u8; 4]); error2_data.extend_from_slice(&42u32.to_le_bytes()); error2_data.extend_from_slice(&3u32.to_le_bytes()); error2_data.extend_from_slice(&[0u8; 4]); let error2_size = error2_data.len();
let vol_desc_off: u64 = FILE_HEADER_SIZE as u64;
let vol_data_off: u64 = vol_desc_off + SECTION_DESCRIPTOR_SIZE as u64;
let tbl_desc_off: u64 = vol_data_off + 94;
let tbl_hdr_off: u64 = tbl_desc_off + SECTION_DESCRIPTOR_SIZE as u64;
let tbl_entry_off: u64 = tbl_hdr_off + 24;
let sectors_desc_off: u64 = tbl_entry_off + 4;
let sectors_data_off: u64 = sectors_desc_off + SECTION_DESCRIPTOR_SIZE as u64;
let error2_desc_off: u64 = sectors_data_off + compressed.len() as u64;
let error2_data_off: u64 = error2_desc_off + SECTION_DESCRIPTOR_SIZE as u64;
let done_desc_off: u64 = error2_data_off + error2_size as u64;
let mut file_data = Vec::new();
file_data.extend_from_slice(&EVF_SIGNATURE);
file_data.push(0x01);
file_data.extend_from_slice(&1u16.to_le_bytes());
file_data.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(&tbl_desc_off.to_le_bytes());
vd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 94).to_le_bytes());
file_data.extend_from_slice(&vd);
let mut vol = [0u8; 94];
vol[4..8].copy_from_slice(&1u32.to_le_bytes());
vol[8..12].copy_from_slice(§ors_per_chunk.to_le_bytes());
vol[12..16].copy_from_slice(&bytes_per_sector.to_le_bytes());
vol[16..24].copy_from_slice(§or_count.to_le_bytes());
file_data.extend_from_slice(&vol);
let mut td = [0u8; SECTION_DESCRIPTOR_SIZE];
td[..5].copy_from_slice(b"table");
td[16..24].copy_from_slice(§ors_desc_off.to_le_bytes());
td[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4).to_le_bytes());
file_data.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(§ors_data_off.to_le_bytes());
file_data.extend_from_slice(&th);
file_data.extend_from_slice(&0x8000_0000u32.to_le_bytes());
let mut sd = [0u8; SECTION_DESCRIPTOR_SIZE];
sd[..7].copy_from_slice(b"sectors");
sd[16..24].copy_from_slice(&error2_desc_off.to_le_bytes());
sd[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + compressed.len() as u64).to_le_bytes());
file_data.extend_from_slice(&sd);
file_data.extend_from_slice(&compressed);
let mut ed = [0u8; SECTION_DESCRIPTOR_SIZE];
ed[..6].copy_from_slice(b"error2");
ed[16..24].copy_from_slice(&done_desc_off.to_le_bytes());
ed[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + error2_size as u64).to_le_bytes());
file_data.extend_from_slice(&ed);
file_data.extend_from_slice(&error2_data);
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());
file_data.extend_from_slice(&dd);
let mut tmp = tempfile::Builder::new().suffix(".E01").tempfile().unwrap();
tmp.write_all(&file_data).unwrap();
tmp.flush().unwrap();
tmp
}
#[test]
fn ewf_reader_parses_error2_from_synthetic() {
let tmp = build_synthetic_e01_with_error2();
let reader = EwfReader::open(tmp.path()).unwrap();
let errors = reader.acquisition_errors();
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].first_sector, 42);
assert_eq!(errors[0].sector_count, 3);
}
#[test]
fn parse_ewf2_device_info_short_data() {
use crate::reader::parse_ewf2_device_info;
let mut cs = 0u64;
let mut ts = 0u64;
parse_ewf2_device_info(&[], &mut cs, &mut ts);
assert_eq!(cs, 0);
parse_ewf2_device_info(&[0x41], &mut cs, &mut ts);
assert_eq!(cs, 0);
}
#[test]
fn parse_ewf2_device_info_too_few_lines() {
use crate::reader::parse_ewf2_device_info;
let mut cs = 0u64;
let mut ts = 0u64;
let text = "hi\n";
let utf16: Vec<u8> = text.encode_utf16().flat_map(|c| c.to_le_bytes()).collect();
parse_ewf2_device_info(&utf16, &mut cs, &mut ts);
assert_eq!(cs, 0);
}
#[test]
fn parse_ewf2_device_info_unknown_fields_ignored() {
use crate::reader::parse_ewf2_device_info;
let mut cs = 0u64;
let mut ts = 0u64;
let text = "2\nmain\nb\tsc\tts\txyz\n512\t64\t128\tignored\n\n";
let utf16: Vec<u8> = text.encode_utf16().flat_map(|c| c.to_le_bytes()).collect();
parse_ewf2_device_info(&utf16, &mut cs, &mut ts);
assert_eq!(cs, 512 * 64);
assert_eq!(ts, 512 * 128);
}
#[test]
fn parse_ewf2_device_info_zero_bytes_per_sector_leaves_chunk_size() {
use crate::reader::parse_ewf2_device_info;
let mut cs: u64 = 99999; let mut ts: u64 = 0;
let text = "2\nmain\nb\tsc\tts\n0\t64\t128\n";
let utf16: Vec<u8> = text.encode_utf16().flat_map(|c| c.to_le_bytes()).collect();
parse_ewf2_device_info(&utf16, &mut cs, &mut ts);
assert_eq!(cs, 99999, "chunk_size should be unchanged when bytes_per_sector=0");
assert_eq!(ts, 0, "total_size should be unchanged when bytes_per_sector=0");
}
#[test]
fn parse_ewf2_device_info_zero_sectors_per_chunk_leaves_chunk_size() {
use crate::reader::parse_ewf2_device_info;
let mut cs: u64 = 99999;
let mut ts: u64 = 0;
let text = "2\nmain\nb\tsc\tts\n512\t0\t128\n";
let utf16: Vec<u8> = text.encode_utf16().flat_map(|c| c.to_le_bytes()).collect();
parse_ewf2_device_info(&utf16, &mut cs, &mut ts);
assert_eq!(cs, 99999, "chunk_size should be unchanged when sectors_per_chunk=0");
assert_eq!(ts, 512 * 128, "total_size should still be computed from valid bytes_per_sector");
}
#[test]
fn ewf2_reader_rejects_segment_gap() {
fn make_minimal_ex01(segment: u32) -> NamedTempFile {
let mut d = Vec::new();
d.extend_from_slice(&ewf2::EVF2_SIGNATURE);
d.push(2); d.push(1);
d.extend_from_slice(&1u16.to_le_bytes());
d.extend_from_slice(&segment.to_le_bytes());
d.extend_from_slice(&[0u8; 16]); let mut done = [0u8; 64];
done[0..4].copy_from_slice(&0x0Fu32.to_le_bytes()); done[24..28].copy_from_slice(&64u32.to_le_bytes());
d.extend_from_slice(&done);
let mut tmp = tempfile::Builder::new().suffix(".Ex01").tempfile().unwrap();
tmp.write_all(&d).unwrap();
tmp.flush().unwrap();
tmp
}
let seg1 = make_minimal_ex01(1);
let seg3 = make_minimal_ex01(3);
let result = EwfReader::open_segments(&[seg1.path().into(), seg3.path().into()]);
assert!(result.is_err(), "Should reject segment gap");
let err_msg = format!("{}", result.unwrap_err());
assert!(err_msg.contains("segment gap"), "Error should mention segment gap: {err_msg}");
}
#[test]
fn ewf2_reader_handles_truncated_chain() {
let mut file_data = Vec::new();
file_data.extend_from_slice(&ewf2::EVF2_SIGNATURE);
file_data.push(2); file_data.push(1);
file_data.extend_from_slice(&1u16.to_le_bytes());
file_data.extend_from_slice(&1u32.to_le_bytes());
file_data.extend_from_slice(&[0u8; 16]);
let mut tmp = tempfile::Builder::new().suffix(".Ex01").tempfile().unwrap();
tmp.write_all(&file_data).unwrap();
tmp.flush().unwrap();
let result = EwfReader::open(tmp.path());
assert!(result.is_ok(), "Truncated Ex01 should open: {:?}", result.err());
let reader = result.unwrap();
assert_eq!(reader.chunk_count(), 0);
}
#[test]
fn ewf_reader_opens_synthetic_ex01() {
let data = b"Hello, EWF2 world!";
let tmp = build_synthetic_ex01(data);
let result = EwfReader::open(tmp.path());
assert!(result.is_ok(), "EwfReader should open Ex01: {:?}", result.err());
let reader = result.unwrap();
assert_eq!(reader.chunk_size(), 32768);
assert_eq!(reader.chunk_count(), 1);
assert_eq!(reader.total_size(), 32768);
}
#[test]
fn ewf_reader_reads_ex01_first_bytes() {
let data = b"DEADBEEF_EWF2_TEST";
let tmp = build_synthetic_ex01(data);
let result = EwfReader::open(tmp.path());
assert!(result.is_ok(), "EwfReader should open Ex01: {:?}", result.err());
let mut reader = result.unwrap();
let mut buf = vec![0u8; data.len()];
reader.read_exact(&mut buf).unwrap();
assert_eq!(&buf, data);
}
#[test]
fn ewf_reader_ex01_seek_and_read() {
let mut test_data = vec![0u8; 1024];
test_data[512..520].copy_from_slice(b"SEEKTEST");
let tmp = build_synthetic_ex01(&test_data);
let result = EwfReader::open(tmp.path());
assert!(result.is_ok(), "EwfReader should open Ex01: {:?}", result.err());
let mut reader = result.unwrap();
reader.seek(SeekFrom::Start(512)).unwrap();
let mut buf = [0u8; 8];
reader.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"SEEKTEST");
}
fn make_temp_files(n: usize) -> (tempfile::TempDir, Vec<std::fs::File>) {
let dir = tempfile::tempdir().unwrap();
let files: Vec<std::fs::File> = (0..n)
.map(|i| {
let p = dir.path().join(format!("seg{i}"));
std::fs::File::create(&p).unwrap();
std::fs::File::open(&p).unwrap()
})
.collect();
(dir, files)
}
#[test]
fn validate_reorder_sequential_segments() {
let (_dir, files) = make_temp_files(3);
let result = crate::reader::validate_and_reorder_segments(files, vec![1, 2, 3]);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 3);
}
#[test]
fn validate_reorder_out_of_order_segments() {
let (_dir, files) = make_temp_files(3);
let result = crate::reader::validate_and_reorder_segments(files, vec![3, 1, 2]);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 3);
}
#[test]
fn validate_reorder_single_segment() {
let (_dir, files) = make_temp_files(1);
let result = crate::reader::validate_and_reorder_segments(files, vec![1]);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 1);
}
#[test]
fn validate_reorder_detects_gap() {
let (_dir, files) = make_temp_files(2);
let result = crate::reader::validate_and_reorder_segments(files, vec![1, 3]);
assert!(
matches!(result, Err(EwfError::SegmentGap { expected: 2, got: 3 })),
"Should detect gap: {:?}", result
);
}
#[test]
fn validate_reorder_detects_gap_starting_at_zero() {
let (_dir, files) = make_temp_files(1);
let result = crate::reader::validate_and_reorder_segments(files, vec![0]);
assert!(
matches!(result, Err(EwfError::SegmentGap { expected: 1, got: 0 })),
"Should reject segment 0: {:?}", result
);
}
#[test]
fn validate_reorder_detects_duplicate_segments() {
let (_dir, files) = make_temp_files(2);
let result = crate::reader::validate_and_reorder_segments(files, vec![1, 1]);
assert!(
matches!(result, Err(EwfError::SegmentGap { .. })),
"Should reject duplicate segment numbers: {:?}", result
);
}
#[test]
fn last_compressed_chunk_has_correct_size() {
let data1 = b"first chunk";
let data2 = b"second chunk data";
let tmp = build_synthetic_e01_two_chunks(data1, data2);
let reader = EwfReader::open(tmp.path()).unwrap();
assert_eq!(reader.chunk_count(), 2);
let c0 = reader.chunk_meta(0);
let c1 = reader.chunk_meta(1);
assert!(c0.compressed);
assert!(c1.compressed);
assert!(c0.size < reader.chunk_size(), "Chunk 0 compressed size should be < chunk_size, got {}", c0.size);
assert!(c1.size < reader.chunk_size(),
"Last chunk compressed size should be < chunk_size ({}), got {}",
reader.chunk_size(), c1.size);
}
#[test]
fn single_compressed_chunk_has_correct_size() {
let data = b"solo chunk data";
let tmp = build_synthetic_e01(data);
let reader = EwfReader::open(tmp.path()).unwrap();
assert_eq!(reader.chunk_count(), 1);
let c0 = reader.chunk_meta(0);
assert!(c0.compressed);
assert!(c0.size < reader.chunk_size(),
"Single compressed chunk size should be < chunk_size ({}), got {}",
reader.chunk_size(), c0.size);
}
#[test]
fn v1_rejects_absurd_table_entry_count() {
let chunk_size: u32 = 32768;
let sectors_per_chunk: u32 = 64;
let bytes_per_sector: u32 = 512;
let sector_count: u64 = (chunk_size / bytes_per_sector) as u64;
let vol_desc_offset: u64 = FILE_HEADER_SIZE as u64;
let vol_data_offset: u64 = vol_desc_offset + SECTION_DESCRIPTOR_SIZE as u64;
let tbl_desc_offset: u64 = vol_data_offset + 94;
let tbl_hdr_offset: u64 = tbl_desc_offset + SECTION_DESCRIPTOR_SIZE as u64;
let done_desc_offset: u64 = tbl_hdr_offset + 24 + 4;
let mut file_data = Vec::new();
file_data.extend_from_slice(&EVF_SIGNATURE);
file_data.push(0x01);
file_data.extend_from_slice(&1u16.to_le_bytes());
file_data.extend_from_slice(&0u16.to_le_bytes());
let mut vol_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
vol_desc[..6].copy_from_slice(b"volume");
vol_desc[16..24].copy_from_slice(&tbl_desc_offset.to_le_bytes());
vol_desc[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64 + 94).to_le_bytes());
file_data.extend_from_slice(&vol_desc);
let mut vol_data = [0u8; 94];
vol_data[0..4].copy_from_slice(&1u32.to_le_bytes());
vol_data[4..8].copy_from_slice(&1u32.to_le_bytes());
vol_data[8..12].copy_from_slice(§ors_per_chunk.to_le_bytes());
vol_data[12..16].copy_from_slice(&bytes_per_sector.to_le_bytes());
vol_data[16..24].copy_from_slice(§or_count.to_le_bytes());
file_data.extend_from_slice(&vol_data);
let mut tbl_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
tbl_desc[..5].copy_from_slice(b"table");
tbl_desc[16..24].copy_from_slice(&done_desc_offset.to_le_bytes());
let tbl_section_size = SECTION_DESCRIPTOR_SIZE as u64 + 24 + 4;
tbl_desc[24..32].copy_from_slice(&tbl_section_size.to_le_bytes());
file_data.extend_from_slice(&tbl_desc);
let mut tbl_hdr = [0u8; 24];
tbl_hdr[0..4].copy_from_slice(&0x1000_0000u32.to_le_bytes()); tbl_hdr[8..16].copy_from_slice(&0u64.to_le_bytes());
file_data.extend_from_slice(&tbl_hdr);
file_data.extend_from_slice(&0u32.to_le_bytes());
let mut done_desc = [0u8; SECTION_DESCRIPTOR_SIZE];
done_desc[..4].copy_from_slice(b"done");
done_desc[24..32].copy_from_slice(&(SECTION_DESCRIPTOR_SIZE as u64).to_le_bytes());
file_data.extend_from_slice(&done_desc);
let mut tmp = tempfile::Builder::new().suffix(".E01").tempfile().unwrap();
tmp.write_all(&file_data).unwrap();
tmp.flush().unwrap();
let result = EwfReader::open(tmp.path());
assert!(result.is_err(), "Should reject absurd entry_count, got: {:?}", result.ok().map(|r| r.chunk_count()));
let err_msg = format!("{}", result.unwrap_err());
assert!(err_msg.contains("entry count"), "Error should mention entry count: {err_msg}");
}
#[test]
fn v2_rejects_absurd_table_entry_count() {
let mut d = Vec::new();
d.extend_from_slice(&ewf2::EVF2_SIGNATURE);
d.push(2); d.push(1); d.extend_from_slice(&1u16.to_le_bytes()); d.extend_from_slice(&1u32.to_le_bytes()); d.extend_from_slice(&[0u8; 16]);
let mut sec = [0u8; 64];
sec[0..4].copy_from_slice(&0x04u32.to_le_bytes()); sec[16..24].copy_from_slice(&36u64.to_le_bytes());
sec[24..28].copy_from_slice(&64u32.to_le_bytes()); d.extend_from_slice(&sec);
let mut tbl_hdr = [0u8; 20];
tbl_hdr[0..8].copy_from_slice(&0u64.to_le_bytes()); tbl_hdr[8..12].copy_from_slice(&0x1000_0000u32.to_le_bytes()); d.extend_from_slice(&tbl_hdr);
d.extend_from_slice(&[0u8; 16]);
let mut tmp = tempfile::Builder::new().suffix(".Ex01").tempfile().unwrap();
tmp.write_all(&d).unwrap();
tmp.flush().unwrap();
let result = EwfReader::open(tmp.path());
assert!(result.is_err(), "Should reject absurd v2 entry_count, got: {:?}", result.ok().map(|r| r.chunk_count()));
let err_msg = format!("{}", result.unwrap_err());
assert!(err_msg.contains("entry count"), "Error should mention entry count: {err_msg}");
}
#[test]
fn ewf2_case_data_empty_input() {
let mut meta = EwfMetadata::default();
reader::parse_ewf2_case_data(&[], &mut meta);
assert!(meta.case_number.is_none());
}
#[test]
fn ewf2_case_data_single_byte() {
let mut meta = EwfMetadata::default();
reader::parse_ewf2_case_data(&[0x41], &mut meta);
assert!(meta.case_number.is_none());
}
#[test]
fn ewf2_case_data_too_few_lines() {
let text = "line1\nline2\n";
let raw: Vec<u8> = text.encode_utf16().flat_map(|c| c.to_le_bytes()).collect();
let mut meta = EwfMetadata::default();
reader::parse_ewf2_case_data(&raw, &mut meta);
assert!(meta.case_number.is_none());
}
#[test]
fn ewf2_case_data_empty_values_skipped() {
let text = "1\nmain\ncn\ten\t\n\t\t\n";
let raw: Vec<u8> = text.encode_utf16().flat_map(|c| c.to_le_bytes()).collect();
let mut meta = EwfMetadata::default();
reader::parse_ewf2_case_data(&raw, &mut meta);
assert!(meta.case_number.is_none());
assert!(meta.evidence_number.is_none());
}
#[test]
fn ewf2_case_data_unknown_fields_ignored() {
let text = "1\nmain\nzz\nfoo\n";
let raw: Vec<u8> = text.encode_utf16().flat_map(|c| c.to_le_bytes()).collect();
let mut meta = EwfMetadata::default();
reader::parse_ewf2_case_data(&raw, &mut meta);
assert!(meta.case_number.is_none());
}
#[test]
fn ewf2_case_data_valid_fields_parsed() {
let text = "1\nmain\ncn\tex\tav\nCASE-001\tJohn\tFTK\n";
let raw: Vec<u8> = text.encode_utf16().flat_map(|c| c.to_le_bytes()).collect();
let mut meta = EwfMetadata::default();
reader::parse_ewf2_case_data(&raw, &mut meta);
assert_eq!(meta.case_number.as_deref(), Some("CASE-001"));
assert_eq!(meta.examiner.as_deref(), Some("John"));
assert_eq!(meta.acquiry_software.as_deref(), Some("FTK"));
}
#[test]
fn v2_zero_advance_section_breaks_loop() {
let mut d = Vec::new();
d.extend_from_slice(&ewf2::EVF2_SIGNATURE);
d.push(2); d.push(1); d.extend_from_slice(&1u16.to_le_bytes()); d.extend_from_slice(&1u32.to_le_bytes()); d.extend_from_slice(&[0u8; 16]);
let sec = [0u8; 64];
d.extend_from_slice(&sec);
let mut tmp = tempfile::Builder::new().suffix(".Ex01").tempfile().unwrap();
tmp.write_all(&d).unwrap();
tmp.flush().unwrap();
let result = EwfReader::open(tmp.path());
if let Ok(ref r) = result {
assert_eq!(r.total_size(), 0);
assert_eq!(r.chunk_count(), 0);
}
}
#[test]
fn truncated_compressed_chunk_returns_error() {
use std::io::{Read, Seek, SeekFrom};
let src_path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/nps-2010-emails.E01");
let src_data = std::fs::read(src_path).unwrap();
let mut tmp = tempfile::Builder::new().suffix(".E01").tempfile().unwrap();
tmp.write_all(&src_data).unwrap();
tmp.flush().unwrap();
let mut reader = EwfReader::open(tmp.path()).unwrap();
assert!(reader.total_size() > 0);
assert!(reader.chunk_count() > 0);
tmp.as_file().set_len(1024).unwrap();
let mut buf = [0u8; 512];
let _ = reader.read(&mut buf);
}
#[test]
fn verify_truncated_image_handles_decompression_error() {
let src_path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/nps-2010-emails.E01");
let src_data = std::fs::read(src_path).unwrap();
let mut tmp = tempfile::Builder::new().suffix(".E01").tempfile().unwrap();
tmp.write_all(&src_data).unwrap();
tmp.flush().unwrap();
let mut reader = EwfReader::open(tmp.path()).unwrap();
tmp.as_file().set_len(1024).unwrap();
let result = reader.verify();
match result {
Ok(v) => {
assert_ne!(v.md5_match, Some(true),
"truncated image should not verify as MD5 match");
}
Err(_) => {
}
}
}
}