#![allow(clippy::unwrap_used, clippy::expect_used)]
use aff4::{container_kind, Aff4Reader, ContainerKind};
use md5::Digest as _;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
fn corpus(name: &str) -> std::path::PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/data")
.join(name)
}
#[test]
fn corpus_base_linear_is_disk() {
let path = corpus("Base-Linear.aff4");
if !path.exists() {
return;
}
assert_eq!(container_kind(&path).unwrap(), ContainerKind::Disk);
}
#[test]
fn corpus_base_linear_virtual_disk_size() {
let path = corpus("Base-Linear.aff4");
if !path.exists() {
return;
}
let reader = Aff4Reader::open(&path).expect("open Base-Linear.aff4");
assert_eq!(
reader.virtual_disk_size(),
268_435_456_u64,
"virtual_disk_size must come from the aff4:Map block (268435456 = 256 MiB)"
);
}
#[test]
fn corpus_base_linear_sector0_is_mbr() {
let path = corpus("Base-Linear.aff4");
if !path.exists() {
return;
}
let mut reader = Aff4Reader::open(&path).expect("open");
let mut buf = [0u8; 512];
reader
.read_exact(&mut buf)
.expect("sector 0 must be readable");
assert_eq!(
(buf[510], buf[511]),
(0x55, 0xAA),
"virtual sector 0 maps to ImageStream chunk 0 (a real MBR); the boot \
signature 0x55AA must appear at offset 510, not zeros"
);
assert_ne!(
buf, [0u8; 512],
"sector 0 is an MBR, not a sparse zero region"
);
}
#[test]
fn corpus_base_linear_snappy_chunk_matches_reference() {
let path = corpus("Base-Linear.aff4");
if !path.exists() {
return;
}
let reference = reference_bytes_via_zip_snap(&path, 65536, 512);
let mut reader = Aff4Reader::open(&path).expect("open");
reader
.seek(SeekFrom::Start(98304))
.expect("seek to first non-sparse virtual region");
let mut buf = vec![0u8; 512];
reader
.read_exact(&mut buf)
.expect("first non-sparse Snappy chunk must be readable");
assert_eq!(
&buf,
&reference[..],
"bytes at virtual offset 98304 must match Snappy-decompressed ImageStream chunk 2"
);
}
fn reference_bytes_via_zip_snap(path: &Path, offset: u64, len: usize) -> Vec<u8> {
use zip::ZipArchive;
let file = std::fs::File::open(path).expect("open");
let mut archive = ZipArchive::new(file).expect("zip");
let segment0_suffix = format!("/{:08x}", 0u32);
let bevy_name = archive
.file_names()
.find(|n| n.ends_with(&segment0_suffix) && !n.contains('.'))
.expect("no segment 0 bevy found")
.to_string();
let index_name = format!("{bevy_name}.index");
let index_bytes = read_zip_entry(&mut archive, &index_name);
let bevy_bytes = read_zip_entry(&mut archive, &bevy_name);
let chunk_size = 32768usize;
let chunk_idx = (offset as usize) / chunk_size;
let offset_in_chunk = (offset as usize) % chunk_size;
let entry_size = 12usize;
let base = chunk_idx * entry_size;
let start = u64::from_le_bytes(index_bytes[base..base + 8].try_into().unwrap()) as usize;
let length = u32::from_le_bytes(index_bytes[base + 8..base + 12].try_into().unwrap()) as usize;
let end = start + length;
let compressed = &bevy_bytes[start..end];
let mut dec = snap::raw::Decoder::new();
let decompressed = dec
.decompress_vec(compressed)
.expect("snap::raw decompress failed");
decompressed[offset_in_chunk..offset_in_chunk + len].to_vec()
}
fn read_zip_entry(archive: &mut zip::ZipArchive<std::fs::File>, name: &str) -> Vec<u8> {
let mut entry = archive.by_name(name).expect("zip entry not found");
let mut data = Vec::new();
entry.read_to_end(&mut data).expect("read zip entry");
data
}
#[test]
fn corpus_exabyte_sparse_virtual_size() {
let path = corpus("Base-ExabyteSparse.aff4");
if !path.exists() {
return;
}
let reader = Aff4Reader::open(&path).expect("open Base-ExabyteSparse.aff4");
assert_eq!(
reader.virtual_disk_size(),
9_223_372_036_854_775_296_u64,
"virtual_disk_size must come from aff4:Map block (size=9223372036854775296), \
not from the inner ImageStream block (size=4718592)"
);
}
#[test]
fn corpus_base_allocated_virtual_size() {
let path = corpus("Base-Allocated.aff4");
if !path.exists() {
return;
}
let reader = Aff4Reader::open(&path).expect("open Base-Allocated.aff4");
assert_eq!(
reader.virtual_disk_size(),
268_435_456_u64,
"virtual_disk_size must come from aff4:Map block (268435456), \
not from the inner ImageStream block (3964928)"
);
}
#[test]
fn corpus_allhashes_stored_image_hashes_parsed() {
let path = corpus("Base-Linear-AllHashes.aff4");
if !path.exists() {
return;
}
let reader = Aff4Reader::open(&path).expect("open");
let hashes = reader.stored_image_hashes();
let algos: std::collections::BTreeSet<String> = hashes
.iter()
.map(|h| h.algorithm.to_ascii_uppercase())
.collect();
for a in ["MD5", "SHA1", "SHA256", "SHA512", "BLAKE2B"] {
assert!(algos.contains(a), "missing stored {a}; got {algos:?}");
}
let md5 = hashes
.iter()
.find(|h| h.algorithm.eq_ignore_ascii_case("MD5"))
.expect("MD5 present");
assert_eq!(
md5.hex, "d5825dc1152a42958c8219ff11ed01a3",
"stored MD5 (Evimetry-authored) must be parsed verbatim"
);
}
#[test]
fn corpus_allhashes_image_stream_content_is_real() {
let path = corpus("Base-Linear-AllHashes.aff4");
if !path.exists() {
return;
}
let mut reader = Aff4Reader::open(&path).expect("open");
assert_eq!(reader.image_stream_size(), 3_964_928);
let mut total = 0u64;
let mut head: Vec<u8> = Vec::new();
reader
.read_image_stream_content(|c| {
if head.len() < 512 {
let want = 512 - head.len();
head.extend_from_slice(&c[..c.len().min(want)]);
}
total += c.len() as u64;
})
.expect("read ImageStream content");
assert_eq!(
total, 3_964_928,
"ImageStream content length must equal aff4:size"
);
assert_eq!(
(head[510], head[511]),
(0x55, 0xAA),
"ImageStream content begins with the MBR (boot signature at 510)"
);
}
fn read_at(reader: &mut Aff4Reader, offset: u64, len: usize) -> Vec<u8> {
reader.seek(SeekFrom::Start(offset)).expect("seek");
let mut buf = vec![0u8; len];
reader.read_exact(&mut buf).expect("read");
buf
}
#[test]
fn corpus_allocated_unknown_data_tile() {
let path = corpus("Base-Allocated.aff4");
if !path.exists() {
return;
}
let mut reader = Aff4Reader::open(&path).expect("open");
assert_eq!(
read_at(&mut reader, 17_825_792, 16),
b"UNKNOWNUNKNOWNUN".to_vec(),
"aff4:UnknownData region must yield the 'UNKNOWN' tile, not zeros"
);
}
#[test]
fn corpus_allocated_unknown_data_seam() {
let path = corpus("Base-Allocated.aff4");
if !path.exists() {
return;
}
let mut reader = Aff4Reader::open(&path).expect("open");
assert_eq!(
read_at(&mut reader, 18_874_365, 8),
b"NKNUNKNO".to_vec(),
"UnknownData tile must reset on the 1 MiB seam (pyaff4 readptr % tilesize)"
);
}
#[test]
fn corpus_linear_symbolic_stream_61() {
let path = corpus("Base-Linear.aff4");
if !path.exists() {
return;
}
let mut reader = Aff4Reader::open(&path).expect("open");
assert_eq!(
read_at(&mut reader, 265_355_264, 8),
vec![0x61u8; 8],
"SymbolicStream61 must fill with byte 0x61, not zeros"
);
}
#[test]
fn aff4l_dream_lists_and_reads_against_pyaff4() {
let Some(path) = std::env::var_os("AFF4L_DREAM") else {
return;
};
let path = std::path::PathBuf::from(path);
if !path.exists() {
return;
}
let mut container = aff4::LogicalContainer::open(&path).expect("open dream.aff4");
let files = container.files().to_vec();
let dream = files
.iter()
.find(|f| f.original_file_name.ends_with("dream.txt"))
.expect("dream.txt logical entry");
assert_eq!(dream.size, 8688, "dream.txt logical size (aff4:size)");
assert!(
dream
.hashes
.iter()
.any(|h| h.algorithm.eq_ignore_ascii_case("MD5")
&& h.hex == "75d83773f8d431a3ca91bfb8859e486d"),
"stored MD5 must be the pyaff4-recorded digest"
);
let bytes = container.read_file(dream).expect("read dream.txt");
assert_eq!(bytes.len(), 8688, "content length matches aff4:size");
let md5 = format!("{:x}", md5::Md5::digest(&bytes));
assert_eq!(
md5, "75d83773f8d431a3ca91bfb8859e486d",
"recomputed MD5 of the logical content must match the stored aff4:hash"
);
assert!(
bytes.starts_with(b"I have a Dream"),
"content must be the MLK speech text"
);
}