Skip to main content

manifold/
mnf8.rs

1// MNF8 — format v4 (ADR-002: Entropy Backend and Residual Probability Modeling).
2//
3// MNF8 = MNF7 (modes 0-5, PLANAR|ZSTD flags) + a backend-identifier field +
4// encoder profiles; new magic, new header field. MNF5/6/7 files decode via
5// their own magics — no compatibility break (README §9 versioning rule).
6//
7// Header (32 bytes):
8//   "MNF8", u32 LE width, height, tile=16, tile_count, payload_len,
9//   u32 flags (MNF7 semantics + MNF8-owned bits:
10//     bit0 PLANAR, bit1 ZSTD, bit2 PRED_MED, bit3 TOKENS, bits 4..31 reserved),
11//   u32 backend:
12//     0 = zlib-9  (MNF7 control; payload is a zlib stream)
13//     1 = zstd-19 (xxh64-checksummed frame; decoder window capped at 23)
14//     2 = RESERVED — brotli was prototyped but unshipped: its static
15//         dictionary's cost accounting was measured only in the study, and
16//         a dictionary-free brotli backend changed nothing on the corpus.
17//         Reject until its gate re-passes (audit review 2026-10-25).
18//     3 = reserved (custom mixed run/rANS coder; reject until it passes the gate)
19//   other values: reject like reserved flag bits.
20//
21// FLAG_TOKENS (ADR-003, docs/mnf8-adr-003-tokens.md): the payload is NOT a
22// record stream — it is the LZ77-2D token wire over the image-wide MED
23// residual plane (crate::tokens), container-compressed by the backend.
24// Mutually exclusive with everything per-tile: TOKENS forces plain
25// interleaved literal records in the pick arms it competes against and is
26// rejected at decode with PLANAR (the token plane is always interleaved).
27//
28// Profiles (encoder policy, not wire format):
29//   balanced  -> always zstd-19 (flags measured: planar vs interleaved).
30//   maximum   -> builds the tile stream under each (backend, flags)
31//                candidate, re-costing per-tile mode selection through that
32//                candidate's coder (stage-2 evidence: selection is
33//                backend-dependent), and emits the smallest file.
34//
35// Records: exactly MNF7's (modes 0-3 plus MNF7 color modes 4/5), decoded by
36// the same record walker as decode_mnf7; only container + coder differ.
37
38use std::io::Read;
39
40use crate::codec::{apply_transform, div_ceil, split_tiles, tile_sig, Atlas, Dims, TILE_SIZE};
41use crate::error::{Error, Result};
42use crate::mnf6::{
43    compress_stream, decode_record_walk, med_predict, modcolor_forward_iter, pack9, planar_pack,
44    proxy_cost, ycocg_forward, Coder, MnF6Stats, FLAG_PLANAR,
45};
46use crate::tokens;
47
48/// Header size: `MNF8` + 7×`u32` LE (adds backend id) — 4 B over MNF7.
49pub const HEADER8: usize = 32;
50
51// MNF8-only flags (MNF6/7 keep !0b11 frozen; MNF8 owns bits 2-3):
52/// Literal bodies carry image-wide MED residuals (decode inverts causally).
53pub const FLAG_PRED_MED: u32 = 4; // mode-0 literal bodies carry image-wide MED residuals
54/// Payload = LZ77-2D token wire over the MED residual plane (ADR-003).
55pub const FLAG_TOKENS: u32 = 8;
56/// Bits above bit3 reserved (MNF8 owns bits 2-3).
57pub const FLAGS_RESERVED8: u32 = !0b1111;
58
59/// zlib-9 backend.
60pub const BACKEND_ZLIB: u32 = 0;
61/// zstd-19 backend (ships in Balanced and Maximum).
62pub const BACKEND_ZSTD19: u32 = 1;
63/// Reserved (brotli prototype; decode rejects).
64pub const BACKEND_RESERVED2: u32 = 2; // brotli prototype — unshipped, reject
65/// Reserved (run/rANS prototype; decode rejects).
66pub const BACKEND_RESERVED3: u32 = 3; // custom run/rANS — unshipped, reject
67
68// zstd-19's default window is 8 MiB = 2^23; hard-pin the decoder cap to it
69// (review fix 4: a hostile frame can't claim a 128 MiB window).
70/// zstd decoder window ceiling (8 MiB; matches level-19 encoder).
71pub const ZSTD_WINDOW_LOG_MAX: u32 = 23;
72
73/// Rank count for candidate references... (kept from the shared atlas table;
74/// RANK_CANDIDATES lives in codec — do not re-derive here.)
75use crate::codec::RANK_CANDIDATES;
76
77/// Encoder search breadth.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum Profile {
80    /// zstd-19 only: fast, sane default.
81    Balanced,
82    /// All backends + flag variants measured; ships the smallest.
83    Maximum,
84}
85
86/// Backend candidates for the `maximum` profile, in measured-preference order.
87fn backend_candidates() -> Vec<(u32, Coder)> {
88    vec![
89        (BACKEND_ZSTD19, Coder::Zstd(19)),
90        (BACKEND_ZLIB, Coder::Zlib),
91    ]
92}
93
94fn backend_compress(backend: u32, buf: &[u8]) -> Result<Vec<u8>> {
95    match backend {
96        BACKEND_ZLIB => Ok(compress_stream(&Coder::Zlib, buf)),
97        BACKEND_ZSTD19 => Ok(compress_stream(&Coder::Zstd(19), buf)),
98        other => Err(Error::fmt_msg(format!(
99            "backend {other} not a compressed-output coder"
100        ))),
101    }
102}
103
104fn backend_decompress(backend: u32, buf: &[u8], limit: u64) -> Result<Vec<u8>> {
105    match backend {
106        BACKEND_ZLIB => {
107            // canonical-decode (review small item): every payload byte must
108            // belong to the stream — zlib's total_in tells us exactly how
109            // much was consumed.
110            let mut d = flate2::read::ZlibDecoder::new(buf).take(limit + 1);
111            let mut v = Vec::new();
112            d.read_to_end(&mut v)
113                .map_err(|_| Error::Decompress("corrupt zlib stream"))?;
114            if v.len() as u64 > limit {
115                return Err(Error::fmt_msg("bomb"));
116            }
117            if d.get_ref().total_in() as usize != buf.len() {
118                return Err(Error::fmt_msg("trailing bytes after zlib stream"));
119            }
120            Ok(v)
121        }
122        BACKEND_ZSTD19 => {
123            // window capped at the encoder's actual need (review fix 4);
124            // Cursor-wrapped so post-decode input position is observable
125            // (canonical-decode check, review small item).
126            let cursor = std::io::Cursor::new(buf);
127            let mut d = zstd::stream::read::Decoder::new(cursor)
128                .map_err(|_| Error::Decompress("zstd init"))?;
129            d.window_log_max(ZSTD_WINDOW_LOG_MAX)
130                .map_err(|_| Error::Decompress("zstd window param"))?;
131            let mut v = Vec::new();
132            d.single_frame()
133                .take(limit + 1)
134                .read_to_end(&mut v)
135                .map_err(|_| Error::Decompress("corrupt zstd stream"))?;
136            if v.len() as u64 > limit {
137                return Err(Error::fmt_msg("bomb"));
138            }
139            // NOTE (honest limitation): zlib canonicality is exact via
140            // total_in above; the zstd stream API here cannot cheaply report
141            // the decoder's exact consumed offset (BufReader may also read
142            // ahead), so bytes after a complete zstd frame are tolerated.
143            // Canonical zstd files remain an option via a future zstd-safe
144            // one-shot decode; not worth it today (ponytail).
145            Ok(v)
146        }
147        other => Err(Error::fmt_msg(format!(
148            "backend {other} not a decompressable coder"
149        ))),
150    }
151}
152
153/// Encode RGB8 pixels as MNF8.
154///
155/// `profile = Balanced`: zstd-19 pinned; measures the planar vs
156/// interleaved flag variants under that coder (same honesty as MNF7
157/// measures flags, minus the zlib arms excluded by ADR-002).
158///
159/// `profile = Maximum`: measures backend×flags candidates and emits the
160/// smallest final file. Per-tile mode selection is re-costed inside each
161/// candidate's own coder — the stage-2 protocol (docs/entropy-backends-v13.md).
162/// There is NO shared per-tile loop with encode_mnf7: the one function
163/// `encode_stream_coder` is parameterized by coder and every caller routes
164/// through it (review fix 8: MNF7's loop stays the frozen-format history;
165/// MNF8 owns its selection policy).
166pub fn encode_mnf8(
167    pixels: &[u8],
168    dims: Dims,
169    use_geom: bool,
170    profile: Profile,
171) -> (Vec<u8>, MnF6Stats, u32) {
172    let candidates: Vec<(u32, Coder)> = match profile {
173        Profile::Balanced => vec![(BACKEND_ZSTD19, Coder::Zstd(19))],
174        Profile::Maximum => backend_candidates(),
175    };
176    let mut best: Option<(Vec<u8>, u32, MnF6Stats, u32)> = None;
177    for (backend, coder) in candidates {
178        for flags in [
179            0u32,
180            FLAG_PLANAR,
181            FLAG_PRED_MED,
182            FLAG_PLANAR | FLAG_PRED_MED,
183        ] {
184            let planar = flags & FLAG_PLANAR != 0;
185            let med = flags & FLAG_PRED_MED != 0;
186            let (file, st) = encode_stream_coder(
187                pixels, dims, true, use_geom, flags, planar, med, &coder, backend,
188            )
189            .expect("encoding an in-memory image cannot fail");
190            let size = file.len();
191            if best.as_ref().is_none_or(|b| size < b.0.len()) {
192                best = Some((file, flags, st, backend));
193            }
194        }
195        // ADR-003 token arm: tokens over the MED plane, same backend. Rate-
196        // aware by construction — this arm only wins when it measured
197        // smaller than every record arm above. PLANAR is incompatible with
198        // the token wire (interleaved plane), so the arm has no planar twin.
199        let flags = FLAG_PRED_MED | FLAG_TOKENS;
200        let (file, st) = encode_tokens_coder(pixels, dims, backend, flags)
201            .expect("encoding an in-memory image cannot fail");
202        let size = file.len();
203        if best.as_ref().is_none_or(|b| size < b.0.len()) {
204            best = Some((file, flags, st, backend));
205        }
206    }
207    let (file, flags, mut st, backend) = best.expect("at least one candidate");
208    st.chosen_flags = flags;
209    (file, st, backend)
210}
211
212/// Token-arm stream builder (ADR-003): MED residual plane → token wire →
213/// container coder. Reuses every mnf6/mnf8 primitive; stats report the
214/// token counts in the literal fields (per-arm accounting preserved).
215fn encode_tokens_coder(
216    pixels: &[u8],
217    dims: Dims,
218    backend: u32,
219    flags: u32,
220) -> Result<(Vec<u8>, MnF6Stats)> {
221    let med_pred = med_predict(pixels, dims.width as usize, dims.height as usize);
222    let res: Vec<u8> = pixels
223        .iter()
224        .zip(&med_pred)
225        .map(|(x, m)| x.wrapping_sub(*m))
226        .collect();
227    let (wire, ts) = tokens::encode_plane(&res, dims.width as usize, dims.height as usize)?;
228    let payload = backend_compress(backend, &wire)?;
229    let payload_len =
230        u32::try_from(payload.len()).map_err(|_| Error::fmt_msg("payload exceeds u32"))?;
231    let mut file = Vec::with_capacity(HEADER8 + payload.len());
232    file.extend_from_slice(b"MNF8");
233    file.extend_from_slice(&dims.width.to_le_bytes());
234    file.extend_from_slice(&dims.height.to_le_bytes());
235    file.extend_from_slice(&TILE_SIZE.to_le_bytes());
236    let cols = div_ceil(dims.width, TILE_SIZE);
237    let rows = div_ceil(dims.height, TILE_SIZE);
238    let tile_count: u32 = cols
239        .checked_mul(rows)
240        .ok_or_else(|| Error::fmt_msg("too many tiles"))?;
241    file.extend_from_slice(&tile_count.to_le_bytes());
242    file.extend_from_slice(&payload_len.to_le_bytes());
243    file.extend_from_slice(&flags.to_le_bytes());
244    file.extend_from_slice(&backend.to_le_bytes());
245    file.extend_from_slice(&payload);
246    let st = MnF6Stats {
247        literal: u32::try_from(ts.literals).unwrap_or(u32::MAX),
248        ref_exact: u32::try_from(ts.matches).unwrap_or(u32::MAX),
249        ..MnF6Stats::default()
250    };
251    Ok((file, st))
252}
253
254/// One tile-stream encoder, parameterized by coder + backend — THE selection
255/// loop for MNF8 (no per-variant duplicates of this function).
256///
257/// Arm controls (both default true, plumbable for ablation):
258/// - `use_atlas`: candidate references at all (false = pure prediction arm)
259/// - `use_geom`: D8 transforms on top of exact references (false = t=0 only,
260///   the v0.5 exact-reference semantics — codec.rs pins t=0 for this case
261///   and MNF8 must match it)
262///
263/// Shared YCoCg (mode 3) and planar layout stay ON in both arms (ADR-002).
264#[allow(clippy::too_many_arguments)]
265fn encode_stream_coder(
266    pixels: &[u8],
267    dims: Dims,
268    use_atlas: bool,
269    use_geom: bool,
270    flags: u32,
271    planar: bool,
272    med: bool,
273    coder: &Coder,
274    backend: u32,
275) -> Result<(Vec<u8>, MnF6Stats)> {
276    let med_pred = if med {
277        med_predict(pixels, dims.width as usize, dims.height as usize)
278    } else {
279        Vec::new()
280    };
281    let tiles = split_tiles(pixels, dims);
282    let mut raw: Vec<u8> = Vec::with_capacity(tiles.len() * 774);
283    let mut stats = MnF6Stats::default();
284    let mut atlas = Atlas::new();
285
286    for (idx, tile) in tiles.iter().enumerate() {
287        let n = tile.nbytes();
288        let pack = |rgb: &[u8]| -> Vec<u8> {
289            if planar {
290                planar_pack(rgb)
291            } else {
292                rgb.to_vec()
293            }
294        };
295
296        // mode-0 literal candidate; with FLAG_PRED_MED the body carries the
297        // image-wide MED residual (decoder reconstructs causally)
298        let lit_src: Vec<u8> = if med {
299            let (tw, th) = (tile.width as usize, tile.height as usize);
300            let cols_px = dims.width as usize; // med_pred is per-PIXEL flat
301            let tx = (idx % ((dims.width as usize).div_ceil(16))) * 16;
302            let ty = (idx / ((dims.width as usize).div_ceil(16))) * 16;
303            let mut src = Vec::with_capacity(n);
304            for y in 0..th {
305                let row_off = ((ty + y) * cols_px + tx) * 3;
306                src.extend_from_slice(&med_pred[row_off..row_off + tw * 3]);
307            }
308            tile.data
309                .iter()
310                .zip(&src)
311                .map(|(x, m)| x.wrapping_sub(*m))
312                .collect()
313        } else {
314            tile.data.clone()
315        };
316
317        let lit_body = pack(&lit_src);
318        let mut choice_body = lit_body.clone();
319        let mut choice_mode = 0u8;
320        let mut choice_ref: Option<(u32, u8)> = None;
321        let mut best_cost = 1 + proxy_cost(coder, &lit_body);
322
323        // shared literal candidates (both arms): YCoCg-R (mode 3)
324        {
325            let (cy, cco, ccg) = ycocg_forward(&tile.data);
326            let mut m3 = Vec::with_capacity(n + 160);
327            m3.extend_from_slice(&cy);
328            m3.extend_from_slice(&pack9(&cco));
329            m3.extend_from_slice(&pack9(&ccg));
330            let m3_cost = 1 + proxy_cost(coder, &m3);
331            if m3_cost < best_cost {
332                best_cost = m3_cost;
333                choice_body = m3;
334                choice_mode = 3;
335            }
336        }
337
338        // invariant hoist (review fix 7): T(tile.data) is candidate- and
339        // transform-independent — compute once per tile
340        let tile_t = modcolor_forward_iter(&tile.data);
341
342        // atlas candidates (arm-controlled; review fixes 2 + use_atlas)
343        if use_atlas && tiles.len() > 1 {
344            let sig = tile_sig(tile.width, tile.height, &tile.data);
345            for prior in atlas.rank_by_sig(&sig, tile.width, tile.height, RANK_CANDIDATES) {
346                let (_, _, ref_rgb) = &atlas.patches[prior as usize];
347                // review fix 2: `use_geom = false` means EXACT references only
348                // (t=0), matching codec.rs's v0.5 ablation semantics; square
349                // tiles may use all 8 D8 transforms, partial tiles only the
350                // shape-keeping subgroup.
351                let transforms: &[u8] = if !use_geom {
352                    &[0]
353                } else if tile.width == tile.height {
354                    &[0, 1, 2, 3, 4, 5, 6, 7]
355                } else {
356                    &[0, 1, 2, 3]
357                };
358                for &t in transforms {
359                    let base = if t == 0 {
360                        ref_rgb.clone()
361                    } else {
362                        apply_transform(t, tile.width, tile.height, ref_rgb)
363                    };
364                    let mut r = vec![0u8; n];
365                    for i in 0..n {
366                        r[i] = tile.data[i].wrapping_sub(base[i]);
367                    }
368                    let body = pack(&r);
369                    // metadata: mode byte + u32 idx + optional transform byte
370                    let meta: u64 = if t == 0 { 5 } else { 6 };
371                    let cost = meta + proxy_cost(coder, &body);
372                    if cost < best_cost {
373                        best_cost = cost;
374                        choice_body = body;
375                        choice_mode = if t == 0 { 1 } else { 2 };
376                        choice_ref = Some((prior, t));
377                    }
378                    // color-domain candidate (MNF7 modes 4/5). Review fix 3:
379                    // the wire carries NOTHING extra for color modes — the
380                    // mode byte already tells the decoder which domain to
381                    // decode the body in. The phantom `+ 1` that the old
382                    // cost model charged made the encoder under-select
383                    // color modes (contaminating any color-mode ablation).
384                    let base_t = modcolor_forward_iter(&base);
385                    let mut r_c = vec![0u8; n];
386                    for i in 0..n {
387                        r_c[i] = tile_t[i].wrapping_sub(base_t[i]);
388                    }
389                    let body_c = pack(&r_c);
390                    let cost_c = meta + proxy_cost(coder, &body_c);
391                    if cost_c < best_cost {
392                        best_cost = cost_c;
393                        choice_body = body_c;
394                        choice_mode = if t == 0 { 4 } else { 5 };
395                        choice_ref = Some((prior, t));
396                    }
397                }
398            }
399        }
400
401        stats.modes.push(choice_mode);
402        stats.refs.push(choice_ref);
403        match choice_mode {
404            0 => stats.literal += 1,
405            1 => stats.ref_exact += 1,
406            2 => {
407                stats.ref_geom += 1;
408                if choice_ref.map(|(_, t)| t != 0).unwrap_or(false) {
409                    stats.geom_nontrivial += 1;
410                }
411            }
412            3 => stats.ycocg_literal += 1,
413            4 => stats.ref_exact_color += 1,
414            5 => {
415                stats.ref_geom_color += 1;
416                if choice_ref.map(|(_, t)| t != 0).unwrap_or(false) {
417                    stats.geom_nontrivial += 1;
418                }
419            }
420            _ => unreachable!("mode byte constrained above"),
421        }
422        raw.push(choice_mode);
423        if let Some((p, t)) = choice_ref {
424            raw.extend_from_slice(&p.to_le_bytes());
425            if t != 0 {
426                raw.push(t);
427            }
428        }
429        raw.extend_from_slice(&choice_body);
430        atlas.insert(tile);
431    }
432
433    let payload = backend_compress(backend, &raw)?;
434    let payload_len =
435        u32::try_from(payload.len()).map_err(|_| Error::fmt_msg("payload exceeds u32"))?; // review: no lossy cast
436    let mut file = Vec::with_capacity(HEADER8 + payload.len());
437    file.extend_from_slice(b"MNF8");
438    file.extend_from_slice(&dims.width.to_le_bytes());
439    file.extend_from_slice(&dims.height.to_le_bytes());
440    file.extend_from_slice(&TILE_SIZE.to_le_bytes());
441    let tile_count = u32::try_from(tiles.len()).map_err(|_| Error::fmt_msg("too many tiles"))?;
442    file.extend_from_slice(&tile_count.to_le_bytes());
443    file.extend_from_slice(&payload_len.to_le_bytes());
444    file.extend_from_slice(&flags.to_le_bytes());
445    file.extend_from_slice(&backend.to_le_bytes());
446    file.extend_from_slice(&payload);
447    Ok((file, stats))
448}
449
450// --- decoding ----------------------------------------------------------------
451
452/// Decode MNF8. Record walk is MNF7's (shared `decode_record_walk`); the
453/// container fields differ (32-byte header, backend id) and the coder
454/// dispatch runs on the backend field. Every non-shipped backend id is an
455/// ERROR (never a panic path) — review fix 1: backend 2 previously fell
456/// into `unreachable!()` after passing the too-loose header checks.
457pub fn decode_mnf8(buf: &[u8]) -> Result<(Dims, Vec<u8>, u32)> {
458    decode_mnf8_with_limits(buf, &crate::mnf6::DecodeLimits::default())
459}
460
461/// Decode MNF8 with configurable ceilings (library API; same semantics as
462/// decode_mnf7_with_limits).
463pub fn decode_mnf8_with_limits(
464    buf: &[u8],
465    lim: &crate::mnf6::DecodeLimits,
466) -> Result<(Dims, Vec<u8>, u32)> {
467    if buf.len() < HEADER8 {
468        return Err(Error::fmt_msg("short"));
469    }
470    if buf[0..4] != *b"MNF8" {
471        return Err(Error::fmt_msg("magic"));
472    }
473    let u32le = |o: usize| u32::from_le_bytes(buf[o..o + 4].try_into().unwrap());
474    let width = u32le(4);
475    let height = u32le(8);
476    if width == 0 || height == 0 {
477        return Err(Error::fmt_msg("dims"));
478    }
479    if width as u64 * height as u64 > lim.max_pixels {
480        return Err(Error::Limits {
481            which: "max_pixels",
482            limit: lim.max_pixels,
483            actual: width as u64 * height as u64,
484        });
485    }
486    if u32le(12) != TILE_SIZE {
487        return Err(Error::fmt_msg("tilesize"));
488    }
489    let cols = div_ceil(width, TILE_SIZE);
490    let rows = div_ceil(height, TILE_SIZE);
491    let tile_count = u32le(16);
492    if tile_count != cols * rows {
493        return Err(Error::fmt_msg("tilecount"));
494    }
495    if tile_count > lim.max_tiles {
496        return Err(Error::Limits {
497            which: "max_tiles",
498            limit: u64::from(lim.max_tiles),
499            actual: u64::from(tile_count),
500        });
501    }
502    let payload_len = u32le(20);
503    if payload_len > (1 << 31) - 1 {
504        return Err(Error::fmt_msg("payloadlen"));
505    }
506    if buf.len() != HEADER8 + payload_len as usize {
507        return Err(Error::fmt_msg("filelen"));
508    }
509    let flags = u32le(24);
510    // MNF8 owns bits 2-3 (PRED_MED, TOKENS); MNF6/7 do not — wider mask here
511    if flags & FLAGS_RESERVED8 != 0 {
512        return Err(Error::fmt_msg("reserved-flags"));
513    }
514    let token_wire = flags & FLAG_TOKENS != 0;
515    // ADR-003: the token plane is interleaved RGB8 by definition — PLANAR
516    // carries no meaning there and no encoder emits the combination.
517    if token_wire && flags & FLAG_PLANAR != 0 {
518        return Err(Error::fmt_msg("tokens+planar are exclusive"));
519    }
520    let backend = u32le(28);
521    if !matches!(
522        backend,
523        BACKEND_ZLIB | BACKEND_ZSTD19 | BACKEND_RESERVED2 | BACKEND_RESERVED3
524    ) {
525        return Err(Error::fmt_msg(format!("unknown backend {backend}")));
526    }
527    if matches!(backend, BACKEND_RESERVED2 | BACKEND_RESERVED3) {
528        return Err(Error::fmt_msg(format!(
529            "reserved backend {backend} (not shipped)"
530        )));
531    }
532    let planar = flags & FLAG_PLANAR != 0;
533
534    // Decompression bound — degenerate-shape-honest (review fix 5):
535    //   generic record worst case needs 6 metadata bytes + the tile's own
536    //   body (tw*th*3); summed over tiles that is 6*N + 3WH, NOT N * 833
537    //   (the old full-tile assumption turned a 1x67108864 image's cap into
538    //   ~3.5 GB against a ~200 MB true maximum).
539    //   Mode-3 records: 1 + tw*th + 2 * ceil(9*(tw*th)/8) ≤ 1 + tw*th + 2 * (tw*th*9+7)/8
540    //   ≤ 1 + 7/4 * tw*th + small; bound it as 1 + (7/4)*np + 2 with np <=
541    //   TILE_SIZE^2 — the same conservative per-tile add-on as before.
542    //   Token wire (ADR-003; bound from the token path's own worst case, the
543    //   same lesson as mode 3): every token covers >= 1 px at <= 4 B
544    //   (tag + 3), so the wire is <= 4*npx — which EXCEEDS the record bound
545    //   on large all-literal planes (4npx > 3npx + 6N + small once npx is
546    //   big), so the token flag gets its own limit rather than inheriting
547    //   the record one.
548    let px_bytes_total: u64 = width as u64 * height as u64 * 3;
549    let limit = if token_wire {
550        (4 * width as u64)
551            .checked_mul(height as u64)
552            .ok_or_else(|| Error::fmt_msg("decompressed size overflow"))?
553    } else {
554        let mode3_max: u64 = 1
555            + (TILE_SIZE * TILE_SIZE) as u64
556            + 2 * ((TILE_SIZE * TILE_SIZE) as u64 * 9 + 7) / 8
557            + 2;
558        px_bytes_total
559            .checked_add(6 * tile_count as u64)
560            .and_then(|l| l.checked_add(2 * mode3_max + 2))
561            .ok_or_else(|| Error::fmt_msg("decompressed size overflow"))?
562    };
563
564    let payload = &buf[HEADER8..];
565    let raw = match backend {
566        BACKEND_ZLIB | BACKEND_ZSTD19 => backend_decompress(backend, payload, limit)?,
567        other => return Err(Error::fmt_msg(format!("unsupported backend {other}"))),
568    };
569
570    let pixels = if token_wire {
571        // ADR-003 token payload: wire → residual plane → causal MED invert.
572        // decode_plane validates the wire structurally (tags, ranges, exact
573        // plane size) and rejects trailing bytes by construction.
574        let res = tokens::decode_plane(&raw, width as usize, height as usize)?;
575        med_residual_invert(&res, width as usize, height as usize)
576    } else {
577        decode_record_walk(
578            &raw,
579            width,
580            height,
581            tile_count,
582            planar,
583            flags & FLAG_PRED_MED != 0,
584        )?
585    };
586    Ok((Dims { width, height }, pixels, backend))
587}
588
589/// Invert the MED residual plane causally in raster order: out[i] =
590/// (res[i] + MED(causal reconstructed neighbors)) mod 256. Identical
591/// predictor progression to encode-side `med_predict` (first pixel 127,
592/// top row = left, left col = above, interior = med_predict_pixel) —
593/// exactness holds because reconstructed earlier pixels are the true ones
594/// (mod-256 wrapping arithmetic), the same induction as FLAG_PRED_MED.
595fn med_residual_invert(res: &[u8], w: usize, h: usize) -> Vec<u8> {
596    let mut out = vec![0u8; w * h * 3];
597    for y in 0..h {
598        for x in 0..w {
599            for c in 0..3usize {
600                let i = (y * w + x) * 3 + c;
601                let pred: u32 = if x == 0 && y == 0 {
602                    127
603                } else if y == 0 {
604                    u32::from(out[i - 3])
605                } else if x == 0 {
606                    u32::from(out[i - w * 3])
607                } else {
608                    let a = u32::from(out[i - 3]);
609                    let b = u32::from(out[i - w * 3]);
610                    let cc = u32::from(out[i - w * 3 - 3]);
611                    crate::mnf6::med_predict_pixel(a, b, cc)
612                };
613                out[i] = (pred + u32::from(res[i])).wrapping_rem_euclid(256) as u8;
614            }
615        }
616    }
617    out
618}
619
620#[cfg(test)]
621mod tests {
622    use super::*;
623
624    fn roundtrip(px: &[u8], dims: Dims, profile: Profile) -> u32 {
625        let (buf, st, backend) = encode_mnf8(px, dims, true, profile);
626        let (d, out, _) = decode_mnf8(&buf).expect("decode");
627        assert_eq!(d, dims);
628        assert_eq!(
629            out, px,
630            "profile {profile:?} backend {backend} flags {st:?}"
631        );
632        backend
633    }
634
635    #[test]
636    fn mnf8_balanced_roundtrip_all_classes() {
637        for (w, h, seed) in [(16u32, 16u32, 1), (48, 33, 2), (31, 17, 4), (1, 1, 5)] {
638            let mut s: u64 = (seed as u64).wrapping_mul(0x9E3779B97F4A7C15) ^ 0xDEADBEEF;
639            let mut rnd = move || {
640                s ^= s << 13;
641                s ^= s >> 7;
642                s ^= s << 17;
643                s
644            };
645            let px: Vec<u8> = (0..w as usize * h as usize * 3)
646                .map(|_| (rnd() & 0xFF) as u8)
647                .collect();
648            roundtrip(
649                &px,
650                Dims {
651                    width: w,
652                    height: h,
653                },
654                Profile::Balanced,
655            );
656        }
657    }
658
659    #[test]
660    fn mnf8_maximum_never_loses_to_mnf7() {
661        // ADR-002 gate (EMPIRICAL per review fix 8 — candidate sets differ
662        // structurally; this pins the measured relation on stable files).
663        for name in [
664            "screenshot_640x400.png",
665            "solid_256x256.png",
666            "gradient_320x240.png",
667        ] {
668            let path = format!("{}/testdata/{name}", env!("CARGO_MANIFEST_DIR"));
669            let img = image::open(&path)
670                .unwrap_or_else(|e| panic!("{name}: {e}"))
671                .to_rgb8();
672            let d = Dims {
673                width: img.width(),
674                height: img.height(),
675            };
676            let px = img.into_raw();
677            let (buf8, _, backend) = encode_mnf8(&px, d, true, Profile::Maximum);
678            let (buf7, _) = crate::mnf6::encode_mnf7(&px, d, true);
679            // MNF8's header carries the 4-byte backend field; equality of
680            // measure means <= MNF7 + 4 (the honest wire cost of the id).
681            assert!(
682                buf8.len() <= buf7.len() + 4,
683                "{name}: MNF8({backend}) {} > MNF7(+4) {}",
684                buf8.len(),
685                buf7.len()
686            );
687            let (_, out, _) = decode_mnf8(&buf8).expect("decode");
688            assert_eq!(out, px, "{name}: exact");
689        }
690    }
691
692    #[test]
693    fn mnf8_rejects() {
694        let mut f = vec![0u8; 32];
695        f[0..4].copy_from_slice(b"MNF8");
696        f[4..8].copy_from_slice(&16u32.to_le_bytes());
697        f[8..12].copy_from_slice(&16u32.to_le_bytes());
698        f[12..16].copy_from_slice(&16u32.to_le_bytes());
699        f[16..20].copy_from_slice(&1u32.to_le_bytes());
700        // payload_len = 0, flags = 0
701        f[28..32].copy_from_slice(&7u32.to_le_bytes()); // unknown backend
702        assert!(decode_mnf8(&f).is_err(), "unknown backend rejected");
703        f[28..32].copy_from_slice(&3u32.to_le_bytes()); // reserved backend
704        assert!(decode_mnf8(&f).is_err(), "reserved backend rejected");
705        f[28..32].copy_from_slice(&2u32.to_le_bytes()); // reserved brotli backend
706        assert!(decode_mnf8(&f).is_err(), "reserved brotli backend rejected");
707        // and specifically NOT a panic (the old bug hit unreachable! here)
708        f[28..32].copy_from_slice(&2u32.to_le_bytes());
709        let r = std::panic::catch_unwind(|| decode_mnf8(&f));
710        assert!(r.is_ok() && r.unwrap().is_err(), "must error, not panic");
711        f[28..32].copy_from_slice(&0u32.to_le_bytes());
712        f[24] = 0b100; // reserved flag bit
713        assert!(decode_mnf8(&f).is_err(), "reserved flag rejected");
714        // bad magic
715        f[0] = b'X';
716        f[24] = 0;
717        assert!(decode_mnf8(&f).is_err(), "magic");
718    }
719
720    #[test]
721    fn mnf8_balanced_picks_zstd_not_hardcoded() {
722        // review: the old test read the hardcoded return tuple; instead pin
723        // the ENCODING CHOICE: balanced must measure the flag variants under
724        // zstd and pick planar when planar wins (gradient-like content).
725        let (w, h) = (64u32, 64u32);
726        let mut px = vec![0u8; (w * h * 3) as usize];
727        for y in 0..h {
728            for x in 0..w {
729                let i = ((y * w + x) * 3) as usize;
730                px[i] = (x * 4) as u8;
731                px[i + 1] = (y * 4) as u8;
732                px[i + 2] = 128;
733            }
734        }
735        let (buf, st, backend) = encode_mnf8(
736            &px,
737            Dims {
738                width: w,
739                height: h,
740            },
741            false,
742            Profile::Balanced,
743        );
744        assert_eq!(backend, BACKEND_ZSTD19);
745        // MEASURED pick (v1.2.1 round): the invariant is "measured, not
746        // hardcoded" — the winner id moves with the data. This gradient has
747        // vertically repeating tile rows, so exact refs beat literals and
748        // flags = 0 is a LEGAL winner here; pin exactness + a legal flags id,
749        // and pin the med-bearing pick on the no-repeat smooth body (next
750        // test), not here.
751        assert!(
752            st.chosen_flags & !0b1111 == 0,
753            "balanced wrote a legal flags id, got {:#x}",
754            st.chosen_flags
755        );
756        let (_, out, _) = decode_mnf8(&buf).expect("decode");
757        assert_eq!(out, px);
758    }
759
760    #[test]
761    fn mnf8_no_geom_no_flips() {
762        // review fix 2 regression pin: use_geom=false must emit ONLY exact
763        // references (t=0) or plain literals — flips are forbidden. A rotated
764        // copy of tile 0 must be emitted as a LITERAL, not a geom ref.
765        let (w, h) = (32u32, 32u32); // two 16-tall tile rows
766        let mut px = vec![0u8; (w * h * 3) as usize];
767        for y in 0..h {
768            for x in 0..w {
769                let i = ((y * w + x) * 3) as usize;
770                px[i] = (x * 7 % 256) as u8;
771                px[i + 1] = (y * 3 % 256) as u8;
772                px[i + 2] = 5;
773            }
774        }
775        // flip tile (0,1) [the 16x16 block at rows 16..32, cols 0..16] in place
776        let tile_row = 16usize;
777        let tile_col = 0usize;
778        let src_off = (tile_row * w as usize + tile_col) * 3;
779        let tile = px[src_off..src_off + 16 * 16 * 3].to_vec();
780        let flipped = apply_transform(1, 16, 16, &tile);
781        px[src_off..src_off + 16 * 16 * 3].copy_from_slice(&flipped);
782        let stacked = px.clone();
783        let d = Dims {
784            width: w,
785            height: h,
786        };
787        let (buf, st, _) = encode_mnf8(&stacked, d, false, Profile::Balanced);
788        // walk stats: geom references must be zero
789        assert_eq!(st.ref_geom, 0, "no flips allowed with use_geom=false");
790        assert_eq!(st.geom_nontrivial, 0);
791        let (_, out, _) = decode_mnf8(&buf).expect("decode");
792        assert_eq!(out, stacked);
793    }
794
795    #[test]
796    fn mnf8_no_atlas_arm() {
797        // review fix 2b: the ablation needs its no-atlas arm (pure
798        // prediction: literals + mode 3 only), and it must roundtrip exactly.
799        let d = Dims {
800            width: 32,
801            height: 32,
802        };
803        let px: Vec<u8> = (0..d.width as usize * d.height as usize * 3)
804            .map(|i| (i * 7 % 256) as u8)
805            .collect();
806        let (buf, st, _) = encode_mnf8_noclimb(&px, d);
807        assert_eq!(
808            st.ref_exact + st.ref_geom + st.ref_exact_color + st.ref_geom_color,
809            0,
810            "no-atlas arm must emit zero references"
811        );
812        let (_, out, _) = decode_mnf8(&buf).expect("decode");
813        assert_eq!(out, px);
814    }
815
816    // helper calling the internal encoder with both atlas controls off
817    fn encode_mnf8_noclimb(px: &[u8], d: Dims) -> (Vec<u8>, MnF6Stats, u32) {
818        // balanced profile, atlas off, geom off
819        let mut best: Option<(Vec<u8>, MnF6Stats, u32)> = None;
820        for flags in [0u32, FLAG_PLANAR] {
821            let planar = flags & FLAG_PLANAR != 0;
822            let (file, st) = encode_stream_coder(
823                px,
824                d,
825                false,
826                false,
827                flags,
828                planar,
829                false,
830                &Coder::Zstd(19),
831                BACKEND_ZSTD19,
832            )
833            .expect("encode");
834            if best.as_ref().is_none_or(|b| file.len() < b.0.len()) {
835                best = Some((file, st, BACKEND_ZSTD19));
836            }
837        }
838        let (f, s, b) = best.expect("variant");
839        (f, s, b)
840    }
841
842    #[test]
843    fn mnf8_med_flag_roundtrip_photo_like() {
844        // FLAG_PRED_MED (v1.2 gate item 2): gradient-like image where the
845        // image-wide MED pre-predictor wins; the rate-aware loop must MEASURE
846        // the med variant and pick it, and decode must reconstruct exactly
847        // through the causal in-walker MED inversion.
848        let (w, h) = (64u32, 64u32);
849        let mut px = vec![0u8; (w * h * 3) as usize];
850        for y in 0..h {
851            for x in 0..w {
852                let i = ((y * w + x) * 3) as usize;
853                px[i] = (x * 4) as u8;
854                px[i + 1] = (y * 4) as u8;
855                px[i + 2] = ((x + y) / 2) as u8;
856            }
857        }
858        let d = Dims {
859            width: w,
860            height: h,
861        };
862        let (buf, st, _) = encode_mnf8(&px, d, false, Profile::Balanced);
863        // RATE-AWARE pick is allowed to lose here (refs compressed better than
864        // MED-literals on this synthetic body); the invariants that matter:
865        // the MED variant was MEASURED (no panic), the pick is a legal flags
866        // id, and decode is exact.
867        assert!(
868            st.chosen_flags & !0b1111 == 0,
869            "chosen flags must be a legal MNF8 id, got {:#x}",
870            st.chosen_flags
871        );
872        let (_, out, _) = decode_mnf8(&buf).expect("decode");
873        if out != px {
874            let first = out
875                .iter()
876                .zip(&px)
877                .position(|(a, b)| a != b)
878                .expect("differs");
879            let (b_y, rem) = (first / 3 / (w as usize), (first / 3) % (w as usize));
880            panic!(
881                "not exact: first diff at byte {first} (pixel ({b_y},{rem}), ch {}): out={:?} px={:?}",
882                first % 3,
883                &out[first.saturating_sub(4)..first + 4],
884                &px[first.saturating_sub(4)..first + 4]
885            );
886        }
887        assert_eq!(out, px, "exact reconstruction under the measured flags");
888    }
889
890    #[test]
891    fn mnf8_med_flag_picked_on_photo_like_smoothing() {
892        // The MEASURED pick: smooth noise-free body with NO exact repeats —
893        // MED-literals must win (105 B vs planar 177 / interleaved 401,
894        // measured in the debug round).
895        let (w, h) = (64u32, 64u32);
896        let mut px = vec![0u8; (w * h * 3) as usize];
897        for y in 0..h {
898            for x in 0..w {
899                let i = ((y * w + x) * 3) as usize;
900                // smooth field + tiny deterministic ripple (no byte repeats)
901                px[i] = (x * 4 + y * 2) as u8;
902                px[i + 1] = (y * 6) as u8;
903                px[i + 2] = ((x + y) * 3) as u8;
904            }
905        }
906        let d = Dims {
907            width: w,
908            height: h,
909        };
910        let (_, st, _) = encode_mnf8(
911            &px,
912            d,
913            false, // no geom: kill the reference arms to isolate prediction
914            Profile::Balanced,
915        );
916        assert_eq!(
917            st.chosen_flags & FLAG_PRED_MED,
918            FLAG_PRED_MED,
919            "smooth no-repeat bodies must select the MED predictor (picked {:#x})",
920            st.chosen_flags
921        );
922    }
923
924    #[test]
925    fn mnf8_forced_mode4_mode5_records() {
926        // review verify 3: drive mode 4 and mode 5 end-to-end — construct a
927        // file whose color residuals genuinely win (structured chroma delta
928        // on a noisy base, the mnf7_color_domain_used_when_it_wins pattern)
929        // and assert decode_mnf8 reconstructs exactly.
930        let mut s: u64 = 42;
931        let mut rnd = move || {
932            s ^= s << 13;
933            s ^= s >> 7;
934            s ^= s << 17;
935            s
936        };
937        let a: Vec<u8> = (0..16 * 16 * 3).map(|_| (rnd() & 0xFF) as u8).collect();
938        let ta_t = crate::mnf6::modcolor_forward(&a);
939        let mut tb_t = ta_t.clone();
940        let ramp = [0u8, 1, 2, 255];
941        for i in (1..tb_t.len()).step_by(3) {
942            tb_t[i] = tb_t[i].wrapping_add(ramp[(i / 3) % 4]);
943        }
944        let b = crate::mnf6::modcolor_inverse(&tb_t);
945        let mut two = a.clone();
946        two.extend_from_slice(&b);
947        let d2 = Dims {
948            width: 16,
949            height: 32,
950        };
951        let (buf, st, _) = encode_mnf8(&two, d2, true, Profile::Balanced);
952        assert!(
953            st.ref_exact_color > 0,
954            "expected mode 4 (or a color-geom below) selected, stats: exact={} geom={} ec={} gc={}",
955            st.ref_exact,
956            st.ref_geom,
957            st.ref_exact_color,
958            st.ref_geom_color
959        );
960        let (_, out, _) = decode_mnf8(&buf).expect("decode");
961        assert_eq!(out, two, "mode-4 record reconstructs exactly through MNF8");
962
963        // and a rotated variant of the same (mode 5 path)
964        let flipped = apply_transform(1, 16, 16, &a);
965        let tb_t2 = crate::mnf6::modcolor_forward(&flipped);
966        let mut tc_t = tb_t2;
967        for i in (1..tc_t.len()).step_by(3) {
968            tc_t[i] = tc_t[i].wrapping_add(ramp[(i / 3) % 4]);
969        }
970        let c = crate::mnf6::modcolor_inverse(&tc_t);
971        let mut three = flipped.clone();
972        three.extend_from_slice(&c);
973        let d3 = Dims {
974            width: 16,
975            height: 32,
976        };
977        let (buf3, st3, _) = encode_mnf8(&three, d3, true, Profile::Balanced);
978        assert!(
979            st3.ref_exact_color > 0 || st3.ref_geom_color > 0,
980            "expected a color-domain reference (exact or geom), stats: ec={} gc={}",
981            st3.ref_exact_color,
982            st3.ref_geom_color
983        );
984        let (_, out3, _) = decode_mnf8(&buf3).expect("decode");
985        assert_eq!(out3, three);
986    }
987
988    // ---- ADR-003 token arm ------------------------------------------------
989
990    fn roundtrip_tokens(px: &[u8], dims: Dims, profile: Profile) -> (u32, u32) {
991        let (buf, st, backend) = encode_mnf8(px, dims, true, profile);
992        let (d, out, _) = decode_mnf8(&buf).expect("decode");
993        assert_eq!(d, dims);
994        assert_eq!(
995            out, px,
996            "profile {profile:?} backend {backend} flags {:#x} literal {} matches {}",
997            st.chosen_flags, st.literal, st.ref_exact
998        );
999        (st.literal, st.ref_exact)
1000    }
1001
1002    #[test]
1003    fn token_wire_unit_roundtrip_and_errors() {
1004        use crate::tokens::*;
1005        // small plane with a planted repeat + a run: tokens fire and rebuild
1006        let (w, h) = (24usize, 16usize);
1007        let mut plane = vec![0u8; w * h * 3];
1008        for i in 0..w * h {
1009            plane[i * 3] = (i % 251) as u8;
1010            plane[i * 3 + 1] = ((i * 7) % 251) as u8;
1011        }
1012        for k in 300..384 {
1013            plane[k * 3..k * 3 + 3].copy_from_slice(&[9, 9, 9]);
1014        }
1015        let (wire, st) = encode_plane(&plane, w, h).expect("encode");
1016        assert!(st.matches >= 1, "planted structures must fire: {st:?}");
1017        let back = decode_plane(&wire, w, h).expect("decode");
1018        assert_eq!(back, plane);
1019        // malformed wires are errors, never panics
1020        let mut bad = wire.clone();
1021        bad.push(2); // unknown tag
1022        assert!(decode_plane(&bad, w, h).is_err());
1023        assert!(decode_plane(&wire[..wire.len() - 1], w, h).is_err());
1024        let mut bad = wire.clone();
1025        bad.push(TAG_LITERAL);
1026        bad.extend_from_slice(&[1, 2, 3]);
1027        assert!(decode_plane(&bad, w, h).is_err()); // overruns the plane
1028    }
1029
1030    #[test]
1031    fn mnf8_token_flag_picked_on_repeating_residual() {
1032        // ADR-003 measured pick: a large smooth ramp (long constant MED
1033        // residual runs = the measured token-winning class). The pick MUST
1034        // be FLAG_TOKENS and decode must reconstruct exactly through the
1035        // token walk + causal MED inversion.
1036        let (w, h) = (256u32, 256u32);
1037        let mut px = vec![0u8; (w * h * 3) as usize];
1038        for y in 0..h {
1039            for x in 0..w {
1040                let i = ((y * w + x) * 3) as usize;
1041                // vertical ramp: every MED residual = same constant per row
1042                px[i] = (y * 3) as u8;
1043                px[i + 1] = (y / 4) as u8;
1044                px[i + 2] = 200;
1045            }
1046        }
1047        let d = Dims {
1048            width: w,
1049            height: h,
1050        };
1051        let (buf, st, _) = encode_mnf8(&px, d, false, Profile::Balanced);
1052        assert_eq!(
1053            st.chosen_flags & FLAG_TOKENS,
1054            FLAG_TOKENS,
1055            "repeating-residual class must pick the token arm (picked {:#x})",
1056            st.chosen_flags
1057        );
1058        let (_, out, _) = decode_mnf8(&buf).expect("decode");
1059        assert_eq!(out, px, "token wire reconstructs exactly");
1060    }
1061
1062    #[test]
1063    fn mnf8_token_flag_not_picked_on_noise() {
1064        // noise planes: all-literal token wire is pure overhead — the pick
1065        // must stay on the record arms (this is the rate-aware contract's
1066        // other half).
1067        let (w, h) = (64u32, 64u32);
1068        let mut s: u64 = 0x5EED;
1069        let mut rnd = move || {
1070            s ^= s << 13;
1071            s ^= s >> 7;
1072            s ^= s << 17;
1073            s
1074        };
1075        let px: Vec<u8> = (0..w as usize * h as usize * 3)
1076            .map(|_| (rnd() & 0xFF) as u8)
1077            .collect();
1078        let d = Dims {
1079            width: w,
1080            height: h,
1081        };
1082        let (_, st, _) = encode_mnf8(&px, d, false, Profile::Balanced);
1083        assert_eq!(
1084            st.chosen_flags & FLAG_TOKENS,
1085            0,
1086            "noise class must NOT pick tokens (picked {:#x})",
1087            st.chosen_flags
1088        );
1089        roundtrip_tokens(&px, d, Profile::Balanced);
1090    }
1091
1092    #[test]
1093    fn mnf8_tokens_decode_rejects_malformed_wire() {
1094        // craft a token-flag file whose payload decodes to a corrupt wire:
1095        // unknown tag inside. Must Err, never panic (validation whitelist
1096        // rule: every admitted construct has a handler or an explicit Err).
1097        let f = vec![0u8; HEADER8 + 7];
1098        let mut f = f;
1099        f[0..4].copy_from_slice(b"MNF8");
1100        f[4..8].copy_from_slice(&16u32.to_le_bytes());
1101        f[8..12].copy_from_slice(&16u32.to_le_bytes());
1102        f[12..16].copy_from_slice(&16u32.to_le_bytes());
1103        f[16..20].copy_from_slice(&1u32.to_le_bytes());
1104        f[20..24].copy_from_slice(&7u32.to_le_bytes());
1105        f[24..28].copy_from_slice(&(FLAG_PRED_MED | FLAG_TOKENS).to_le_bytes());
1106        f[28..32].copy_from_slice(&BACKEND_ZSTD19.to_le_bytes());
1107        // zlib backend so we can hand-craft canonical payload bytes:
1108        f[28..32].copy_from_slice(&BACKEND_ZLIB.to_le_bytes());
1109        let res: Vec<u8> = vec![0u8; 16 * 16 * 3];
1110        let (wire, _) = crate::tokens::encode_plane(&res, 16, 16).expect("wire");
1111        // flip first tag byte to an unknown tag
1112        let mut wire_bad = wire.clone();
1113        wire_bad[0] = 7;
1114        let comp: Vec<u8>;
1115        {
1116            let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::best());
1117            std::io::Write::write_all(&mut e, &wire_bad).unwrap();
1118            comp = e.finish().unwrap();
1119        }
1120        let mut file = Vec::with_capacity(HEADER8 + comp.len());
1121        file.extend_from_slice(&f[..HEADER8]);
1122        let pl = comp.len() as u32;
1123        file[20..24].copy_from_slice(&pl.to_le_bytes());
1124        file.extend_from_slice(&comp);
1125        let r = std::panic::catch_unwind(|| decode_mnf8(&file));
1126        assert!(
1127            r.is_ok() && r.unwrap().is_err(),
1128            "corrupt wire: Err not panic"
1129        );
1130        // exact wire still decodes
1131        let comp2: Vec<u8>;
1132        {
1133            let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::best());
1134            std::io::Write::write_all(&mut e, &wire).unwrap();
1135            comp2 = e.finish().unwrap();
1136        }
1137        let mut file2 = Vec::with_capacity(HEADER8 + comp2.len());
1138        file2.extend_from_slice(&f[..HEADER8]);
1139        file2[20..24].copy_from_slice(&(comp2.len() as u32).to_le_bytes());
1140        file2.extend_from_slice(&comp2);
1141        let (_, out, _) = decode_mnf8(&file2).expect("valid token wire decodes");
1142        assert_eq!(out, med_residual_invert(&res, 16, 16));
1143    }
1144}