#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::cast_possible_truncation,
clippy::identity_op
)]
use ad1::testfix;
use ad1::{Ad1Error, Ad1Reader};
use std::path::PathBuf;
const MARGIN: usize = 512;
fn write_one(bytes: &[u8]) -> (tempfile::TempDir, PathBuf) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("m.ad1");
std::fs::write(&path, bytes).unwrap();
(dir, path)
}
fn rd_u64(b: &[u8], phys: usize) -> u64 {
u64::from_le_bytes(b[phys..phys + 8].try_into().unwrap())
}
fn put_u64(b: &mut [u8], phys: usize, v: u64) {
b[phys..phys + 8].copy_from_slice(&v.to_le_bytes());
}
fn put_u32(b: &mut [u8], phys: usize, v: u32) {
b[phys..phys + 4].copy_from_slice(&v.to_le_bytes());
}
fn phys(logical: u64) -> usize {
MARGIN + logical as usize
}
fn item_offset(bytes: &[u8], name: &str) -> u64 {
let first = rd_u64(bytes, phys(0) + 0x24); let mut stack = vec![first];
while let Some(addr) = stack.pop() {
if addr == 0 {
continue;
}
let base = phys(addr);
let name_len =
u32::from_le_bytes(bytes[base + 0x2c..base + 0x30].try_into().unwrap()) as usize;
let nm = String::from_utf8_lossy(&bytes[base + 0x30..base + 0x30 + name_len]);
if nm == name {
return addr;
}
let next = rd_u64(bytes, base + 0x00);
let child = rd_u64(bytes, base + 0x08);
stack.push(next);
stack.push(child);
}
panic!("item {name} not found");
}
#[test]
fn zero_fragments_size_is_rejected() {
let built = testfix::build(testfix::sample_tree());
let mut bytes = built.bytes;
put_u32(&mut bytes, 0x22, 0); let (_d, p) = write_one(&bytes);
assert!(matches!(Ad1Reader::open(&p), Err(Ad1Error::Malformed(_))));
}
#[test]
fn zero_chunk_size_is_rejected() {
let built = testfix::build(testfix::sample_tree());
let mut bytes = built.bytes;
put_u32(&mut bytes, 0x218, 0); let (_d, p) = write_one(&bytes);
assert!(matches!(Ad1Reader::open(&p), Err(Ad1Error::Malformed(_))));
}
#[test]
fn implausible_chunk_size_is_rejected() {
let built = testfix::build(testfix::sample_tree());
let mut bytes = built.bytes;
put_u32(&mut bytes, 0x218, u32::MAX); let (_d, p) = write_one(&bytes);
assert!(matches!(Ad1Reader::open(&p), Err(Ad1Error::Malformed(_))));
}
#[test]
fn truncated_header_is_not_ad1() {
let (_d, p) = write_one(b"AD"); assert!(matches!(Ad1Reader::open(&p), Err(Ad1Error::NotAd1(_))));
}
#[test]
fn oversize_item_name_is_rejected() {
let built = testfix::build(testfix::sample_tree());
let off = item_offset(&built.bytes, "hello.txt");
let mut bytes = built.bytes;
put_u32(&mut bytes, phys(off) + 0x2c, 100_000); let (_d, p) = write_one(&bytes);
assert!(matches!(Ad1Reader::open(&p), Err(Ad1Error::Malformed(_))));
}
#[test]
fn tree_cycle_is_detected() {
let built = testfix::build(testfix::sample_tree());
let first = rd_u64(&built.bytes, phys(0) + 0x24);
let mut bytes = built.bytes;
put_u64(&mut bytes, phys(first) + 0x00, first);
let (_d, p) = write_one(&bytes);
match Ad1Reader::open(&p) {
Err(Ad1Error::Malformed(m)) => assert!(m.contains("cycle"), "{m}"),
other => panic!("expected cycle Malformed, got {other:?}"),
}
}
#[test]
fn oversize_metadata_payload_is_rejected() {
let built = testfix::build(testfix::sample_tree());
let off = item_offset(&built.bytes, "hello.txt");
let meta_addr = rd_u64(&built.bytes, phys(off) + 0x10);
let mut bytes = built.bytes;
put_u32(&mut bytes, phys(meta_addr) + 0x10, 10_000_000);
let (_d, p) = write_one(&bytes);
assert!(matches!(Ad1Reader::open(&p), Err(Ad1Error::Malformed(_))));
}
#[test]
fn zero_chunk_count_on_a_sized_file_is_rejected() {
let built = testfix::build(testfix::sample_tree());
let off = item_offset(&built.bytes, "a.bin");
let zlib_addr = rd_u64(&built.bytes, phys(off) + 0x18);
let mut bytes = built.bytes;
put_u64(&mut bytes, phys(zlib_addr), 0); let (_d, p) = write_one(&bytes);
let r = Ad1Reader::open(&p).unwrap();
let entry = r
.entries()
.iter()
.find(|e| e.path == "root/sub/a.bin")
.unwrap();
let mut buf = vec![0u8; 64];
assert!(matches!(
r.read_at(entry, 0, &mut buf),
Err(Ad1Error::Malformed(_))
));
}
#[test]
fn max_chunk_count_does_not_overflow() {
let built = testfix::build(testfix::sample_tree());
let off = item_offset(&built.bytes, "a.bin");
let zlib_addr = rd_u64(&built.bytes, phys(off) + 0x18);
let mut bytes = built.bytes;
put_u64(&mut bytes, phys(zlib_addr), u64::MAX);
let (_d, p) = write_one(&bytes);
let r = Ad1Reader::open(&p).unwrap();
let entry = r
.entries()
.iter()
.find(|e| e.path == "root/sub/a.bin")
.unwrap();
let mut buf = vec![0u8; 64];
assert!(matches!(
r.read_at(entry, 0, &mut buf),
Err(Ad1Error::Malformed(_))
));
}
#[test]
fn corrupt_compressed_chunk_errors_on_read() {
let built = testfix::build(testfix::sample_tree());
let off = item_offset(&built.bytes, "a.bin");
let zlib_addr = rd_u64(&built.bytes, phys(off) + 0x18);
let chunk0 = rd_u64(&built.bytes, phys(zlib_addr) + 0x08); let mut bytes = built.bytes;
bytes[phys(chunk0)] = 0xff;
bytes[phys(chunk0) + 1] = 0xff;
let (_d, p) = write_one(&bytes);
let r = Ad1Reader::open(&p).unwrap();
let entry = r
.entries()
.iter()
.find(|e| e.path == "root/sub/a.bin")
.unwrap();
let mut buf = vec![0u8; 1024];
assert!(r.read_at(entry, 0, &mut buf).is_err());
}
#[test]
fn non_monotonic_chunk_addresses_are_rejected() {
let built = testfix::build(testfix::sample_tree());
let off = item_offset(&built.bytes, "a.bin");
let zlib_addr = rd_u64(&built.bytes, phys(off) + 0x18);
let mut bytes = built.bytes;
put_u64(&mut bytes, phys(zlib_addr) + 0x10, 0);
let (_d, p) = write_one(&bytes);
let r = Ad1Reader::open(&p).unwrap();
let entry = r
.entries()
.iter()
.find(|e| e.path == "root/sub/a.bin")
.unwrap();
let mut buf = vec![0u8; 1024];
assert!(matches!(
r.read_at(entry, 0, &mut buf),
Err(Ad1Error::Malformed(_))
));
}
#[test]
fn sized_file_without_chunk_table_is_rejected() {
let built = testfix::build(testfix::sample_tree());
let off = item_offset(&built.bytes, "a.bin");
let mut bytes = built.bytes;
put_u64(&mut bytes, phys(off) + 0x18, 0); let (_d, p) = write_one(&bytes);
let r = Ad1Reader::open(&p).unwrap();
let entry = r
.entries()
.iter()
.find(|e| e.path == "root/sub/a.bin")
.unwrap();
let mut buf = vec![0u8; 64];
assert!(matches!(
r.read_at(entry, 0, &mut buf),
Err(Ad1Error::Malformed(_))
));
}
#[test]
fn metadata_chain_cycle_is_tolerated() {
let built = testfix::build(testfix::sample_tree());
let off = item_offset(&built.bytes, "hello.txt");
let meta_addr = rd_u64(&built.bytes, phys(off) + 0x10);
let mut bytes = built.bytes;
put_u64(&mut bytes, phys(meta_addr) + 0x00, meta_addr);
let (_d, p) = write_one(&bytes);
assert!(Ad1Reader::open(&p).is_ok());
}
#[test]
fn chunk_length_exceeding_image_size_is_rejected() {
let built = testfix::build(testfix::sample_tree());
let off = item_offset(&built.bytes, "a.bin");
let zlib_addr = rd_u64(&built.bytes, phys(off) + 0x18);
let mut bytes = built.bytes;
put_u64(&mut bytes, phys(zlib_addr) + 0x10, u64::MAX / 2);
let (_d, p) = write_one(&bytes);
let r = Ad1Reader::open(&p).unwrap();
let entry = r
.entries()
.iter()
.find(|e| e.path == "root/sub/a.bin")
.unwrap();
let mut buf = vec![0u8; 64];
assert!(matches!(
r.read_at(entry, 0, &mut buf),
Err(Ad1Error::Malformed(_))
));
}
#[test]
fn absurd_segment_count_is_rejected_not_allocated() {
let built = testfix::build(testfix::sample_tree());
let mut bytes = built.bytes;
put_u32(&mut bytes, 0x1c, 100_000); let (_d, p) = write_one(&bytes);
assert!(matches!(Ad1Reader::open(&p), Err(Ad1Error::Malformed(_))));
}
#[test]
fn short_non_last_chunk_does_not_panic() {
use flate2::write::ZlibEncoder;
use flate2::Compression;
use std::io::Write as _;
let built = testfix::build(testfix::sample_tree());
let off = item_offset(&built.bytes, "a.bin");
let zlib_addr = rd_u64(&built.bytes, phys(off) + 0x18);
let chunk0 = rd_u64(&built.bytes, phys(zlib_addr) + 0x08);
let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
enc.write_all(&[0u8; 16]).unwrap();
let short = enc.finish().unwrap();
let mut bytes = built.bytes;
bytes[phys(chunk0)..phys(chunk0) + short.len()].copy_from_slice(&short);
let (_d, p) = write_one(&bytes);
let r = Ad1Reader::open(&p).unwrap();
let entry = r
.entries()
.iter()
.find(|e| e.path == "root/sub/a.bin")
.unwrap();
let mut buf = vec![0u8; entry.size as usize];
let n = r.read_at(entry, 0, &mut buf).unwrap(); assert!(
n < entry.size as usize,
"short chunk should yield a short read"
);
}
#[test]
fn read_at_directory_and_empty_file_yield_zero() {
let built = testfix::build(testfix::sample_tree());
let (_d, p) = write_one(&built.bytes);
let r = Ad1Reader::open(&p).unwrap();
let dir = r.entries().iter().find(|e| e.path == "root/sub").unwrap();
let empty = r
.entries()
.iter()
.find(|e| e.path == "root/sub/empty.dat")
.unwrap();
let mut buf = [0u8; 8];
assert_eq!(r.read_at(dir, 0, &mut buf).unwrap(), 0);
assert_eq!(r.read_at(empty, 0, &mut buf).unwrap(), 0);
assert_eq!(r.read_at(empty, 0, &mut []).unwrap(), 0); }