Files
lightcraft/crates/raw/tests/crx_properties.rs
Pierre-Edouard HonnetandClaude Opus 5.5 c806debada LR-IMP-FORMATS: CRX high-frequency band width from the tile geometry (#899)
The decoder tried each high-frequency band at its synthesis width and up to
two more columns, keeping the first width whose entropy stream ended exactly.
Two widths can both do that with different coefficients (#899).

Measured on every C-RAW plane in cr3_corpus.rs (CC0 raw.pixls.us files):
each band decodes at exactly one width. A tile with a neighbour on its right
codes bands 5 and 8 (the vertical high-pass of the level-2 and level-1
low-pass columns) at an odd width, so an even band gains one unused column;
no other band is extended. The width now comes from that rule, and a stream
that does not fit it is corrupt (the CR3 falls back to its preview).

All corpus sensor hashes are unchanged. The property-test encoder now codes
bands 5 and 8 the same way. Middle tiles (three or more C-RAW tiles) follow
the left-tile rule; no public file has one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:12:46 +02:00

1061 lines
48 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Property-based invariants of the Canon CRX decoder, exercised through the public decode path.
//!
//! `vendor/crx` and the 5/3 synthesis in `vendor/crx_wavelet` are `pub(crate)`, so every property here
//! wraps a synthetic CRX stream in a minimal CR3 container (ftyp + moov + a CRAW track with CMP1/CDI1)
//! and calls [`lightcraft_raw::decode`]. The CRX entropy streams are produced by reference encoders in
//! this file that mirror the documented coders (MED prediction, signed mapping, adaptive Rice, the
//! MELCODE zero run, the QP map predictor — sources cited in `crates/raw/src/vendor/crx.rs`).
//!
//! Wavelet roundtrip: the analysis side is this file's exact algebraic inverse of one inverse-lifting
//! line, so the property tests the crate's own synthesis. It is exact by construction because the
//! tile-edge halo high-pass coefficients are free parameters of the encoding — synthesis absorbs any
//! stored value through the neighbouring low-pass coefficient — so random planes of every reachable
//! parity (odd plane sizes via even tile sizes ≡ 2 mod 4) and multi-tile halo configuration must decode
//! back to exactly the encoded sensor samples.
//!
//! Properties:
//! 1. C-RAW wavelet roundtrip is exact across sizes, parities and tile halos.
//! 2. Quantization: every supported uniform quant value (4–44) and adaptive QP value (131–168)
//! dequantizes within the CMP1 bit depth — C-RAW clamps the overshoot, lossless rejects it — and
//! the values just outside those bands (quant 3 and 45, QP 130 and 169) are rejected.
//! 3. Rice/Golomb bounds: adversarial entropy prefixes, in the plane bands and in the QP map, error
//! out, never panic, and stay deterministic.
//! 4. IAD1 geometry: `active_area ⊆ sensor dims` and `crop ⊆ active_area` hold or the file is rejected.
//! 5. Decoding is deterministic across repeated and parallel invocations (rayon plane decode).
//! 6. Odd QP-map heights decode: the last QP row has no partner row and is used alone.
//!
//! The case budget is proptest's default (256 per property); the `PROPTEST_CASES` environment variable
//! overrides it.
use lightcraft_raw::{RawData, RawError, RawFormat, RawImage, decode, probe, probe_info};
use proptest::prelude::*;
// --- reference entropy coders (mirrors of the decoder's documented bitstream grammar) ---
/// MELCODE run-index table (crx.rs `RUN_EXPONENT`).
const RUN_EXPONENT: [u8; 32] = [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 9, 10, 11, 12, 13, 14, 15];
/// MSB-first bit writer; `into_bytes` zero-pads the final byte (the decoder's `finish` allows ≤ 7 zero tail bits).
#[derive(Default)]
struct BitWriter {
bytes: Vec<u8>,
filled: usize,
}
impl BitWriter {
fn put(&mut self, value: u32, count: u32) {
for shift in (0..count).rev() {
if self.filled.is_multiple_of(8) {
self.bytes.push(0);
}
if value.checked_shr(shift).unwrap_or(0) & 1 == 1 {
let byte = self.bytes.last_mut().unwrap();
*byte |= 0x80 >> (self.filled % 8);
}
self.filled += 1;
}
}
fn into_bytes(self) -> Vec<u8> {
self.bytes
}
}
/// Adaptive Rice code: ≤ 40-zero prefix, otherwise the 41-zero escape with a 21-bit literal (crx.rs `Bits::rice`).
fn put_rice(bits: &mut BitWriter, k: u32, symbol: u32) {
let zeros = symbol >> k;
if zeros <= 40 {
bits.put(0, zeros);
bits.put(1, 1);
bits.put(symbol & ((1 << k) - 1), k);
} else {
assert!(symbol < 1 << 21, "reference encoder literals are bounded by the plane precision");
bits.put(0, 41);
bits.put(1, 1);
bits.put(symbol & 0x001f_ffff, 21);
}
}
/// QP Rice code (crx.rs `qp_rice`), a limited-length code: a quotient below 23 is a plain Rice code, anything
/// larger is 23 zeros, the terminating 1 and the whole symbol in 8 bits.
fn put_qp_rice(bits: &mut BitWriter, k: u32, symbol: u32) {
let zeros = symbol >> k;
if zeros < 23 {
bits.put(0, zeros);
bits.put(1, 1);
bits.put(symbol & ((1 << k) - 1), k);
} else {
assert!(symbol < 1 << 8, "reference QP escapes carry an 8-bit symbol");
bits.put(0, 23);
bits.put(1, 1);
bits.put(symbol, 8);
}
}
/// Emits the MELCODE bits whose decode (`crx.rs run`) yields exactly `target` with the same run-index bookkeeping.
fn put_run(bits: &mut BitWriter, index: &mut usize, remaining: usize, target: usize) {
assert!(target >= 1 && target <= remaining);
let mut length = 1usize;
while length < remaining {
let exponent = u32::from(RUN_EXPONENT[*index]);
if target - length >= 1usize << exponent {
bits.put(1, 1);
length += 1usize << exponent;
if length <= remaining {
*index = (*index + 1).min(RUN_EXPONENT.len() - 1);
}
} else {
bits.put(0, 1);
bits.put((target - length) as u32, exponent);
*index = index.saturating_sub(1);
return;
}
}
assert_eq!(length, target, "a run reaching the row end needs no terminator");
}
/// Inverse of the decoder's signed mapping (crx.rs `signed`).
fn zig(value: i32) -> u32 {
((value << 1) ^ (value >> 31)) as u32
}
/// MED prediction (crx.rs `median`).
fn median(left: i32, above: i32, corner: i32) -> i32 {
if corner >= left.max(above) {
left.min(above)
} else if corner <= left.min(above) {
left.max(above)
} else {
left + above - corner
}
}
/// Adaptive Rice parameter update (crx.rs `update_rice`).
fn update_rice(mut k: u32, symbol: u32) -> u32 {
if symbol >= 3 << k {
k += 1;
if symbol >= 3 << k {
k += 1;
}
} else if k > 0 && symbol < 1 << (k - 1) {
k -= 1;
}
k.min(20)
}
/// Encodes a plane with the lossless coder: a zero run flag wherever the decoder reads one, then MED + Rice.
fn encode_lossless_plane(values: &[i32], width: usize, height: usize, bits: &mut BitWriter) {
let mut k = 0u32; // runs are never emitted, so the MELCODE run index stays unused here
let (mut previous, mut current) = (vec![0i32; width], vec![0i32; width]);
for y in 0..height {
let mut left = previous[0];
for x in 0..width {
let above = previous[x];
let right = previous.get(x + 1).copied().unwrap_or(above);
if x + 1 < width && left == above && left == right {
bits.put(0, 1); // a run is never required: regular prediction encodes any sample
}
let corner = if x == 0 { above } else { previous[x - 1] };
let symbol = zig(values[y * width + x] - median(left, above, corner));
put_rice(bits, k, symbol);
current[x] = values[y * width + x];
left = values[y * width + x];
let estimate = if y > 0 && x + 1 < width { (symbol + 2 * right.abs_diff(above)) / 2 } else { symbol };
k = update_rice(k, estimate);
}
std::mem::swap(&mut current, &mut previous);
}
}
/// Encodes a high-frequency band: zero-run flags in the all-zero context (forced for zero coefficients),
/// the nonzero-proven mapping after an interruption, and the upper-row Rice context (crx.rs `high_frequency_band`).
fn encode_high_frequency_band(values: &[i32], width: usize, height: usize, bits: &mut BitWriter) {
let (mut k, mut run_index) = (0u32, 0usize);
let (mut previous, mut current) = (vec![0i32; width], vec![0i32; width]);
let (mut previous_k, mut current_k) = (vec![0i32; width], vec![0i32; width]);
for y in 0..height {
let (mut x, mut left) = (0usize, 0i32);
while x < width {
let above = previous[x];
let right = previous.get(x + 1).copied().unwrap_or(above);
let mut nonzero = false;
if x + 1 < width && left == 0 && above == 0 && right == 0 {
if values[y * width + x] == 0 {
bits.put(1, 1); // a zero in this context must take the run path
let mut length = 1usize;
while x + length < width && values[y * width + x + length] == 0 {
length += 1;
}
put_run(bits, &mut run_index, width - x, length);
for slot in &mut current[x..x + length] {
*slot = 0;
}
for slot in &mut current_k[x..x + length] {
*slot = 0;
}
left = 0;
x += length;
if x == width {
continue;
}
nonzero = true; // the interruption proves the next coefficient nonzero
} else {
bits.put(0, 1);
nonzero = true;
}
}
let coded = zig(values[y * width + x]) - u32::from(nonzero);
put_rice(bits, k, coded);
current[x] = values[y * width + x];
left = values[y * width + x];
k = update_rice(k, coded);
if y > 0 && x + 1 < width && previous_k[x + 1] as u32 > k + 1 {
k += 1;
}
current_k[x] = k as i32;
x += 1;
}
std::mem::swap(&mut current, &mut previous);
std::mem::swap(&mut current_k, &mut previous_k);
}
}
/// Encodes an adaptive QP map: the first symbol as a 32-bit literal, then QP Rice codes over the MED
/// predictor (crx.rs `qp_map`).
fn encode_qp_map(values: &[i32], width: usize, height: usize) -> Vec<u8> {
let mut k = 0u32;
let (mut previous, mut current) = (vec![0i32; width], vec![0i32; width]);
let mut bits = BitWriter::default();
for y in 0..height {
let mut left = previous[0];
for x in 0..width {
let above = previous[x];
let corner = if x == 0 { above } else { previous[x - 1] };
let right = previous.get(x + 1).copied().unwrap_or(above);
let symbol = zig(values[y * width + x] - median(left, above, corner));
if x == 0 && y == 0 {
bits.put(symbol, 32);
} else {
put_qp_rice(&mut bits, k, symbol);
}
current[x] = values[y * width + x];
left = values[y * width + x];
let estimate = if y > 0 && x + 1 < width { (symbol + 2 * right.abs_diff(above)) / 2 } else { symbol };
k = update_rice(k, estimate);
}
std::mem::swap(&mut current, &mut previous);
}
bits.into_bytes()
}
// --- exact algebraic inverse of the crate's 5/3 synthesis (the forward transform under test) ---
#[derive(Clone)]
struct Band {
width: usize,
height: usize,
data: Vec<i32>,
}
/// Produces the (low, high) pair whose synthesis (`crx_wavelet::inverse_line`) reproduces `line` exactly.
///
/// Mirrors the inverse lifting: each odd sample fixes its stored high coefficient, each even sample its
/// stored low coefficient. The `before`/`after` halo coefficients are free parameters (any value works:
/// synthesis absorbs them through the boundary low coefficient), so they are supplied by the caller.
fn forward_lift(line: &[i32], before: bool, after: bool, halo_before: i32, halo_after: i32) -> (Vec<i32>, Vec<i32>) {
let n = line.len();
let high_count = n / 2 + usize::from(before) + usize::from(after);
let low_count = n.div_ceil(2) + usize::from(after && n.is_multiple_of(2));
if high_count == 0 {
return (line.to_vec(), Vec::new());
}
// The right prediction of the final odd sample of an even, right-halo line (`after`: even(i + 1)).
let extension = if after && n.is_multiple_of(2) { Some(2 * (line[n - 1] - halo_after) - line[n - 2]) } else { None };
let mut high = Vec::with_capacity(high_count);
if before {
high.push(halo_before);
}
for i in 0..n / 2 {
let right = match line.get(2 * i + 2) {
Some(&value) => value,
None => extension.unwrap_or(line[2 * i]), // no right neighbour: synthesis mirrors the last even sample
};
high.push(line[2 * i + 1] - (line[2 * i] + right).div_euclid(2));
}
if after {
high.push(halo_after);
}
let mut low = Vec::with_capacity(low_count);
let evens = n.div_ceil(2);
for i in 0..low_count {
let even_value = if i < evens { line[2 * i] } else { extension.unwrap_or_default() };
let left_index = (i + usize::from(before)).saturating_sub(1).min(high.len() - 1);
let right_index = (i + usize::from(before)).min(high.len() - 1);
low.push(even_value + (high[left_index] + high[right_index] + 2).div_euclid(4));
}
(low, high)
}
/// One inverse level, reversed: vertical lifting first (CRX analysis order), then horizontal, with the
/// same tile-edge flags the decoder passes for every level.
fn forward_level(plane: &[i32], width: usize, height: usize, before: bool, after: bool, halo: i32) -> (Band, Band, Band, Band) {
let (low_h, high_h) = (height.div_ceil(2), height / 2);
let mut temporary = vec![0i32; width * height];
for x in 0..width {
let column: Vec<i32> = (0..height).map(|y| plane[y * width + x]).collect();
let (lo, hi) = forward_lift(&column, false, false, halo, halo);
for (y, value) in lo.into_iter().chain(hi).enumerate() {
temporary[y * width + x] = value;
}
}
let low_w = width.div_ceil(2) + usize::from(after && width.is_multiple_of(2));
let high_w = width / 2 + usize::from(before) + usize::from(after);
let band = |w: usize, h: usize| Band { width: w, height: h, data: vec![0i32; w * h] };
let (mut ll, mut hl, mut lh, mut hh) = (band(low_w, low_h), band(high_w, low_h), band(low_w, high_h), band(high_w, high_h));
for y in 0..height {
let (lo, hi) = forward_lift(&temporary[y * width..(y + 1) * width], before, after, halo, halo);
let (low_band, high_band, by) = if y < low_h { (&mut ll, &mut hl, y) } else { (&mut lh, &mut hh, y - low_h) };
low_band.data[by * low_w..(by + 1) * low_w].copy_from_slice(&lo);
high_band.data[by * high_w..(by + 1) * high_w].copy_from_slice(&hi);
}
(ll, hl, lh, hh)
}
/// Subbands in CRX order: LL3, HL3, LH3, HH3, HL2, LH2, HH2, HL1, LH1, HH1.
fn forward_bands(plane: &[i32], width: usize, height: usize, before: bool, after: bool, halo: i32) -> Vec<Band> {
let (ll1, hl1, lh1, hh1) = forward_level(plane, width, height, before, after, halo);
let (ll2, hl2, lh2, hh2) = forward_level(&ll1.data, ll1.width, ll1.height, before, after, halo);
let (ll3, hl3, lh3, hh3) = forward_level(&ll2.data, ll2.width, ll2.height, before, after, halo);
vec![ll3, hl3, lh3, hh3, hl2, lh2, hh2, hl1, lh1, hh1]
}
/// Subband shapes the decoder derives (crx.rs `wavelet_plane` / `crx_wavelet::sizes`).
fn band_shapes(plane_width: usize, plane_height: usize, before: bool, after: bool) -> [(usize, usize); 10] {
let mut levels = [(0usize, 0usize, 0usize, 0usize); 3]; // (low_w, high_w, low_h, high_h), finest first
let (mut w, mut h) = (plane_width, plane_height);
for level in &mut levels {
let low_w = w.div_ceil(2) + usize::from(after && w % 2 == 0);
let high_w = w / 2 + usize::from(before) + usize::from(after);
*level = (low_w, high_w, h.div_ceil(2), h / 2);
w = low_w;
h = level.2;
}
let (lw1, hw1, lh1, hh1) = levels[0];
let (lw2, hw2, lh2, hh2) = levels[1];
let (lw3, hw3, lh3, hh3) = levels[2];
[(lw3, lh3), (hw3, lh3), (lw3, hh3), (hw3, hh3), (hw2, lh2), (lw2, hh2), (hw2, hh2), (hw1, lh1), (lw1, hh1), (hw1, hh1)]
}
/// The band as the camera codes it (crx.rs `coded_band_width`): with a tile on the right, bands 5 and 8
/// are coded at an odd width, so an even band gains a column that synthesis ignores (zero here).
fn coded_band(band: &Band, index: usize, after: bool) -> Band {
if !(after && matches!(index, 5 | 8) && band.width.is_multiple_of(2)) {
return band.clone();
}
let width = band.width + 1;
let data = band.data.chunks_exact(band.width).flat_map(|row| row.iter().copied().chain([0])).collect();
Band { width, height: band.height, data }
}
fn encode_band(band: &Band, lowpass: bool) -> Vec<u8> {
let mut bits = BitWriter::default();
if lowpass {
encode_lossless_plane(&band.data, band.width, band.height, &mut bits);
} else {
encode_high_frequency_band(&band.data, band.width, band.height, &mut bits);
}
bits.into_bytes()
}
/// The measured uniform quantization sequence (crx.rs `quant_step`).
fn quant_step(value: u8) -> i32 {
let scale = [40i32, 45, 51, 57, 64, 72][usize::from(value % 6)];
(scale << (value / 6)) / 64
}
// --- synthetic CR3 container and CRX sample assembly ---
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_box(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))))
}
/// Wraps a CRAW sample entry and its sample bytes in a minimal CR3 file (no metadata boxes: geometry
/// and levels come from the container and CMP1 alone).
fn cr3_file(entry: &[u8], sample: &[u8]) -> Vec<u8> {
let ftyp = box_bytes(b"ftyp", b"crx \0\0\0\x01crx isom");
// The track length does not depend on the offset value (always a 64-bit field), so it can be
// measured with a placeholder before the real offset is known.
let track_len = track_box(entry, 0, sample.len() as u32).len();
let offset = (ftyp.len() + 8 + track_len + 7) & !7;
let mut file = ftyp;
file.extend(box_bytes(b"moov", &track_box(entry, offset as u64, sample.len() as u32)));
file.resize(offset, 0);
file.extend_from_slice(sample);
file
}
fn cmp1(version: u16, 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(&version.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; // four planes, CFA pattern 0 (RGGB)
coding[26] = levels; // encoding 0
coding[28..32].copy_from_slice(&header_size.to_be_bytes());
coding
}
fn iad1_bytes(width: u16, height: u16, full: u16, crop: [u16; 4], masked_left: [u16; 4], masked_top: [u16; 4], active: [u16; 4]) -> Vec<u8> {
let mut area = vec![0u8; 48];
let mut put = |at: usize, value: u16| area[at..at + 2].copy_from_slice(&value.to_be_bytes());
put(4, width);
put(6, height);
put(10, full);
for (i, value) in crop.iter().enumerate() {
put(16 + 2 * i, *value);
}
for (i, value) in masked_left.iter().enumerate() {
put(24 + 2 * i, *value);
}
if full == 2 {
for (i, value) in masked_top.iter().enumerate() {
put(32 + 2 * i, *value);
}
for (i, value) in active.iter().enumerate() {
put(40 + 2 * i, *value);
}
}
area
}
fn craw_entry(coding: &[u8], width: u16, height: u16, iad1: Option<&[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));
if let Some(area) = iad1 {
craw.extend(box_bytes(b"CDI1", &[vec![0u8; 4], box_bytes(b"IAD1", area)].concat()));
}
box_bytes(b"CRAW", &craw)
}
fn put_marker(out: &mut Vec<u8>, code: u16, a: u32, b: u32) {
out.extend(code.to_be_bytes());
out.extend(8u16.to_be_bytes());
out.extend(a.to_be_bytes());
out.extend(b.to_be_bytes());
}
fn put_marker16(out: &mut Vec<u8>, code: u16, words: [u32; 4]) {
out.extend(code.to_be_bytes());
out.extend(16u16.to_be_bytes());
for word in words {
out.extend(word.to_be_bytes());
}
}
/// Assembles a version 0x100 sample: one ff01 tile header and per plane an ff02 header plus `bands`
/// ff03 headers, then the band bytes. `bands` is 1 (lossless) or 10 (three-level C-RAW).
fn v100_sample(tiles: &[[Vec<Vec<u8>>; 4]], quant: u8) -> (usize, Vec<u8>) {
let mut header = Vec::new();
let mut data = Vec::new();
for (index, planes) in tiles.iter().enumerate() {
let tile_size: usize = planes.iter().flatten().map(Vec::len).sum();
put_marker(&mut header, 0xff01, tile_size as u32, u32::from(index as u16) << 16);
for (p, bands) in planes.iter().enumerate() {
let plane_size: usize = bands.iter().map(Vec::len).sum();
put_marker(&mut header, 0xff02, plane_size as u32, (p as u32) << 28 | 0x0800_0000);
for (b, bytes) in bands.iter().enumerate() {
put_marker(&mut header, 0xff03, bytes.len() as u32, (b as u32) << 28 | u32::from(quant) << 19);
}
}
for bands in planes {
for bytes in bands {
data.extend_from_slice(bytes);
}
}
}
let header_size = header.len();
header.extend(data);
(header_size, header)
}
/// Assembles a version 0x200 single-tile sample: an ff11 header with the adaptive QP stream, ff12/ff13
/// headers with per-band base and gain, then QP data, its padding and the band bytes.
fn v200_sample(tile: &[Vec<Vec<u8>>; 4], gains: [u32; 10], bases: [u32; 10], qp: &[u8], qp_padding: usize) -> (usize, Vec<u8>) {
let band_bytes: usize = tile.iter().map(|bands| bands.iter().map(Vec::len).sum::<usize>()).sum();
let tile_size = qp.len() + qp_padding + band_bytes;
let mut header = Vec::new();
put_marker16(&mut header, 0xff11, [tile_size as u32, 0x4000, qp.len() as u32, u32::from(qp_padding as u16) << 16]);
for (p, bands) in tile.iter().enumerate() {
let plane_size: usize = bands.iter().map(Vec::len).sum();
put_marker(&mut header, 0xff12, plane_size as u32, (p as u32) << 28 | 0x0800_0000);
for (b, bytes) in bands.iter().enumerate() {
put_marker16(&mut header, 0xff13, [bytes.len() as u32, (b as u32) << 28 | gains[b], bases[b], 0]);
}
}
let header_size = header.len();
let mut data = header;
data.extend_from_slice(qp);
data.resize(data.len() + qp_padding, 0);
for bands in tile {
for bytes in bands {
data.extend_from_slice(bytes);
}
}
(header_size, data)
}
// --- fixture builders ---
/// A synthetic CRX stream plus its container, with the expected sensor samples when exact decoding is asserted.
struct Fixture {
file: Vec<u8>,
expected: Vec<u16>,
}
fn wrap(coding: &[u8], width: u32, height: u32, sample: &[u8], iad1: Option<&[u8]>) -> Vec<u8> {
cr3_file(&craw_entry(coding, width as u16, height as u16, iad1), sample)
}
/// Deterministic pseudo-random plane content for fixed regression-style files.
fn lcg_values(seed: u64, count: usize) -> Vec<i32> {
let mut state = seed;
(0..count)
.map(|_| {
state = state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
((state >> 33) % 16_384) as i32 - 8192
})
.collect()
}
/// Tile geometries with three non-empty wavelet levels: (tile width, tile height, tile count).
/// Plane sizes are half of each; odd plane sizes come from tile sizes ≡ 2 (mod 4).
const GEOMETRIES: [(usize, usize, usize); 8] =
[(16, 16, 1), (18, 16, 1), (16, 18, 1), (18, 18, 2), (24, 16, 2), (32, 18, 2), (24, 24, 1), (48, 16, 3)];
const MAX_PLANE_VALUES: usize = 3 * 4 * 24 * 12;
const LOSSLESS_GEOMETRIES: [(usize, usize); 5] = [(4, 4), (8, 4), (6, 8), (12, 6), (8, 8)];
/// Version 0x200 sensor sizes; the QP map has one cell per 16×4 pixels, so heights 12 and 20 give odd map heights (3 and 5).
const V200_GEOMETRIES: [(usize, usize); 6] = [(16, 16), (32, 16), (16, 24), (16, 32), (16, 12), (32, 20)];
/// Encodes the four planes of one tile into CRX band byte streams.
fn encode_tile_planes(
planes_values: [Vec<i32>; 4],
plane_w: usize,
plane_h: usize,
before: bool,
after: bool,
halo: i32,
levels: u8,
) -> [Vec<Vec<u8>>; 4] {
let mut encoded: [Vec<Vec<u8>>; 4] = Default::default();
for (p, plane_bytes) in encoded.iter_mut().enumerate() {
let values = &planes_values[p];
if levels == 0 {
let band = Band { width: plane_w, height: plane_h, data: values.clone() };
*plane_bytes = vec![encode_band(&band, true)];
} else {
let bands = forward_bands(values, plane_w, plane_h, before, after, halo);
*plane_bytes = bands.iter().enumerate().map(|(i, band)| encode_band(&coded_band(band, i, after), i == 0)).collect();
}
}
encoded
}
/// Builds a C-RAW file whose planes hold the given random values exactly (uniform quant 4/8/9 has step 1).
fn craw_roundtrip_file((tile_w, tile_h, tile_count): (usize, usize, usize), quant: u8, halo: i32, values: &[i32]) -> Fixture {
let (plane_w, plane_h) = (tile_w / 2, tile_h / 2);
let (width, height) = (tile_w * tile_count, tile_h);
let mut cursor = 0usize;
let mut tiles: Vec<[Vec<Vec<u8>>; 4]> = Vec::new();
let mut expected = vec![0u16; width * height];
for tile in 0..tile_count {
let mut planes_values = [Vec::new(), Vec::new(), Vec::new(), Vec::new()];
for p in 0..4 {
planes_values[p] = values[cursor..cursor + plane_w * plane_h].to_vec();
cursor += plane_w * plane_h;
for (i, &value) in planes_values[p].iter().enumerate() {
let x = tile * tile_w + 2 * (i % plane_w) + p % 2;
let y = 2 * (i / plane_w) + p / 2;
expected[y * width + x] = (value + 8192) as u16; // in sensor range: no clamping
}
}
tiles.push(encode_tile_planes(planes_values, plane_w, plane_h, tile > 0, tile + 1 < tile_count, halo, 3));
}
let (header_size, sample) = v100_sample(&tiles, quant);
let coding = cmp1(0x100, width as u32, height as u32, tile_w as u32, tile_h as u32, 3, header_size as u32);
Fixture { file: wrap(&coding, width as u32, height as u32, &sample, None), expected }
}
/// Builds a lossless file (one band per plane) whose planes hold exactly the given values.
fn lossless_file((width, height): (usize, usize), values: &[i32]) -> Fixture {
let (plane_w, plane_h) = (width / 2, height / 2);
let mut planes_values = [Vec::new(), Vec::new(), Vec::new(), Vec::new()];
let mut expected = vec![0u16; width * height];
for p in 0..4 {
planes_values[p] = values[p * plane_w * plane_h..(p + 1) * plane_w * plane_h].to_vec();
for (i, &value) in planes_values[p].iter().enumerate() {
let x = 2 * (i % plane_w) + p % 2;
let y = 2 * (i / plane_w) + p / 2;
expected[y * width + x] = (value + 8192) as u16;
}
}
let tiles = [encode_tile_planes(planes_values, plane_w, plane_h, false, false, 0, 0)];
let (header_size, sample) = v100_sample(&tiles, 4);
let coding = cmp1(0x100, width as u32, height as u32, width as u32, height as u32, 0, header_size as u32);
Fixture { file: wrap(&coding, width as u32, height as u32, &sample, None), expected }
}
/// Constant-coefficient planes for one tile: LL3 flat at `ll3`, band `mid_band.0` flat at `mid_band.1`,
/// everything else zero. Encoded at the given level count with the given quant/gain/base parameters.
fn constant_band_planes(
plane_w: usize,
plane_h: usize,
levels: u8,
ll3: i32,
mid_band: Option<(usize, i32)>,
quant: u8,
gains: [u32; 10],
bases: [u32; 10],
qp: Option<&[i32]>,
qp_padding: usize,
before: bool,
after: bool,
) -> (usize, Vec<u8>) {
let planes: [Vec<Vec<u8>>; 4] = if levels == 0 {
let mut encoded: [Vec<Vec<u8>>; 4] = Default::default();
for bands in encoded.iter_mut() {
let band = Band { width: plane_w, height: plane_h, data: vec![ll3; plane_w * plane_h] };
*bands = vec![encode_band(&band, true)];
}
encoded
} else {
let shapes = band_shapes(plane_w, plane_h, before, after);
let mut encoded: [Vec<Vec<u8>>; 4] = Default::default();
for bands in encoded.iter_mut() {
let bands_data: Vec<Band> = shapes
.iter()
.enumerate()
.map(|(i, &(w, h))| {
let value = if i == 0 { ll3 } else { mid_band.filter(|&(band, _)| band == i).map_or(0, |(_, value)| value) };
Band { width: w, height: h, data: vec![value; w * h] }
})
.collect();
*bands = bands_data.iter().enumerate().map(|(i, band)| encode_band(band, i == 0)).collect();
}
encoded
};
if levels == 0 {
v100_sample(&[planes], quant)
} else if let Some(map) = qp {
let (map_w, map_h) = (plane_w / 8, plane_h / 2); // one QP cell per 16×4 sensor pixels
v200_sample(&planes, gains, bases, &encode_qp_map(map, map_w, map_h), qp_padding)
} else {
v100_sample(&[planes], quant)
}
}
/// A 16×16 three-level C-RAW file built from constant bands at the given uniform quant value.
fn constant_bands_file(ll3: i32, mid_band: Option<(usize, i32)>, quant: u8) -> Fixture {
let (header_size, sample) = constant_band_planes(8, 8, 3, ll3, mid_band, quant, [0; 10], [0; 10], None, 0, false, false);
let coding = cmp1(0x100, 16, 16, 16, 16, 3, header_size as u32);
Fixture { file: wrap(&coding, 16, 16, &sample, None), expected: Vec::new() }
}
/// A version 0x200 file with the given adaptive QP map (one cell per 16×4 sensor pixels) and per-band gains/bases.
fn v200_mapped_file(
width: usize,
height: usize,
map: &[i32],
ll3: i32,
mid_band: Option<(usize, i32)>,
gains: [u32; 10],
bases: [u32; 10],
) -> Fixture {
assert_eq!(map.len(), (width / 16) * (height / 4));
let (header_size, sample) = constant_band_planes(width / 2, height / 2, 3, ll3, mid_band, 4, gains, bases, Some(map), 3, false, false);
let coding = cmp1(0x200, width as u32, height as u32, width as u32, height as u32, 3, header_size as u32);
Fixture { file: wrap(&coding, width as u32, height as u32, &sample, None), expected: Vec::new() }
}
/// A version 0x200 file with a uniform adaptive QP map and the given per-band gains/bases.
fn v200_constant_bands_file(
width: usize,
height: usize,
qp_value: i32,
ll3: i32,
mid_band: Option<(usize, i32)>,
gains: [u32; 10],
bases: [u32; 10],
) -> Fixture {
v200_mapped_file(width, height, &vec![qp_value; (width / 16) * (height / 4)], ll3, mid_band, gains, bases)
}
fn sensor_hash(image: &RawImage) -> u64 {
match &image.data {
RawData::U16(samples) => samples
.iter()
.flat_map(|value| value.to_le_bytes())
.fold(0xcbf2_9ce4_8422_2325u64, |hash, byte| (hash ^ u64::from(byte)).wrapping_mul(0x100_0000_01b3)),
RawData::F32(_) => panic!("u16 sensor data expected"),
}
}
fn quad() -> impl proptest::strategy::Strategy<Value = [u16; 4]> {
(any::<u16>(), any::<u16>(), any::<u16>(), any::<u16>()).prop_map(|(a, b, c, d)| [a, b, c, d])
}
proptest! {
/// Property 1: synthesis after the exact-inverse analysis reproduces random planes exactly, across
/// odd plane parities and multi-tile halo configurations, with arbitrary halo values.
#[test]
fn craw_wavelet_roundtrip_is_exact(
geometry in 0usize..GEOMETRIES.len(),
values in proptest::collection::vec(-8192i32..=8191, MAX_PLANE_VALUES),
halo in -4096i32..=4096,
quant in proptest::prop_oneof![Just(4u8), Just(8u8), Just(9u8)],
) {
let (tile_w, tile_h, tile_count) = GEOMETRIES[geometry];
let fixture = craw_roundtrip_file(GEOMETRIES[geometry], quant, halo, &values);
assert_eq!(probe(&fixture.file), Some(RawFormat::Cr3));
let image = decode(&fixture.file).unwrap();
assert_eq!((image.width, image.height, image.bits), (tile_w * tile_count, tile_h, 14));
let RawData::U16(samples) = &image.data else { panic!("u16 sensor data expected") };
assert_eq!(samples, &fixture.expected, "synthesis after analysis must be exact");
assert_eq!(probe_info(&fixture.file).unwrap(), image.info());
}
/// Property 2 (lossless side): in-range planes decode exactly; a sample outside the CMP1 bit depth
/// is corrupt data, never clamped.
#[test]
fn lossless_planes_are_exact_and_out_of_range_values_are_rejected(
geometry in 0usize..LOSSLESS_GEOMETRIES.len(),
values in proptest::collection::vec(-8192i32..=8191, 4 * 6 * 4),
out_of_range in 0u8..=2,
) {
let (width, height) = LOSSLESS_GEOMETRIES[geometry];
let mut values = values[..4 * (width / 2) * (height / 2)].to_vec();
if out_of_range > 0 {
values[0] = if out_of_range == 1 { 9000 } else { -9000 };
}
let fixture = lossless_file((width, height), &values);
if out_of_range == 0 {
let image = decode(&fixture.file).unwrap();
let RawData::U16(samples) = &image.data else { panic!("u16 sensor data expected") };
assert_eq!(samples, &fixture.expected);
} else {
assert!(matches!(decode(&fixture.file), Err(RawError::Corrupt(_))));
}
}
/// Property 3: pathological Rice/Golomb prefixes inside well-formed frames are errors or clean
/// decodes — never a panic, never a hang, always deterministic.
#[test]
fn adversarial_entropy_never_panics_and_is_deterministic(
kind in 0usize..4,
payload in proptest::collection::vec(any::<u8>(), 0..=600),
slot in any::<usize>(),
) {
let file = adversarial_frame(kind, &payload, slot);
let first = decode(&file);
assert_eq!(first, decode(&file), "repeated decodes of the same bytes must agree");
if let Ok(image) = &first {
let RawData::U16(samples) = &image.data else { panic!("u16 sensor data expected") };
assert!(samples.iter().all(|&value| value <= 16383), "C-RAW clamps to the CMP1 bit depth");
}
let _ = probe_info(&file);
}
/// Property 2 (adaptive side): random QP maps in the supported 131–168 band, including odd map heights,
/// and random per-band base/gain parameters always decode, with every sample inside the 14-bit range.
#[test]
fn adaptive_qp_maps_stay_within_the_bit_depth(
geometry in 0usize..V200_GEOMETRIES.len(),
qp_values in proptest::collection::vec(131i32..=168, 16),
ll3 in -2048i32..=2048,
mid in -16i32..=16,
gain in 0u32..=32,
base in 0u32..=8,
) {
let (width, height) = V200_GEOMETRIES[geometry];
let (map_w, map_h) = (width / 16, height / 4);
let mut gains = [0u32; 10];
let mut bases = [0u32; 10];
for (i, (g, b)) in gains.iter_mut().zip(bases.iter_mut()).enumerate() {
if i >= 4 {
*g = gain;
}
*b = if i < 4 { base + 1 } else { base };
}
let (header_size, sample) = constant_band_planes(
width / 2,
height / 2,
3,
ll3,
Some((4, mid)),
4,
gains,
bases,
Some(&qp_values[..map_w * map_h]),
3,
false,
false,
);
let coding = cmp1(0x200, width as u32, height as u32, width as u32, height as u32, 3, header_size as u32);
let file = wrap(&coding, width as u32, height as u32, &sample, None);
let image = decode(&file).unwrap();
let RawData::U16(samples) = &image.data else { panic!("u16 sensor data expected") };
assert!(samples.iter().all(|&value| value <= 16383), "adaptive dequantization must stay in the bit depth");
assert_eq!(probe_info(&file).unwrap(), image.info());
}
/// Property 4: randomly generated IAD1 fields either decode — with `active_area ⊆ sensor dims` and
/// `crop ⊆ active_area` — or are rejected (no overflow or out-of-bounds geometry gets through).
#[test]
fn random_iad1_geometry_keeps_active_area_and_crop_invariants(
base in 0usize..2,
full in proptest::prop_oneof![Just(0u16), Just(2u16)],
crop in quad(),
masked_left in quad(),
masked_top in quad(),
active in quad(),
width_off in -3i32..=3,
height_off in -3i32..=3,
) {
let (width, height, coding, sample) = match base {
0 => (4usize, 4usize, cmp1(0x100, 4, 4, 4, 4, 0, 108), lossless_sample(100)),
_ => (16usize, 16usize, cmp1(0x100, 16, 16, 16, 16, 3, 540), constant_bands_sample(0, None, 4)),
};
let clamp16 = |off: i32, base_dim: usize| u16::try_from((base_dim as i64 + off as i64).clamp(1, i64::from(u16::MAX))).unwrap();
let area = iad1_bytes(clamp16(width_off, width), clamp16(height_off, height), full, crop, masked_left, masked_top, active);
let file = wrap(&coding, width as u32, height as u32, &sample, Some(&area));
assert_eq!(probe(&file), Some(RawFormat::Cr3));
if let Ok(image) = decode(&file) {
image.validate().unwrap();
let a = image.active_area;
assert!(a.width > 0 && a.height > 0, "{a:?}");
assert!(a.x + a.width <= image.width && a.y + a.height <= image.height, "active area outside sensor: {a:?}");
assert!(
image.crop.x + image.crop.width <= a.width && image.crop.y + image.crop.height <= a.height,
"crop outside active area: {:?} in {a:?}",
image.crop
);
assert_eq!(probe_info(&file).unwrap(), image.info());
}
}
}
/// Raw sample bytes of the 4×4 lossless fixture (a 108-byte marker header plus four flat planes).
fn lossless_sample(value: i32) -> Vec<u8> {
let band = Band { width: 2, height: 2, data: vec![value; 4] };
let tiles = [std::array::from_fn(|_| vec![encode_band(&band, true)])];
let (_, sample) = v100_sample(&tiles, 4);
sample
}
/// Raw sample bytes of a constant-band 16×16 C-RAW fixture.
fn constant_bands_sample(ll3: i32, mid_band: Option<(usize, i32)>, quant: u8) -> Vec<u8> {
let (_, sample) = constant_band_planes(8, 8, 3, ll3, mid_band, quant, [0; 10], [0; 10], None, 0, false, false);
sample
}
// --- adversarial entropy frames (property 3) ---
/// A well-formed frame of the given kind (0: lossless 8×6, 1: C-RAW 16×16, 2: 0x200 with an adaptive
/// QP map, 3: the same 0x200 frame with the QP map itself replaced) in which one plane's entropy streams
/// (kinds 0 to 2) or the QP map stream (kind 3) are replaced by the adversarial payload. The marker
/// sizes follow the payload, so the layout stays parseable and the pathological bits reach the
/// Rice/Golomb readers; the untouched planes decode fine, reaching the mosaic stage on success.
fn adversarial_frame(kind: usize, payload: &[u8], slot: usize) -> Vec<u8> {
let shapes = if kind == 0 { vec![(4usize, 3usize)] } else { band_shapes(8, 8, false, false).to_vec() };
let mut tiles: Vec<[Vec<Vec<u8>>; 4]> = vec![Default::default()];
for (p, bands) in tiles[0].iter_mut().enumerate() {
*bands = if kind != 3 && p == slot % 4 {
shapes.iter().map(|_| payload.to_vec()).collect()
} else {
shapes
.iter()
.enumerate()
.map(|(i, &(w, h))| {
let band = Band { width: w, height: h, data: vec![0; w * h] };
encode_band(&band, i == 0)
})
.collect()
};
}
let (header_size, sample) = if kind == 2 {
let map = vec![150i32; 16];
v200_sample(&tiles[0], [0; 10], [1; 10], &encode_qp_map(&map, 1, 4), 3)
} else if kind == 3 {
v200_sample(&tiles[0], [0; 10], [1; 10], payload, 3)
} else {
v100_sample(&tiles, 4)
};
let (width, height, levels) = match kind {
0 => (8u32, 6u32, 0u8),
1 => (16, 16, 3),
_ => (16, 16, 3),
};
let version = if kind >= 2 { 0x200u16 } else { 0x100 };
let coding = cmp1(version, width, height, width, height, levels, header_size as u32);
wrap(&coding, width, height, &sample, None)
}
// --- exhaustive coverage of the verified quantization bands (property 2) ---
#[test]
fn every_uniform_quant_value_dequantizes_within_the_bit_depth() {
for quant in 4u8..=44 {
let step = quant_step(quant);
// Overshoot: the dequantized DC leaves the 14-bit range and C-RAW clamps it.
let fixture = constant_bands_file(8000, None, quant);
let expected_value = (8192 + 8000 * step).clamp(0, 16383) as u16;
let image = decode(&fixture.file).unwrap();
assert_eq!(image.data, RawData::U16(vec![expected_value; 256]), "quant {quant}");
assert_eq!(probe_info(&fixture.file).unwrap().bits, 14);
// In range: the DC lands exactly on 8192 + level * step, so the step itself is observable
// (the overshoot case above saturates for every step above 1).
let level = 8000 / step;
let fixture = constant_bands_file(level, None, quant);
let image = decode(&fixture.file).unwrap();
assert_eq!(image.data, RawData::U16(vec![(8192 + level * step) as u16; 256]), "quant {quant} in range");
}
for quant in [3u8, 45] {
let fixture = constant_bands_file(8000, None, quant);
assert!(matches!(decode(&fixture.file), Err(RawError::Unsupported(_))), "quant {quant}");
assert!(matches!(probe_info(&fixture.file), Err(RawError::Unsupported(_))));
}
}
#[test]
fn every_qp_value_in_the_supported_band_dequantizes_within_the_bit_depth() {
let gains = [0, 0, 0, 0, 8, 8, 8, 8, 8, 8];
let bases = [8, 8, 8, 8, 2, 2, 2, 2, 2, 2];
let mut first_hash = 0u64;
let mut last_hash = 0u64;
for value in 131i32..=175 {
let fixture = v200_constant_bands_file(16, 16, value, 400, Some((4, 8)), gains, bases);
let image = decode(&fixture.file).unwrap();
let RawData::U16(samples) = &image.data else { panic!("u16 sensor data expected") };
assert!(samples.iter().all(|&sample| sample <= 16383), "QP {value}");
assert_eq!(probe_info(&fixture.file).unwrap(), image.info());
if value == 131 {
first_hash = sensor_hash(&image);
}
last_hash = sensor_hash(&image);
}
assert_ne!(first_hash, last_hash, "the QP value must reach the dequantization step");
for value in [130i32, 176] {
let fixture = v200_constant_bands_file(16, 16, value, 400, None, gains, bases);
assert!(matches!(decode(&fixture.file), Err(RawError::Unsupported(_))), "QP {value}");
assert!(matches!(probe_info(&fixture.file), Err(RawError::Unsupported(_))));
}
}
// --- determinism (property 5) ---
#[test]
fn decoding_is_deterministic_across_repeated_and_parallel_invocations() {
let craw = craw_roundtrip_file((24, 16, 2), 4, 7, &lcg_values(7, MAX_PLANE_VALUES));
let lossless = lossless_file((8, 6), &lcg_values(11, 4 * 4 * 3));
let adaptive = v200_constant_bands_file(16, 16, 150, 400, Some((4, 8)), [0, 0, 0, 0, 8, 8, 8, 8, 8, 8], [8, 8, 8, 8, 2, 2, 2, 2, 2, 2]);
for fixture in [&craw, &lossless, &adaptive] {
let first = decode(&fixture.file).unwrap();
assert_eq!(first, decode(&fixture.file).unwrap(), "repeated decodes must be identical");
let expected_hash = sensor_hash(&first);
let hashes: Vec<u64> = std::thread::scope(|scope| {
let handles: Vec<_> = (0..4).map(|_| scope.spawn(|| sensor_hash(&decode(&fixture.file).unwrap()))).collect();
handles.into_iter().map(|handle| handle.join().unwrap()).collect()
});
assert!(hashes.iter().all(|&hash| hash == expected_hash), "parallel decodes must agree with the serial hash");
}
}
// --- QP map shapes: odd heights, large jumps, over-long prefixes (properties 3 and 6) ---
/// An odd QP-map height (16×12 sensor, 1×3 map) decodes, and with step 1 (base 1, no gain) the flat LL3
/// coefficient comes through exactly.
#[test]
fn odd_qp_map_height_0x200_decodes_within_range() {
let gains = [0; 10];
let bases = [1; 10];
let fixture = v200_constant_bands_file(16, 12, 150, 400, None, gains, bases);
let image = decode(&fixture.file).unwrap();
let RawData::U16(samples) = &image.data else { panic!("u16 sensor data expected") };
assert_eq!(samples, &vec![8592u16; 16 * 12], "step 1 (base 1) keeps the LL3 coefficient exact");
assert_eq!(probe_info(&fixture.file).unwrap(), image.info());
}
/// Adaptive steps on an odd-height map: the coarser bands pair QP rows, averaging the QP values before the
/// step lookup, and the last row of an odd map has no partner and is used alone. Here HL2 has two rows: the
/// first pairs QP rows 0 and 1 (150 and 153, average 151), the second is QP row 2 alone (151). The adaptive
/// stream must therefore decode exactly like the uniform stream whose step is the one QP 151 gives,
/// and differ from the uniform streams one step either side.
#[test]
fn odd_height_qp_map_uses_its_last_row_alone() {
let map = [150, 153, 151];
let step = |qp: i32| 2 + 8 * quant_step((qp - 124) as u8) as u32 / 8;
assert_eq!(step(151), 16);
let adaptive_gains = [0, 0, 0, 0, 8, 8, 8, 8, 8, 8];
let adaptive_bases = [8, 8, 8, 8, 2, 2, 2, 2, 2, 2];
let uniform = |hl_step: u32| {
let fixture = v200_mapped_file(16, 12, &map, 400, Some((4, 8)), [0; 10], [8, 8, 8, 8, hl_step, hl_step, hl_step, hl_step, hl_step, hl_step]);
decode(&fixture.file).unwrap()
};
let fixture = v200_mapped_file(16, 12, &map, 400, Some((4, 8)), adaptive_gains, adaptive_bases);
let adaptive = decode(&fixture.file).unwrap();
assert_eq!(adaptive, uniform(step(151)), "QP rows 0+1 average to 151 and the unpaired row 2 is 151 alone");
assert_ne!(adaptive, uniform(step(151) - 1));
assert_ne!(adaptive, uniform(step(151) + 1));
assert_eq!(probe_info(&fixture.file).unwrap(), adaptive.info());
}
/// A map whose neighbouring cells differ by up to 37 forces symbols past the plain Rice range, so the
/// 23-zero escape is exercised deterministically (a run of equal cells drops the Rice parameter to 0 first).
#[test]
fn qp_map_with_large_jumps_uses_the_escape_code() {
let map = [131, 131, 131, 131, 168, 131, 168, 131];
let gains = [0, 0, 0, 0, 8, 8, 8, 8, 8, 8];
let bases = [8, 8, 8, 8, 2, 2, 2, 2, 2, 2];
let fixture = v200_mapped_file(16, 32, &map, 400, Some((4, 8)), gains, bases);
let image = decode(&fixture.file).unwrap();
let RawData::U16(samples) = &image.data else { panic!("u16 sensor data expected") };
assert!(samples.iter().all(|&sample| sample <= 16383));
assert_eq!(probe_info(&fixture.file).unwrap(), image.info());
}
/// A QP Rice prefix longer than the 23-zero escape is corrupt data, not an endless read.
#[test]
fn qp_rice_prefix_longer_than_the_escape_is_corrupt() {
// The 32-bit literal 300 (QP 150), then 24 zero bits with no terminating 1 within 23 zeros.
let payload = [0x00, 0x00, 0x01, 0x2c, 0x00, 0x00, 0x00, 0x80];
let file = adversarial_frame(3, &payload, 0);
let error = decode(&file).unwrap_err();
assert!(matches!(error, RawError::Corrupt(ref message) if message.contains("QP Rice escape prefix")), "{error:?}");
assert!(probe_info(&file).is_err());
}