mod builder;
use builder::{
adler32, make_ewf2_descriptor, make_ewf2_file_header, EVF2_SECTION_TYPE_CHUNK_TABLE,
EVF2_SECTION_TYPE_DONE, EVF2_SECTION_TYPE_MD5_HASH,
};
use ewf_forensic::{EwfIntegrity, EwfIntegrityAnomaly};
use md5::Digest as _;
const CHUNK_TABLE_HEADER_SIZE: usize = 32;
const CHUNK_TABLE_ENTRY_SIZE: usize = 16;
fn make_segment_with_chunk_at(chunk_data: &[u8], chunk_offset: usize) -> Vec<u8> {
assert!(chunk_offset >= 32, "chunk must be after file header");
let mut buf = Vec::new();
buf.extend_from_slice(&make_ewf2_file_header(1));
buf.resize(chunk_offset, 0u8);
buf.extend_from_slice(chunk_data);
let crc = adler32(chunk_data);
buf.extend_from_slice(&crc.to_le_bytes());
let entry_data_size = (chunk_data.len() as u32) + 4;
let ct_body_start = buf.len();
let mut header = [0u8; CHUNK_TABLE_HEADER_SIZE];
header[8..16].copy_from_slice(&1u64.to_le_bytes());
buf.extend_from_slice(&header);
let offset_u64 = chunk_offset as u64;
let mut entry = [0u8; CHUNK_TABLE_ENTRY_SIZE];
entry[0..8].copy_from_slice(&offset_u64.to_le_bytes()); entry[8..12].copy_from_slice(&entry_data_size.to_le_bytes());
buf.extend_from_slice(&entry);
buf.extend_from_slice(&adler32(&entry).to_le_bytes());
let ct_body_len = (buf.len() - ct_body_start) as u64;
let ct_desc_off = buf.len() as u64;
buf.extend_from_slice(&make_ewf2_descriptor(
EVF2_SECTION_TYPE_CHUNK_TABLE,
0,
0,
ct_body_len,
[0u8; 16],
));
let stored_md5: [u8; 16] = md5::Md5::digest(chunk_data).into();
buf.extend_from_slice(&stored_md5);
buf.extend_from_slice(&[0u8; 16]);
let md5_desc_off = buf.len() as u64;
buf.extend_from_slice(&make_ewf2_descriptor(
EVF2_SECTION_TYPE_MD5_HASH,
0,
ct_desc_off,
32,
[0u8; 16],
));
buf.extend_from_slice(&make_ewf2_descriptor(
EVF2_SECTION_TYPE_DONE,
0,
md5_desc_off,
0,
[0u8; 16],
));
buf
}
#[test]
fn ewf2_chunk_at_small_offset_no_hash_mismatch() {
let chunk = [0u8; 512];
let seg = make_segment_with_chunk_at(&chunk, 128);
let findings = EwfIntegrity::new(&seg).analyse();
assert!(
!findings
.iter()
.any(|a| matches!(a, EwfIntegrityAnomaly::HashMismatch { .. })),
"chunk at offset 128 must not produce HashMismatch; got: {findings:#?}"
);
}
#[test]
fn ewf2_chunk_entry_adler32_covers_full_u64() {
let chunk = b"structural_correctness_test".to_vec();
let seg = make_segment_with_chunk_at(&chunk, 256);
let findings = EwfIntegrity::new(&seg).analyse();
assert!(
!findings.iter().any(|a| matches!(
a,
EwfIntegrityAnomaly::Ewf2ChunkTableChecksumMismatch { .. }
)),
"chunk table Adler-32 must be correct for full-u64 encoded entry; got: {findings:#?}"
);
}
#[test]
fn ewf2_chunk_at_64k_offset_no_hash_mismatch() {
let chunk = vec![0xABu8; 512];
let seg = make_segment_with_chunk_at(&chunk, 65536);
let findings = EwfIntegrity::new(&seg).analyse();
assert!(
!findings
.iter()
.any(|a| matches!(a, EwfIntegrityAnomaly::HashMismatch { .. })),
"chunk at 64k offset must not produce HashMismatch; got: {findings:#?}"
);
}