From 115b19bdcb0a3c189e4f323d0795708274dbad94 Mon Sep 17 00:00:00 2001 From: eliets2 <287273517+eliets2@users.noreply.github.com> Date: Tue, 13 Oct 2026 10:40:45 +0300 Subject: [PATCH] LR-IMP-FORMATS: fuzz structurally valid CR3 containers in the robustness tests crates/raw/tests/robust.rs only mutated DNGs. Add two small valid CR3 fixtures (a 4x4 lossless CRX and a 16x16 three-level C-RAW CRX) and a property that flips bytes in them or truncates them, then runs the existing exercise() helper (probe, embedded_preview, decode, develop and the colour transform) plus probe_info, the header-only description used on import: errors or clean decodes, never a panic. Unlike random TIFF-like bytes, a structurally valid file gets past the container walk, so the mutations land in the CRX marker walker and entropy readers. A fixture test asserts that both files decode as built, so the mutations hit live decode paths. Tests only; the existing DNG property keeps its 400 cases and the new one uses proptest's default (256, PROPTEST_CASES overrides it). --- crates/raw/tests/robust.rs | 139 +++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/crates/raw/tests/robust.rs b/crates/raw/tests/robust.rs index 7bd5a5e54..f2e6426bf 100644 --- a/crates/raw/tests/robust.rs +++ b/crates/raw/tests/robust.rs @@ -102,3 +102,142 @@ fn hostile_dimensions_are_rejected_quickly() { assert!(lightcraft_raw::decode(&bytes).is_err()); assert!(t0.elapsed().as_secs_f64() < 2.0); } + +// --- 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 { + 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 { + 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 { + 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 { + 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, 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 { + 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 { + 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 { + 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 { + 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::(), any::()), 1..24), + cut in any::(), + ) { + 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]); + } +}