Skip to main content

pith_jpeg/
reference.rs

1//! The committed reference contract of this crate, re-expressed over
2//! the crate's own pipeline.
3//!
4//! [`tools/gen-reference`](../../tools/gen-reference) recomputes
5//! everything in this module and either writes `reference.json` (repo
6//! root) or verifies the committed copy against the recomputation;
7//! CI runs the verify mode on every commit, and CD ships the file
8//! with every SDK artifact. Python, Node and Go SDKs test against the
9//! same bytes.
10//!
11//! # File schema (version 1)
12//!
13//! Two sections, both sorted by name, two-space indentation, LF line
14//! endings, one trailing newline — byte-identical across
15//! regenerations:
16//!
17//! * `digests` — one entry per committed `tests/fixtures/*.jpg`
18//!   conformance fixture: [`pith_digest::fnv1a64`] over the decoded
19//!   sample bytes in row-major, channel-interleaved order (the crate's
20//!   own output layout, exactly what the `.raw` companions hold).
21//!   Digests must match bit-for-bit.
22//! * `vectors` — the DCT/dequant numerics in the suite's numeric
23//!   schema (the `pith-math` convention): every `f64` is its raw
24//!   IEEE-754 bit pattern as 16-digit lowercase hex, so consumers
25//!   never see a decimal round-trip. Each vector records `exact`
26//!   plus the two tolerance budgets `tol_abs`/`tol_rel` (hex `f64`
27//!   like everything else). Verification policy:
28//!   * `exact: true` — the pipeline is pure integer arithmetic
29//!     (de-zigzag, dequantize, the islow IDCT transcription), so any
30//!     correct implementation must reproduce the recorded bits.
31//!   * `exact: false` — the `f64` trig kernel
32//!     ([`pith_math::idct2_2d`], the orthonormal oracle documented in
33//!     [`crate`]) depends on the platform `libm`'s `sin`/`cos`, which
34//!     may differ in the last ulp across platforms; verification
35//!     bounds the per-value error by
36//!     `max(tol_abs, tol_rel · |expected|)`. The absolute floor
37//!     (1e-13) covers the cancellation noise of near-zero bins, where
38//!     platform differences move the value by a few ulps of the
39//!     *summands*, not of the tiny result.
40//!
41//! Vector input layouts (documented here because the file carries no
42//! per-field prose): all inputs are built from integer arithmetic
43//! alone, so input generation is itself platform-independent.
44//!
45//! * `dequant.zigzag.8x8` — input: 64 DQT-payload values in JPEG scan
46//!   order; output: the same values in natural (row-major) order,
47//!   i.e. `out[ZIGZAG[k]] = in[k]`.
48//! * `dequant.8x8` — input: 64 natural-order coefficients followed by
49//!   64 natural-order quantizer values (one flat 128-value buffer);
50//!   output: the 64 `i64` products, exactly representable in `f64`.
51//! * `idct.islow.8x8` — same input layout as `dequant.8x8`; output:
52//!   the 64 `u8` samples of the [`crate::idct`] `islow` transcription
53//!   (dequantize + two-pass fixed-point IDCT + level shift + clamp),
54//!   written into a stride-8 block.
55//! * `idct.oracle.8x8` — input: the 64 *dequantized* coefficients
56//!   (the `dequant.8x8` output, so SDK tests chain the two); output:
57//!   the raw [`pith_math::idct2_2d`] result, no level shift, no clamp.
58
59use crate::decode;
60use crate::huffman::ZIGZAG;
61use crate::idct::dequant_idct_into;
62use pith_digest::fnv1a64;
63use pith_math::idct2_2d;
64
65/// Absolute tolerance floor for the `f64` oracle vector: covers the
66/// cancellation noise of near-zero trig bins (see the module docs).
67pub const TOL_ABS: f64 = 1e-13;
68
69/// Relative tolerance for the `f64` oracle vector, applied to the
70/// expected magnitude.
71pub const TOL_REL: f64 = 1e-12;
72
73/// One recorded numeric vector: the named kernel, its input, and the
74/// output the committed file must carry.
75#[derive(Clone, Debug)]
76pub struct Vector {
77    /// Vector name as it appears in `reference.json`.
78    pub name: &'static str,
79    /// Whether the output must match bit-for-bit (integer pipeline) or
80    /// within the recorded tolerance (`f64` trig kernels).
81    pub exact: bool,
82    /// `[w, h]` of the recorded matrix, when the vector is a 2D block.
83    pub shape: Option<[usize; 2]>,
84    /// Tolerance budgets (both recorded per vector even when
85    /// [`Vector::exact`] is true).
86    pub tol_abs: f64,
87    /// See [`Vector::tol_abs`].
88    pub tol_rel: f64,
89    /// Kernel input, hex-recorded in the file.
90    pub input: Vec<f64>,
91    /// Kernel output the committed file must carry.
92    pub output: Vec<f64>,
93}
94
95/// One committed conformance fixture and its expected decode digest.
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97pub struct Digest {
98    /// Digest name as it appears in `reference.json`.
99    pub name: &'static str,
100    /// Fixture file under `tests/fixtures/`.
101    pub file: &'static str,
102}
103
104/// The eleven committed fixtures, in fixture-table order (the
105/// conformance suite's own order); [`reference_json`] sorts by name
106/// when it serializes.
107#[must_use]
108pub fn digests() -> &'static [Digest] {
109    DIGESTS
110}
111
112const DIGESTS: &[Digest] = &[
113    Digest {
114        name: "base_420_odd",
115        file: "base_420_odd.jpg",
116    },
117    Digest {
118        name: "base_420_rst",
119        file: "base_420_rst.jpg",
120    },
121    Digest {
122        name: "base_422",
123        file: "base_422.jpg",
124    },
125    Digest {
126        name: "base_422_odd",
127        file: "base_422_odd.jpg",
128    },
129    Digest {
130        name: "base_440",
131        file: "base_440.jpg",
132    },
133    Digest {
134        name: "base_444",
135        file: "base_444.jpg",
136    },
137    Digest {
138        name: "base_gray",
139        file: "base_gray.jpg",
140    },
141    Digest {
142        name: "prog_420",
143        file: "prog_420.jpg",
144    },
145    Digest {
146        name: "prog_444",
147        file: "prog_444.jpg",
148    },
149    Digest {
150        name: "prog_444_rst",
151        file: "prog_444_rst.jpg",
152    },
153    Digest {
154        name: "prog_gray",
155        file: "prog_gray.jpg",
156    },
157];
158
159/// Deterministic coefficient block, natural order: `i32` values in
160/// `[-512, 511]` from an integer LCG — the signed-11-bit coefficient
161/// range the entropy decoder can emit.
162fn coef_block() -> Vec<f64> {
163    (0..64)
164        .map(|i: i32| (((i * 37 + 11) % 1024) - 512) as f64)
165        .collect()
166}
167
168/// Deterministic quantizer block, natural order: `u16` values in
169/// `[1, 32]` — a typical mid-quality table's magnitude range.
170fn quant_block() -> Vec<f64> {
171    (0..64)
172        .map(|i: u16| ((i * 17 + 3) % 32 + 1) as f64)
173        .collect()
174}
175
176/// Deterministic DQT-payload block, JPEG scan order: `u8` values.
177fn scan_payload() -> Vec<f64> {
178    (0..64)
179        .map(|k: u16| f64::from(((k * 91 + 13) % 256) as u8))
180        .collect()
181}
182
183/// The dequantized coefficient block the IDCT vectors share.
184fn dequantized() -> Vec<f64> {
185    let coefs = coef_block();
186    let qt = quant_block();
187    coefs.iter().zip(qt.iter()).map(|(&c, &q)| c * q).collect()
188}
189
190/// Every numeric vector the suite shares, recomputed on each
191/// invocation.
192#[must_use]
193pub fn vectors() -> Vec<Vector> {
194    let mut v: Vec<Vector> = Vec::new();
195
196    // -- De-zigzag (the DQT payload walk) --------------------------------
197    let payload = scan_payload();
198    let mut natural = vec![0.0; 64];
199    for (k, &val) in payload.iter().enumerate() {
200        natural[ZIGZAG[k] as usize] = val;
201    }
202    v.push(Vector {
203        name: "dequant.zigzag.8x8",
204        exact: true,
205        shape: Some([8, 8]),
206        tol_abs: TOL_ABS,
207        tol_rel: TOL_REL,
208        input: payload,
209        output: natural,
210    });
211
212    // -- Dequantize (element-wise product, exact in f64) -----------------
213    let coefs = coef_block();
214    let qt = quant_block();
215    let products = dequantized();
216    let mut input = coefs;
217    input.extend_from_slice(&qt);
218    v.push(Vector {
219        name: "dequant.8x8",
220        exact: true,
221        shape: None,
222        tol_abs: TOL_ABS,
223        tol_rel: TOL_REL,
224        input,
225        output: products.clone(),
226    });
227
228    // -- islow IDCT (fixed-point transcription, byte-exact) --------------
229    let coefs = coef_block();
230    let qt = quant_block();
231    let coefs: Vec<i32> = coefs.iter().map(|&c| c as i32).collect();
232    let qt: Vec<u16> = qt.iter().map(|&q| q as u16).collect();
233    let mut plane = [0u8; 64];
234    dequant_idct_into(&coefs, &qt, &mut plane, 8, 0, 0);
235    v.push(Vector {
236        name: "idct.islow.8x8",
237        exact: true,
238        shape: None,
239        tol_abs: TOL_ABS,
240        tol_rel: TOL_REL,
241        input: {
242            let mut input = coefs.iter().map(|&c| f64::from(c)).collect::<Vec<f64>>();
243            input.extend(qt.iter().map(|&q| f64::from(q)));
244            input
245        },
246        output: plane.iter().map(|&s| f64::from(s)).collect(),
247    });
248
249    // -- Orthonormal f64 oracle (platform-libm-shaped) -------------------
250    let mut f = dequantized();
251    idct2_2d(&mut f, 8, 8);
252    v.push(Vector {
253        name: "idct.oracle.8x8",
254        exact: false,
255        shape: Some([8, 8]),
256        tol_abs: TOL_ABS,
257        tol_rel: TOL_REL,
258        input: dequantized(),
259        output: f,
260    });
261
262    v
263}
264
265/// Decodes a committed fixture and digests the pixel output.
266///
267/// The file is read relative to the crate manifest directory (the same
268/// convention the conformance tests use), so both `cargo test` and
269/// `cargo run --bin gen-reference` resolve it from a repository
270/// checkout.
271///
272/// # Panics
273///
274/// Panics if the fixture is unreadable or fails to decode: the
275/// committed corpus must always decode, and a generator that cannot
276/// reproduce a digest must be loud, not silent.
277#[must_use]
278pub fn digest_of(d: &Digest) -> u64 {
279    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
280        .join("tests")
281        .join("fixtures")
282        .join(d.file);
283    let bytes = std::fs::read(&path)
284        .unwrap_or_else(|e| panic!("cannot read fixture {}: {e}", path.display()));
285    match decode(&bytes) {
286        Ok(crate::Jpeg::Gray(img)) => fnv1a64(img.as_slice()),
287        Ok(crate::Jpeg::Rgb(img)) => fnv1a64(img.as_slice()),
288        Err(why) => panic!("fixture {} no longer decodes: {why}", d.file),
289    }
290}
291
292/// Every fixture digest, recomputed.
293#[must_use]
294pub fn digest_values() -> Vec<(&'static str, u64)> {
295    DIGESTS.iter().map(|d| (d.name, digest_of(d))).collect()
296}
297
298/// `f64` → canonical 16-digit lowercase hex of the raw bit pattern.
299#[must_use]
300pub fn hx(v: f64) -> String {
301    format!("{:016x}", v.to_bits())
302}
303
304fn push_f64s(out: &mut String, vs: &[f64], indent: &str) {
305    out.push_str("[\n");
306    for v in vs {
307        out.push_str(indent);
308        out.push('"');
309        out.push_str(&hx(*v));
310        out.push_str("\",\n");
311    }
312    // Trim the trailing comma of the last element for strict JSON.
313    if !vs.is_empty() {
314        let l = out.len() - 2;
315        out.truncate(l);
316        out.push('\n');
317    }
318    out.push_str(indent.trim_end());
319    out.push(']');
320}
321
322/// Serializes the canonical `reference.json` bytes.
323///
324/// Deterministic: sections sorted by name, two-space indentation, a
325/// single trailing newline — byte-identical across regenerations on
326/// the same platform. The `f64` oracle vector's bits are
327/// platform-libm-shaped by design; cross-platform verification is
328/// semantic ([`verify_str`]), never a byte compare of the whole file.
329///
330/// # Panics
331///
332/// Panics if a committed fixture is unreadable or fails to decode
333/// (see [`digest_of`]).
334#[must_use]
335pub fn reference_json() -> String {
336    let mut digests: Vec<(&str, u64)> = digest_values();
337    digests.sort_unstable_by_key(|&(name, _)| name);
338    let mut vectors = vectors();
339    vectors.sort_unstable_by(|a, b| a.name.cmp(b.name));
340
341    let mut out = String::new();
342    out.push_str("{\n");
343    out.push_str("  \"schema\": 1,\n");
344    out.push_str("  \"suite\": \"pith-jpeg\",\n");
345    out.push_str(
346        "  \"note\": \"every f64 is its raw IEEE-754 bit pattern as 16-digit \
347lowercase hex; exact vectors must match bit-for-bit, the orthonormal-oracle \
348vector within max(tol_abs, tol_rel * |expected|); digests are fnv1a64 over \
349the decoded sample bytes (row-major, channel-interleaved); dequant.8x8 and \
350idct.islow.8x8 inputs are 64 coefficients followed by 64 quant values, \
351natural order\",\n",
352    );
353    out.push_str("  \"digests\": {\n");
354    for (i, (name, digest)) in digests.iter().enumerate() {
355        out.push_str(&format!("    \"{name}\": \"{digest:016x}\""));
356        out.push_str(if i + 1 == digests.len() { "\n" } else { ",\n" });
357    }
358    out.push_str("  },\n");
359    out.push_str("  \"vectors\": {\n");
360    for (i, v) in vectors.iter().enumerate() {
361        out.push_str("    \"");
362        out.push_str(v.name);
363        out.push_str("\": {\n");
364        out.push_str("      \"exact\": ");
365        out.push_str(if v.exact { "true" } else { "false" });
366        out.push_str(",\n");
367        if let Some([w, h]) = v.shape {
368            out.push_str(&format!("      \"shape\": [{w}, {h}],\n"));
369        }
370        out.push_str("      \"tol_abs\": \"");
371        out.push_str(&hx(v.tol_abs));
372        out.push_str("\",\n");
373        out.push_str("      \"tol_rel\": \"");
374        out.push_str(&hx(v.tol_rel));
375        out.push_str("\",\n");
376        out.push_str("      \"input\": ");
377        push_f64s(&mut out, &v.input, "        ");
378        out.push_str(",\n");
379        out.push_str("      \"output\": ");
380        push_f64s(&mut out, &v.output, "        ");
381        out.push('\n');
382        out.push_str("    }");
383        out.push_str(if i + 1 == vectors.len() { "\n" } else { ",\n" });
384    }
385    out.push_str("  }\n}\n");
386    out
387}
388
389/// Compares the committed `reference.json` against a fresh
390/// recomputation, per-value and per-policy (never a whole-file byte
391/// compare: the oracle vector's bits are platform-libm-shaped).
392///
393/// # Errors
394///
395/// A description naming the first divergence per offending vector, or
396/// the read failure.
397///
398/// # Panics
399///
400/// Panics if a committed fixture is unreadable or fails to decode
401/// (see [`digest_of`]).
402pub fn verify() -> Result<(), String> {
403    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("reference.json");
404    let committed = std::fs::read_to_string(&path)
405        .map_err(|e| format!("cannot read {}: {e}", path.display()))?;
406    verify_str(&committed)
407}
408
409/// One vector as parsed back out of the committed file.
410struct Committed {
411    exact: bool,
412    tol_abs: f64,
413    tol_rel: f64,
414    input: Vec<f64>,
415    output: Vec<f64>,
416}
417
418/// The comparison underlying [`verify`]: a committed render against a
419/// fresh recomputation.
420///
421/// # Errors
422///
423/// A description naming every divergence found.
424///
425/// # Panics
426///
427/// Panics if a committed fixture is unreadable or fails to decode
428/// (see [`digest_of`]).
429pub fn verify_str(committed: &str) -> Result<(), String> {
430    let mut p = Json::new(committed);
431    p.expect(b'{')?;
432    let mut committed_digests: Vec<(String, u64)> = Vec::new();
433    let mut committed_vectors: Vec<(String, Committed)> = Vec::new();
434    loop {
435        match p.peek() {
436            Some(b'}') => break,
437            Some(b'"') => {
438                let key = p.string()?;
439                p.expect(b':')?;
440                match key.as_str() {
441                    "digests" => committed_digests = p.entries(Json::hex_u64)?,
442                    "vectors" => committed_vectors = p.entries(Json::vector)?,
443                    _ => p.skip_value()?,
444                }
445                if p.peek() == Some(b',') {
446                    p.i += 1;
447                }
448            }
449            got => {
450                return Err(format!(
451                    "expected key or }}, found {:?} at byte {}",
452                    got.map(char::from),
453                    p.i
454                ));
455            }
456        }
457    }
458
459    let mut fails: Vec<String> = Vec::new();
460
461    // -- digests: bit-exact ----------------------------------------------
462    let fresh = digest_values();
463    for (name, want) in &fresh {
464        match committed_digests.iter().find(|(n, _)| n == name) {
465            None => fails.push(format!("[digest {name}] missing from committed file")),
466            Some((_, got)) => {
467                if *got != *want {
468                    fails.push(format!(
469                        "[digest {name}] drifted: committed {got:016x}, recomputed {want:016x}"
470                    ));
471                }
472            }
473        }
474    }
475    for (name, _) in &committed_digests {
476        if !fresh.iter().any(|&(n, _)| n == name.as_str()) {
477            fails.push(format!("[digest {name}] not recomputable: unknown fixture"));
478        }
479    }
480
481    // -- vectors ----------------------------------------------------------
482    let fresh_vectors = vectors();
483    for want in &fresh_vectors {
484        let pos = committed_vectors
485            .iter()
486            .position(|(name, _)| name == want.name);
487        let Some(idx) = pos else {
488            fails.push(format!("[{}] missing from committed file", want.name));
489            continue;
490        };
491        let (_, got) = committed_vectors.swap_remove(idx);
492        if got.exact != want.exact {
493            fails.push(format!("[{}] exact flag drifted", want.name));
494        }
495        if got.tol_abs.to_bits() != want.tol_abs.to_bits()
496            || got.tol_rel.to_bits() != want.tol_rel.to_bits()
497        {
498            fails.push(format!("[{}] tolerance budgets drifted", want.name));
499        }
500        if got.input.len() != want.input.len() || got.output.len() != want.output.len() {
501            fails.push(format!("[{}] length drifted", want.name));
502            continue;
503        }
504        let input_drift = got
505            .input
506            .iter()
507            .zip(&want.input)
508            .any(|(g, w)| g.to_bits() != w.to_bits());
509        if input_drift {
510            fails.push(format!("[{}] input drifted", want.name));
511        }
512        for (i, (g, w)) in got.output.iter().zip(&want.output).enumerate() {
513            if want.exact {
514                if g.to_bits() != w.to_bits() {
515                    fails.push(format!(
516                        "[{}] output[{i}] drifted: committed {}, recomputed {}",
517                        want.name,
518                        hx(*g),
519                        hx(*w)
520                    ));
521                    break;
522                }
523            } else {
524                let budget = want.tol_abs.max(want.tol_rel * w.abs());
525                let err = (g - w).abs();
526                if err > budget {
527                    fails.push(format!(
528                        "[{}] output[{i}] drifted beyond tolerance: committed {}, recomputed {}, err {err:e}, budget {budget:e}",
529                        want.name,
530                        hx(*g),
531                        hx(*w)
532                    ));
533                    break;
534                }
535            }
536        }
537    }
538    for (name, _) in &committed_vectors {
539        if !fresh_vectors.iter().any(|v| v.name == name.as_str()) {
540            fails.push(format!("[{name}] not recomputable: unknown vector"));
541        }
542    }
543
544    if fails.is_empty() {
545        Ok(())
546    } else {
547        Err(fails.join("\n"))
548    }
549}
550
551/// Tiny JSON walker for exactly the schema [`reference_json`] emits:
552/// objects, arrays, strings, booleans — every numeric payload is a hex
553/// string, so no float parsing ever happens.
554struct Json<'a> {
555    b: &'a [u8],
556    i: usize,
557}
558
559impl<'a> Json<'a> {
560    fn new(s: &'a str) -> Self {
561        Json {
562            b: s.as_bytes(),
563            i: 0,
564        }
565    }
566
567    fn ws(&mut self) {
568        while self.i < self.b.len() && (self.b[self.i] as char).is_ascii_whitespace() {
569            self.i += 1;
570        }
571    }
572
573    fn peek(&mut self) -> Option<u8> {
574        self.ws();
575        self.b.get(self.i).copied()
576    }
577
578    fn expect(&mut self, c: u8) -> Result<(), String> {
579        match self.peek() {
580            Some(got) if got == c => {
581                self.i += 1;
582                Ok(())
583            }
584            got => Err(format!(
585                "expected {:?}, found {:?} at byte {}",
586                c as char,
587                got.map(char::from),
588                self.i
589            )),
590        }
591    }
592
593    fn string(&mut self) -> Result<String, String> {
594        self.expect(b'"')?;
595        let start = self.i;
596        while self.i < self.b.len() && self.b[self.i] != b'"' {
597            self.i += 1;
598        }
599        if self.i >= self.b.len() {
600            return Err("unterminated string".into());
601        }
602        let s = core::str::from_utf8(&self.b[start..self.i]).map_err(|e| e.to_string())?;
603        self.i += 1;
604        Ok(s.to_owned())
605    }
606
607    fn hex_u64(&mut self) -> Result<u64, String> {
608        let s = self.string()?;
609        u64::from_str_radix(&s, 16).map_err(|e| format!("bad hex u64 {s:?}: {e}"))
610    }
611
612    fn hex_f64(&mut self) -> Result<f64, String> {
613        let s = self.string()?;
614        let bits = u64::from_str_radix(&s, 16).map_err(|e| format!("bad hex f64 {s:?}: {e}"))?;
615        Ok(f64::from_bits(bits))
616    }
617
618    fn boolean(&mut self) -> Result<bool, String> {
619        self.ws();
620        if self.b[self.i..].starts_with(b"true") {
621            self.i += 4;
622            Ok(true)
623        } else if self.b[self.i..].starts_with(b"false") {
624            self.i += 5;
625            Ok(false)
626        } else {
627            Err("expected boolean".into())
628        }
629    }
630
631    fn number(&mut self) -> Result<i64, String> {
632        self.ws();
633        let start = self.i;
634        while self.i < self.b.len() && (self.b[self.i] as char).is_ascii_digit() {
635            self.i += 1;
636        }
637        core::str::from_utf8(&self.b[start..self.i])
638            .map_err(|e| e.to_string())?
639            .parse::<i64>()
640            .map_err(|e| format!("bad integer: {e}"))
641    }
642
643    fn f64_array(&mut self) -> Result<Vec<f64>, String> {
644        self.expect(b'[')?;
645        let mut out = Vec::new();
646        if self.peek() == Some(b']') {
647            self.i += 1;
648            return Ok(out);
649        }
650        loop {
651            out.push(self.hex_f64()?);
652            match self.peek() {
653                Some(b',') => self.i += 1,
654                Some(b']') => {
655                    self.i += 1;
656                    return Ok(out);
657                }
658                got => {
659                    return Err(format!(
660                        "expected , or ] in array, found {:?}",
661                        got.map(char::from)
662                    ));
663                }
664            }
665        }
666    }
667
668    fn vector(&mut self) -> Result<Committed, String> {
669        self.expect(b'{')?;
670        let mut c = Committed {
671            exact: false,
672            tol_abs: 0.0,
673            tol_rel: 0.0,
674            input: Vec::new(),
675            output: Vec::new(),
676        };
677        loop {
678            match self.peek() {
679                Some(b'}') => {
680                    self.i += 1;
681                    return Ok(c);
682                }
683                Some(b'"') => {
684                    let key = self.string()?;
685                    self.expect(b':')?;
686                    match key.as_str() {
687                        "exact" => c.exact = self.boolean()?,
688                        "tol_abs" => c.tol_abs = self.hex_f64()?,
689                        "tol_rel" => c.tol_rel = self.hex_f64()?,
690                        "input" => c.input = self.f64_array()?,
691                        "output" => c.output = self.f64_array()?,
692                        "shape" => self.skip_value()?,
693                        other => return Err(format!("unexpected vector key {other:?}")),
694                    }
695                    if self.peek() == Some(b',') {
696                        self.i += 1;
697                    }
698                }
699                got => {
700                    return Err(format!(
701                        "expected key or }}, found {:?}",
702                        got.map(char::from)
703                    ));
704                }
705            }
706        }
707    }
708
709    fn skip_value(&mut self) -> Result<(), String> {
710        match self.peek() {
711            Some(b'"') => {
712                self.string()?;
713            }
714            // Arrays in this schema are hex-string arrays (input/output)
715            // or bare-integer arrays (shape); both are skipped by
716            // bracket counting — the schema's strings never contain
717            // brackets.
718            Some(b'[') => {
719                self.i += 1;
720                let mut depth = 1;
721                while depth > 0 {
722                    match self.peek() {
723                        Some(b'[') => {
724                            depth += 1;
725                            self.i += 1;
726                        }
727                        Some(b']') => {
728                            depth -= 1;
729                            self.i += 1;
730                        }
731                        Some(_) => self.i += 1,
732                        None => return Err("unterminated array".into()),
733                    }
734                }
735            }
736            Some(b'{') => {
737                self.i += 1;
738                let mut depth = 1;
739                while depth > 0 {
740                    match self.peek() {
741                        Some(b'{') => {
742                            depth += 1;
743                            self.i += 1;
744                        }
745                        Some(b'}') => {
746                            depth -= 1;
747                            self.i += 1;
748                        }
749                        Some(_) => self.i += 1,
750                        None => return Err("unterminated object".into()),
751                    }
752                }
753            }
754            Some(b't' | b'f') => {
755                self.boolean()?;
756            }
757            Some(c) if c.is_ascii_digit() || c == b'-' => {
758                self.number()?;
759            }
760            got => return Err(format!("unexpected value start {:?}", got.map(char::from))),
761        }
762        Ok(())
763    }
764
765    /// Parses the `{ name: value, ... }` body of a section whose key
766    /// and colon are already consumed, applying `item` to every entry.
767    fn entries<T>(
768        &mut self,
769        item: fn(&mut Self) -> Result<T, String>,
770    ) -> Result<Vec<(String, T)>, String> {
771        self.expect(b'{')?;
772        let mut out = Vec::new();
773        loop {
774            match self.peek() {
775                Some(b'}') => {
776                    self.i += 1;
777                    return Ok(out);
778                }
779                Some(b'"') => {
780                    let name = self.string()?;
781                    self.expect(b':')?;
782                    let value = item(self)?;
783                    out.push((name, value));
784                    if self.peek() == Some(b',') {
785                        self.i += 1;
786                    }
787                }
788                got => {
789                    return Err(format!(
790                        "expected key or }}, found {:?}",
791                        got.map(char::from)
792                    ));
793                }
794            }
795        }
796    }
797}
798
799#[cfg(test)]
800mod tests {
801    use super::{
802        Committed, Digest, Json, TOL_ABS, TOL_REL, digest_of, digest_values, hx, reference_json,
803        vectors, verify_str,
804    };
805
806    /// The canonical render is deterministic: two runs agree.
807    #[test]
808    fn render_is_deterministic() {
809        assert_eq!(reference_json(), reference_json());
810    }
811
812    /// The committed file verifies against a fresh recomputation.
813    #[test]
814    fn verify_accepts_current() {
815        verify_str(&reference_json()).expect("current render verifies");
816    }
817
818    /// Replaces `from` with `to`, asserting `from` occurs exactly once
819    /// so the perturbation lands where the test aims.
820    fn replace_unique(committed: &str, from: &str, to: &str) -> String {
821        assert_eq!(committed.matches(from).count(), 1, "{from} is not unique");
822        committed.replacen(from, to, 1)
823    }
824
825    /// The first digest whose hex occurs exactly once in the render.
826    fn unique_digest_hex(committed: &str) -> (&'static str, String) {
827        digest_values()
828            .iter()
829            .map(|&(name, digest)| (name, format!("{digest:016x}")))
830            .find(|(_, hex)| committed.matches(hex.as_str()).count() == 1)
831            .expect("a digest unique in the render")
832    }
833
834    /// The first oracle output whose hex occurs exactly once in the
835    /// render.
836    fn unique_oracle_hex(committed: &str) -> (f64, String) {
837        vectors()
838            .into_iter()
839            .find(|v| v.name == "idct.oracle.8x8")
840            .expect("oracle vector")
841            .output
842            .into_iter()
843            .map(|v| (v, hx(v)))
844            .find(|(_, hex)| committed.matches(hex.as_str()).count() == 1)
845            .expect("an oracle output unique in the render")
846    }
847
848    /// A stale digest fails verification by name.
849    #[test]
850    fn verify_rejects_stale_digest() {
851        let committed = reference_json();
852        let (name, hex) = unique_digest_hex(&committed);
853        let stale = replace_unique(&committed, &hex, "0000000000000000");
854        let err = verify_str(&stale).expect_err("stale digest must fail");
855        assert!(err.contains(&format!("[digest {name}]")), "{err}");
856    }
857
858    /// A drifted exact vector fails bit-for-bit.
859    #[test]
860    fn verify_rejects_drifted_exact_output() {
861        let exact: Vec<_> = vectors().into_iter().filter(|v| v.exact).collect();
862        let committed = reference_json();
863        // Target an output sample whose hex occurs exactly once in the
864        // render, so the drift lands in an exact vector's output, not
865        // in some other array.
866        let (name, committed_hex, drifted_hex) = exact
867            .iter()
868            .find_map(|v| {
869                v.output.iter().find_map(|&s| {
870                    let hex = hx(s);
871                    let drifted = hx(f64::from_bits(s.to_bits() ^ 1));
872                    (committed.matches(hex.as_str()).count() == 1).then_some((v.name, hex, drifted))
873                })
874            })
875            .expect("an exact output sample unique in the render");
876        let stale = committed.replacen(&committed_hex, &drifted_hex, 1);
877        let err = verify_str(&stale).expect_err("drifted exact output must fail");
878        assert!(err.contains(name), "{err}");
879    }
880
881    /// An in-budget perturbation of the oracle output still verifies:
882    /// the tolerance path is exercised, not just the bit path.
883    #[test]
884    fn verify_accepts_in_budget_oracle_drift() {
885        let committed = reference_json();
886        let (value, hex) = unique_oracle_hex(&committed);
887        // One ulp: far inside max(tol_abs, tol_rel * |expected|) for
888        // every magnitude in this block.
889        let nudged = f64::from_bits(value.to_bits() + 1);
890        let stale = replace_unique(&committed, &hex, &hx(nudged));
891        verify_str(&stale).expect("one-ulp drift is inside budget");
892    }
893
894    /// An out-of-budget perturbation of the oracle output fails.
895    #[test]
896    fn verify_rejects_out_of_budget_oracle_drift() {
897        let committed = reference_json();
898        let (value, hex) = unique_oracle_hex(&committed);
899        let blown = value + 1.0;
900        let stale = replace_unique(&committed, &hex, &hx(blown));
901        let err = verify_str(&stale).expect_err("unit drift must fail");
902        assert!(err.contains("idct.oracle.8x8"), "{err}");
903    }
904
905    /// An unknown extra vector in the committed file is rejected.
906    #[test]
907    fn verify_rejects_unknown_vector() {
908        let extra = format!(
909            "    \"bogus.vector\": {{\n      \"exact\": true,\n      \"tol_abs\": \"{}\",\n      \"tol_rel\": \"{}\",\n      \"input\": [\"0000000000000000\"],\n      \"output\": [\"0000000000000000\"]\n    }},\n",
910            hx(TOL_ABS),
911            hx(TOL_REL)
912        );
913        let stale = reference_json().replacen(
914            "  \"vectors\": {\n",
915            &format!("  \"vectors\": {{\n{extra}"),
916            1,
917        );
918        let err = verify_str(&stale).expect_err("unknown vector must fail");
919        assert!(err.contains("bogus.vector"), "{err}");
920    }
921
922    /// Every digest recomputes to a stable, non-zero value.
923    #[test]
924    fn digests_are_stable_and_nonzero() {
925        for d in digest_values() {
926            assert_ne!(d.1, 0, "{} digests to zero", d.0);
927        }
928        let d = Digest {
929            name: "x",
930            file: "base_444.jpg",
931        };
932        assert_eq!(
933            digest_of(&d),
934            digest_values()
935                .iter()
936                .find(|(n, _)| *n == "base_444")
937                .unwrap()
938                .1
939        );
940    }
941
942    /// The tolerance budgets match the suite convention.
943    #[test]
944    fn tolerance_budgets_are_the_suite_floors() {
945        assert_eq!(hx(TOL_ABS), "3d3c25c268497682");
946        assert_eq!(hx(TOL_REL), "3d719799812dea11");
947    }
948
949    /// The JSON walker reports malformed input instead of panicking.
950    #[test]
951    fn json_walker_rejects_garbage() {
952        let mut p = Json::new("{");
953        assert!(p.entries(Json::hex_u64).is_err());
954        let mut p = Json::new("{\"a\": \"zz\"}");
955        assert!(p.entries(Json::hex_u64).is_err());
956        let mut p = Json::new("\"zz\" ");
957        assert!(p.hex_u64().is_err());
958        let mut p = Json::new("[1, 2]");
959        assert!(p.f64_array().is_err());
960        let mut p = Json::new("\"true\"");
961        assert!(p.boolean().is_err());
962        let mut p = Json::new("{} ");
963        assert!(p.skip_value().is_ok());
964        let mut p = Json::new("]");
965        assert!(p.skip_value().is_err());
966    }
967
968    /// verify_str refuses structurally broken committed files.
969    #[test]
970    fn verify_rejects_broken_structure() {
971        assert!(verify_str("").is_err()); // no object at all
972        assert!(verify_str("[1]").is_err()); // not an object
973        assert!(verify_str("{\"digests\": 5}").is_err()); // section not an object
974        // An unknown top-level section carrying an object or an array
975        // is skipped, then the missing-digest failure surfaces.
976        assert!(verify_str("{\"bogus\": {\"a\": [1]}, \"tail\": [1, 2]}").is_err());
977        // A vector entry with an unexpected key.
978        assert!(verify_str("{\"vectors\": {\"v\": {\"bogus\": 1}}}").is_err());
979        // A vector entry whose exact flag is not a boolean.
980        assert!(verify_str("{\"vectors\": {\"v\": {\"exact\": 1}}}").is_err());
981        // A digest entry that is not hex.
982        assert!(verify_str("{\"digests\": {\"a\": \"zz\"}}").is_err());
983        // A committed file with neither section fails on the digests.
984        assert!(verify_str("{}").is_err());
985    }
986
987    /// digest_of must be loud about unreadable or undecodable
988    /// fixtures: a generator that cannot reproduce a digest is a bug.
989    #[test]
990    #[should_panic(expected = "cannot read fixture")]
991    fn missing_fixture_panics() {
992        let _ = digest_of(&Digest {
993            name: "absent",
994            file: "does_not_exist.jpg",
995        });
996    }
997
998    /// A fixture that exists but is not a JPEG panics with a decode
999    /// diagnosis.
1000    #[test]
1001    #[should_panic(expected = "no longer decodes")]
1002    fn undecodable_fixture_panics() {
1003        let _ = digest_of(&Digest {
1004            name: "junk",
1005            file: "PROVENANCE.md",
1006        });
1007    }
1008
1009    /// Vector defaults parse back from a minimal object.
1010    #[test]
1011    fn committed_defaults() {
1012        let mut p = Json::new("{}");
1013        let c: Committed = p.vector().expect("empty object");
1014        assert!(!c.exact);
1015        assert!(c.input.is_empty() && c.output.is_empty());
1016    }
1017}