# Conflicts:
#	crates/raw/tests/robust.rs
This commit is contained in:
Brandon Thomas
2026-10-14 22:29:11 -04:00
2 changed files with 1187 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+139
View File
@@ -169,3 +169,142 @@ fn non_finite_black_levels_and_samples_develop() {
assert!(rgb.data.iter().flatten().all(|v| v.is_finite()), "float NaN samples: finite output");
}
}
// --- CR3 containers (Canon CRX): structurally valid files whose every byte may be mutated ---
/// A CR3 with a single CRAW track carrying a CMP1 descriptor (and optional CDI1/IAD1) plus raw samples.
mod cr3 {
pub(crate) fn box_bytes(kind: &[u8; 4], body: &[u8]) -> Vec<u8> {
let mut out = ((body.len() + 8) as u32).to_be_bytes().to_vec();
out.extend_from_slice(kind);
out.extend_from_slice(body);
out
}
fn track(entry: &[u8], offset: u64, size: u32) -> Vec<u8> {
let mut stsd = vec![0u8; 4];
stsd.extend(1u32.to_be_bytes());
stsd.extend_from_slice(entry);
let mut stsz = vec![0u8; 4];
stsz.extend(size.to_be_bytes());
stsz.extend(1u32.to_be_bytes());
let mut co64 = vec![0u8; 4];
co64.extend(1u32.to_be_bytes());
co64.extend(offset.to_be_bytes());
let tables = [box_bytes(b"stsd", &stsd), box_bytes(b"stsz", &stsz), box_bytes(b"co64", &co64)].concat();
box_bytes(b"trak", &box_bytes(b"mdia", &box_bytes(b"minf", &box_bytes(b"stbl", &tables))))
}
/// `CMP1` coding descriptor: 52 bytes, four planes, 14 bits, RGGB, `levels` wavelet levels.
pub(crate) fn cmp1(width: u32, height: u32, tile_width: u32, tile_height: u32, levels: u8, header_size: u32) -> Vec<u8> {
let mut coding = vec![0u8; 52];
coding[0..2].copy_from_slice(&0xff00u16.to_be_bytes());
coding[2..4].copy_from_slice(&48u16.to_be_bytes());
coding[4..6].copy_from_slice(&0x100u16.to_be_bytes());
coding[8..12].copy_from_slice(&width.to_be_bytes());
coding[12..16].copy_from_slice(&height.to_be_bytes());
coding[16..20].copy_from_slice(&tile_width.to_be_bytes());
coding[20..24].copy_from_slice(&tile_height.to_be_bytes());
coding[24] = 14;
coding[25] = 0x40;
coding[26] = levels;
coding[28..32].copy_from_slice(&header_size.to_be_bytes());
coding
}
pub(crate) fn file(coding: &[u8], width: u16, height: u16, sample: &[u8]) -> Vec<u8> {
let mut craw = vec![0u8; 82];
craw[24..26].copy_from_slice(&width.to_be_bytes());
craw[26..28].copy_from_slice(&height.to_be_bytes());
craw.extend(box_bytes(b"CMP1", coding));
let entry = box_bytes(b"CRAW", &craw);
let ftyp = box_bytes(b"ftyp", b"crx \0\0\0\x01crx isom");
let track_len = track(&entry, 0, sample.len() as u32).len();
let offset = (ftyp.len() + 8 + track_len + 7) & !7;
let mut out = ftyp;
out.extend(box_bytes(b"moov", &track(&entry, offset as u64, sample.len() as u32)));
out.resize(offset, 0);
out.extend_from_slice(sample);
out
}
fn marker(out: &mut Vec<u8>, code: u16, size: u32, flags: u32) {
out.extend(code.to_be_bytes());
out.extend(8u16.to_be_bytes());
out.extend(size.to_be_bytes());
out.extend(flags.to_be_bytes());
}
/// A 4×4 lossless CRX stream: four 2×2 planes, each sample value 1 (sensor level 8193).
pub(crate) fn lossless_sample() -> Vec<u8> {
let mut sample = Vec::new();
marker(&mut sample, 0xff01, 4, 0);
for p in 0..4u32 {
marker(&mut sample, 0xff02, 1, (p << 28) | 0x0800_0000);
marker(&mut sample, 0xff03, 1, 4 << 19);
}
sample.extend([0x1e; 4]);
sample
}
pub(crate) fn lossless_file() -> Vec<u8> {
file(&cmp1(4, 4, 4, 4, 0, 108), 4, 4, &lossless_sample())
}
/// A 16×16 three-level C-RAW CRX stream of flat zero subbands (sensor level 8192).
pub(crate) fn craw_sample() -> Vec<u8> {
let mut sample = Vec::new();
marker(&mut sample, 0xff01, 52, 0);
for plane in 0..4u32 {
marker(&mut sample, 0xff02, 13, (plane << 28) | 0x0800_0000);
for band in 0..10u32 {
marker(&mut sample, 0xff03, if band >= 7 { 2 } else { 1 }, (band << 28) | (4 << 19));
}
}
for _ in 0..4 {
sample.extend([0x80; 4]);
sample.extend([0xf0; 3]);
for _ in 0..3 {
sample.extend([0xff, 0xf8]);
}
}
sample
}
pub(crate) fn craw_file() -> Vec<u8> {
file(&cmp1(16, 16, 16, 16, 3, 540), 16, 16, &craw_sample())
}
}
/// Both fixtures decode as built, so the mutations below hit live decode paths.
#[test]
fn cr3_fixtures_decode() {
let lossless = lightcraft_raw::decode(&cr3::lossless_file()).unwrap();
assert_eq!((lossless.width, lossless.height, lossless.bits), (4, 4, 14));
assert_eq!(lossless.data.get(0), 8193.0);
let craw = lightcraft_raw::decode(&cr3::craw_file()).unwrap();
assert_eq!((craw.width, craw.height, craw.bits), (16, 16, 14));
assert_eq!(craw.data.get(0), 8192.0);
assert_eq!(lightcraft_raw::probe_info(&cr3::craw_file()).unwrap(), craw.info());
}
proptest! {
/// Mutating or truncating structurally valid CR3 containers exercises the CRX marker walker and
/// entropy readers far deeper than random TIFF bytes: errors or clean decodes, never a panic.
#[test]
fn mutated_cr3_containers_never_panic(
kind in 0usize..2,
flips in proptest::collection::vec((any::<usize>(), any::<u8>()), 1..24),
cut in any::<usize>(),
) {
let mut data = if kind == 0 { cr3::lossless_file() } else { cr3::craw_file() };
for (i, v) in flips {
let n = data.len();
data[i % n] = v;
}
let keep = if cut % 3 == 0 { cut % data.len() } else { data.len() };
exercise(&data[..keep]);
// The header-only description path (used when importing) must hold up against the same mutations.
let _ = lightcraft_raw::probe_info(&data[..keep]);
}
}