Skip to main content

manifold/
refdecode.rs

1// Independent reference decoder for MNF5 (README ยง6: at least one decoder
2// implementation driven by the format specification rather than reused
3// encoder helpers).
4//
5// Deliberately different from codec::decode at every decision point:
6// - validates in spec order Section 1 (magic -> dims -> tile_size ->
7//   tile_count -> payload_len) with its own arithmetic;
8// - parses records with manual bounds checks (no shared cursor pattern);
9// - implements D8 as push maps (source pixel scattered to its output
10//   position) instead of codec's pull maps;
11// - reconstructs references by building the transformed base then adding
12//   residuals via iterator zip.
13
14use crate::codec::Dims;
15use crate::error::{Error, Result};
16use std::io::Read;
17
18const HEADER: usize = 24;
19const TS: u32 = 16;
20const MAX_PIXELS: u64 = 1 << 26;
21const MAX_PAYLOAD: u32 = (1 << 31) - 1;
22
23/// Result of the independent MNF5 decode.
24pub struct RefDecodeResult {
25    /// Decoded dimensions.
26    pub dims: Dims,
27    /// RGB8 pixels, row-major.
28    pub pixels: Vec<u8>,
29}
30
31// Spec-quote helper (docs/format.md uses `ceil`); std div_ceil is the same math.
32fn div_ceil(a: u32, b: u32) -> u32 {
33    a.div_ceil(b)
34}
35
36/// D8 push map: where does transform t place source pixel (sx, sy)?
37/// Returns (out_w, out_h, out_x, out_y). Inverse viewpoint of codec's
38/// pull maps; same labeling, pinned by the differential tests.
39fn push_target(t: u8, w: u32, h: u32, sx: u32, sy: u32) -> (u32, u32, u32, u32) {
40    match t {
41        0 => (w, h, sx, sy),
42        1 => (w, h, w - 1 - sx, sy),
43        2 => (w, h, sx, h - 1 - sy),
44        3 => (w, h, w - 1 - sx, h - 1 - sy),
45        4 => (h, w, h - 1 - sy, sx),
46        5 => {
47            let (ow, oh, ox, oy) = push_target(4, w, h, sx, sy);
48            push_target(1, ow, oh, ox, oy)
49        }
50        6 => {
51            let (ow, oh, ox, oy) = push_target(4, w, h, sx, sy);
52            push_target(3, ow, oh, ox, oy)
53        }
54        7 => {
55            let (ow, oh, ox, oy) = push_target(4, w, h, sx, sy);
56            let (w2, h2, ox2, oy2) = push_target(3, ow, oh, ox, oy);
57            push_target(1, w2, h2, ox2, oy2)
58        }
59        _ => unreachable!("range validated by caller"),
60    }
61}
62
63fn push_transform(t: u8, w: u32, h: u32, data: &[u8]) -> Vec<u8> {
64    let (ow, oh) = if t >= 4 { (h, w) } else { (w, h) };
65    let mut out = vec![0u8; (ow * oh * 3) as usize];
66    let mut s = 0usize;
67    for sy in 0..h {
68        for sx in 0..w {
69            let (_ow, _oh, ox, oy) = push_target(t, w, h, sx, sy);
70            let ow_ = if t >= 4 { h } else { w };
71            let d = ((oy * ow_ + ox) * 3) as usize;
72            out[d] = data[s];
73            out[d + 1] = data[s + 1];
74            out[d + 2] = data[s + 2];
75            s += 3;
76        }
77    }
78    out
79}
80
81/// Reference-implementation decode of MNF5 (independent path for differential testing).
82pub fn ref_decode(buf: &[u8]) -> Result<RefDecodeResult> {
83    if buf.len() < HEADER {
84        return Err(Error::fmt_msg("short"));
85    }
86    if buf[0..4] != *b"MNF5" {
87        return Err(Error::fmt_msg("magic"));
88    }
89    let u32le = |o: usize| u32::from_le_bytes(buf[o..o + 4].try_into().unwrap());
90    let width = u32le(4);
91    let height = u32le(8);
92    if width == 0 || height == 0 {
93        return Err(Error::fmt_msg("dims"));
94    }
95    if width as u64 * height as u64 > MAX_PIXELS {
96        return Err(Error::fmt_msg("pixels"));
97    }
98    let ts = u32le(12);
99    if ts != TS {
100        return Err(Error::fmt_msg("tilesize"));
101    }
102    let cols = div_ceil(width, TS);
103    let rows = div_ceil(height, TS);
104    let tile_count = u32le(16);
105    if tile_count != cols * rows {
106        return Err(Error::fmt_msg("tilecount"));
107    }
108    let payload_len = u32le(20);
109    if payload_len > MAX_PAYLOAD {
110        return Err(Error::fmt_msg("payloadlen"));
111    }
112    if buf.len() != HEADER + payload_len as usize {
113        return Err(Error::fmt_msg("filelen"));
114    }
115    let zlib = &buf[HEADER..];
116
117    let limit = tile_count as u64 * (1 + 5 + (TS * TS * 3) as u64);
118    let mut raw: Vec<u8> = Vec::new();
119    {
120        let mut d = flate2::read::ZlibDecoder::new(zlib).take(limit + 1);
121        d.read_to_end(&mut raw)
122            .map_err(|_| Error::Decompress("corrupt zlib stream"))?;
123    }
124    if raw.len() as u64 > limit {
125        return Err(Error::fmt_msg("bomb"));
126    }
127
128    let mut pixels = vec![0u8; width as usize * height as usize * 3];
129    let mut decoded: Vec<(u32, u32, Vec<u8>)> = Vec::with_capacity(tile_count as usize);
130    let mut ip = 0usize;
131    for i in 0..tile_count {
132        let row = i / cols;
133        let col = i % cols;
134        let tw = TS.min(width - col * TS);
135        let th = TS.min(height - row * TS);
136        let n = (tw * th * 3) as usize;
137
138        if ip >= raw.len() {
139            return Err(Error::fmt_msg("eof"));
140        }
141        let mode = raw[ip];
142        ip += 1;
143
144        let refs: Option<(usize, u8)> = match mode {
145            0 => None,
146            1 => {
147                if ip + 4 > raw.len() {
148                    return Err(Error::fmt_msg("refmeta"));
149                }
150                let idx = u32::from_le_bytes(raw[ip..ip + 4].try_into().unwrap()) as usize;
151                ip += 4;
152                Some((idx, 0))
153            }
154            2 => {
155                if ip + 5 > raw.len() {
156                    return Err(Error::fmt_msg("refmeta"));
157                }
158                let idx = u32::from_le_bytes(raw[ip..ip + 4].try_into().unwrap()) as usize;
159                let tr = raw[ip + 4];
160                ip += 5;
161                Some((idx, tr))
162            }
163            _ => return Err(Error::fmt_msg("mode")),
164        };
165
166        if ip + n > raw.len() {
167            return Err(Error::fmt_msg("body"));
168        }
169        let body = raw[ip..ip + n].to_vec();
170        ip += n;
171
172        let tile = match refs {
173            None => body,
174            Some((src, tr)) => {
175                if src >= i as usize {
176                    return Err(Error::fmt_msg("reforder"));
177                }
178                if tr >= 8 {
179                    return Err(Error::fmt_msg("tranform-range"));
180                }
181                let (rw, rh, ref_rgb) = &decoded[src];
182                if *rw != tw || *rh != th {
183                    return Err(Error::fmt_msg("refshape"));
184                }
185                if tw != th && tr >= 4 {
186                    return Err(Error::fmt_msg("xform-shape"));
187                }
188                let base = if tr == 0 {
189                    ref_rgb.clone()
190                } else {
191                    push_transform(tr, tw, th, ref_rgb)
192                };
193                base.iter()
194                    .zip(body.iter())
195                    .map(|(a, r)| a.wrapping_add(*r))
196                    .collect()
197            }
198        };
199        decoded.push((tw, th, tile.clone()));
200
201        for y in 0..th as usize {
202            let py = row as usize * TS as usize + y;
203            for x in 0..tw as usize {
204                let pxx = col as usize * TS as usize + x;
205                let d = (py * width as usize + pxx) * 3;
206                let s = (y * tw as usize + x) * 3;
207                pixels[d] = tile[s];
208                pixels[d + 1] = tile[s + 1];
209                pixels[d + 2] = tile[s + 2];
210            }
211        }
212    }
213    if ip != raw.len() {
214        return Err(Error::fmt_msg("extra"));
215    }
216    Ok(RefDecodeResult {
217        dims: Dims { width, height },
218        pixels,
219    })
220}