Skip to main content

macula_rust/
cbor.rs

1//! Deterministic CBOR encode/decode, byte-for-byte compatible with
2//! macula's own wire codec.
3//!
4//! This is NOT generic CBOR (no ciborium involved on either side) — it is
5//! a direct Rust transcription of the hand-rolled canonical encoder macula
6//! actually ships in `native/macula_cbor_nif/src/deterministic.rs`
7//! (`macula-io/macula`), which `macula_frame.erl`'s wire codec calls as
8//! `pack_deterministic/1` / `unpack_deterministic/1`. Every frame's
9//! Ed25519 signature is computed over these exact bytes, so a divergence
10//! here silently breaks signature verification against real stations —
11//! this module's tests include fixtures captured directly from the real
12//! NIF (`rebar3 shell` against `macula-io/macula` at v10.10.0), not just
13//! hand-derived expectations.
14//!
15//! Encoding rules (all verified against the reference, see `tests` below):
16//! - Integers: minimal-length encoding (inline for 0..=23, else the
17//!   smallest of 1/2/4/8 extra bytes that fits). Non-negative → major 0.
18//!   Negative → major 1, encoded value is `-1 - n`. Range:
19//!   `-(2^64)..=u64::MAX` — anything outside that is a hard encode error,
20//!   not silent truncation.
21//! - Byte strings → major 2, raw bytes.
22//! - Text → major 3. Used both for real text payloads and for macula's
23//!   fixed field-name/enum-value vocabulary (what the Erlang side encodes
24//!   as atoms) — there is no separate "atom" wire type.
25//! - Lists → major 4.
26//! - Maps → major 5, with keys sorted by the **bytewise order of their
27//!   own already-encoded bytes** — encode each key independently, then
28//!   sort the resulting `(key_bytes, value_bytes)` pairs by `key_bytes`
29//!   using plain `Ord`. This is the single rule most likely to be gotten
30//!   wrong: sorting by the *original* value instead of its *encoded*
31//!   bytes silently diverges from station output for keys of different
32//!   CBOR major types or different lengths.
33//! - `Value::Null` → major 7, additional info 22 (`0xF6`).
34//! - Floats → **always** binary64 (major 7, AI 27, `0xFB` prefix),
35//!   regardless of whether the value would round-trip in fewer bits. This
36//!   is a deliberate divergence from RFC 8949's own canonical-form
37//!   recommendation (which prefers the shortest float width that
38//!   round-trips) — macula's own comment says it's done so the byte
39//!   derivation is independent of platform float encoding. A generic
40//!   "canonical CBOR" crate that follows the RFC's shortest-float rule
41//!   would silently produce non-matching, non-verifying bytes here.
42//!
43//! Decode is deliberately narrow to match the reference: major type 6
44//! (tags) is rejected outright, and major 7 only supports `null` and the
45//! three float widths (binary16/32/64, all promoted to `f64`) — no
46//! booleans, no "undefined" simple value. Every read is bounds-checked;
47//! nothing in this module panics on malformed or truncated input, since
48//! decode exists specifically to parse untrusted, network-received bytes.
49
50use std::fmt;
51
52/// A deterministic-CBOR value, restricted to exactly the shapes macula's
53/// wire format supports. There is no generic "any CBOR" here on purpose.
54#[derive(Debug, Clone, PartialEq)]
55pub enum Value {
56    /// Signed, but the encodable range is asymmetric: `-(2^64)..=u64::MAX`,
57    /// matching the reference codec's own u64/i128 split.
58    Int(i128),
59    Bytes(Vec<u8>),
60    /// Also what an Erlang atom (frame-type names, field names, enum
61    /// values) becomes on the wire — see the module doc.
62    Text(String),
63    List(Vec<Value>),
64    /// Insertion order on construction; canonical key sort happens at
65    /// encode time, not here. Decode preserves last-write-wins on
66    /// duplicate keys, matching the reference decoder exactly.
67    Map(Vec<(Value, Value)>),
68    Null,
69    /// Always round-trips through binary64 — see the module doc's note
70    /// on why this diverges from RFC 8949's canonical-form guidance.
71    Float(f64),
72}
73
74impl Value {
75    /// Convenience: build a `Text` value from anything `Into<String>`.
76    pub fn text(s: impl Into<String>) -> Self {
77        Value::Text(s.into())
78    }
79
80    /// Look up a field by text key. `None` if this isn't a `Map` or the
81    /// key isn't present — mirrors macula's own field vocabulary, which
82    /// is always text keys (see the module doc's atom/text note).
83    pub fn get(&self, key: &str) -> Option<&Value> {
84        match self {
85            Value::Map(pairs) => pairs
86                .iter()
87                .find(|(k, _)| matches!(k, Value::Text(t) if t == key))
88                .map(|(_, v)| v),
89            _ => None,
90        }
91    }
92
93    /// A new `Map` with the given text keys removed. Non-maps pass
94    /// through unchanged. Used to compute a frame's signable bytes (the
95    /// frame minus `signature`/`publisher_sig`) — see `crate::frame`.
96    pub fn without(&self, keys: &[&str]) -> Value {
97        match self {
98            Value::Map(pairs) => Value::Map(
99                pairs
100                    .iter()
101                    .filter(|(k, _)| !matches!(k, Value::Text(t) if keys.contains(&t.as_str())))
102                    .cloned()
103                    .collect(),
104            ),
105            other => other.clone(),
106        }
107    }
108
109    /// Insert or replace a field in a `Map` by text key, consuming and
110    /// returning `self` for chaining. A no-op on a non-map value.
111    pub fn with_field(mut self, key: &str, value: Value) -> Value {
112        if let Value::Map(pairs) = &mut self {
113            match pairs
114                .iter_mut()
115                .find(|(k, _)| matches!(k, Value::Text(t) if t == key))
116            {
117                Some(entry) => entry.1 = value,
118                None => pairs.push((Value::text(key), value)),
119            }
120        }
121        self
122    }
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub struct IntOutOfRange(pub i128);
127
128impl fmt::Display for IntOutOfRange {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        write!(
131            f,
132            "integer {} is outside the encodable range -(2^64)..=u64::MAX",
133            self.0
134        )
135    }
136}
137
138impl std::error::Error for IntOutOfRange {}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub enum DecodeError {
142    /// The buffer ended before a complete value could be read.
143    Truncated,
144    /// Major type 6 (tags) — not part of macula's wire format.
145    UnsupportedMajorType(u8),
146    /// A major-7 additional-info value with no meaning here (only 22
147    /// \[null\] and 25/26/27 \[floats\] are supported).
148    UnsupportedAdditionalInfo(u8),
149    /// Additional-info 28-31 on any major type — reserved, unused.
150    UnsupportedAdditionalInfoEncoding(u8),
151    /// A single top-level value didn't consume the whole buffer.
152    TrailingBytes,
153    /// A major-3 (text) value's bytes were not valid UTF-8. The reference
154    /// Erlang/Rust codec does not validate this on decode (it stores
155    /// whatever bytes arrived); this port deliberately diverges and
156    /// treats it as an error instead of losslessly carrying invalid
157    /// UTF-8, since every real macula text value is ASCII/UTF-8 by
158    /// construction and failing closed on malformed input from a peer is
159    /// the safer default. Documented, not accidental.
160    InvalidUtf8,
161    /// A half-float (binary16) with exponent 31 — NaN or infinity, which
162    /// has no representation as an ordinary `f64` value here (matches
163    /// the reference decoder's own behavior: no clause for it).
164    UnrepresentableFloat,
165    /// Lists/maps nested more than [`MAX_NESTING_DEPTH`] levels deep.
166    /// Not part of the wire format's own semantics — a defense against a
167    /// maliciously crafted frame: a list-of-one-list-of-one-list... can
168    /// encode extreme nesting in very few bytes (one byte per level),
169    /// and this decoder is plain recursive descent, so without a limit
170    /// a peer could crash the process with a stack overflow (not a
171    /// catchable panic) from a single frame well under
172    /// `frame::MAX_FRAME_BYTES`. No real macula wire value nests anywhere
173    /// close to this deep.
174    NestingTooDeep,
175}
176
177impl fmt::Display for DecodeError {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        match self {
180            DecodeError::Truncated => write!(f, "truncated input"),
181            DecodeError::UnsupportedMajorType(m) => {
182                write!(
183                    f,
184                    "unsupported major type {m} (only 0-5 and 7 are valid here)"
185                )
186            }
187            DecodeError::UnsupportedAdditionalInfo(ai) => {
188                write!(f, "unsupported major-7 additional info {ai}")
189            }
190            DecodeError::UnsupportedAdditionalInfoEncoding(ai) => {
191                write!(
192                    f,
193                    "unsupported additional-info encoding {ai} (28-31 are reserved)"
194                )
195            }
196            DecodeError::TrailingBytes => write!(f, "trailing bytes after the top-level value"),
197            DecodeError::InvalidUtf8 => write!(f, "text value was not valid UTF-8"),
198            DecodeError::UnrepresentableFloat => {
199                write!(f, "half-float NaN/infinity has no f64 representation here")
200            }
201            DecodeError::NestingTooDeep => {
202                write!(f, "list/map nesting exceeds {MAX_NESTING_DEPTH} levels")
203            }
204        }
205    }
206}
207
208impl std::error::Error for DecodeError {}
209
210/// Encode `value` as deterministic CBOR. See the module doc for the exact
211/// rules; every one of them is verified against the real reference in
212/// this module's tests.
213pub fn encode(value: &Value) -> Result<Vec<u8>, IntOutOfRange> {
214    let mut out = Vec::with_capacity(64);
215    encode_value(value, &mut out)?;
216    Ok(out)
217}
218
219fn encode_value(value: &Value, out: &mut Vec<u8>) -> Result<(), IntOutOfRange> {
220    match value {
221        Value::Int(n) => encode_int(*n, out),
222        Value::Bytes(b) => {
223            encode_head(2, b.len() as u64, out);
224            out.extend_from_slice(b);
225            Ok(())
226        }
227        Value::Text(s) => {
228            let bytes = s.as_bytes();
229            encode_head(3, bytes.len() as u64, out);
230            out.extend_from_slice(bytes);
231            Ok(())
232        }
233        Value::List(items) => {
234            encode_head(4, items.len() as u64, out);
235            for item in items {
236                encode_value(item, out)?;
237            }
238            Ok(())
239        }
240        Value::Map(pairs) => encode_map(pairs, out),
241        Value::Null => {
242            out.push(0xF6); // major 7, additional info 22
243            Ok(())
244        }
245        Value::Float(v) => {
246            out.push(0xFB); // major 7, additional info 27 (binary64)
247            out.extend_from_slice(&v.to_be_bytes());
248            Ok(())
249        }
250    }
251}
252
253fn encode_int(n: i128, out: &mut Vec<u8>) -> Result<(), IntOutOfRange> {
254    if n >= 0 {
255        if n <= u64::MAX as i128 {
256            encode_head(0, n as u64, out);
257            Ok(())
258        } else {
259            Err(IntOutOfRange(n))
260        }
261    } else {
262        // n in -(2^64)..=-1 => count in 0..=2^64-1
263        let count = -1i128 - n;
264        if (0..=u64::MAX as i128).contains(&count) {
265            encode_head(1, count as u64, out);
266            Ok(())
267        } else {
268            Err(IntOutOfRange(n))
269        }
270    }
271}
272
273/// Encode each key/value independently, then sort the resulting pairs by
274/// the key's OWN ENCODED BYTES (plain lexicographic `Ord` on `Vec<u8>`,
275/// which already implements "shorter is smaller when a prefix" — no
276/// special-casing needed). This is the one rule a naive implementation is
277/// most likely to get wrong; see the module doc.
278fn encode_map(pairs: &[(Value, Value)], out: &mut Vec<u8>) -> Result<(), IntOutOfRange> {
279    let mut encoded: Vec<(Vec<u8>, Vec<u8>)> = Vec::with_capacity(pairs.len());
280    for (k, v) in pairs {
281        let mut kbuf = Vec::with_capacity(16);
282        encode_value(k, &mut kbuf)?;
283        let mut vbuf = Vec::with_capacity(16);
284        encode_value(v, &mut vbuf)?;
285        encoded.push((kbuf, vbuf));
286    }
287    encoded.sort_by(|a, b| a.0.cmp(&b.0));
288    encode_head(5, encoded.len() as u64, out);
289    for (k, v) in &encoded {
290        out.extend_from_slice(k);
291        out.extend_from_slice(v);
292    }
293    Ok(())
294}
295
296fn encode_head(major: u8, n: u64, out: &mut Vec<u8>) {
297    if n <= 23 {
298        out.push((major << 5) | (n as u8));
299    } else if n <= 0xFF {
300        out.push((major << 5) | 24);
301        out.push(n as u8);
302    } else if n <= 0xFFFF {
303        out.push((major << 5) | 25);
304        out.extend_from_slice(&(n as u16).to_be_bytes());
305    } else if n <= 0xFFFF_FFFF {
306        out.push((major << 5) | 26);
307        out.extend_from_slice(&(n as u32).to_be_bytes());
308    } else {
309        out.push((major << 5) | 27);
310        out.extend_from_slice(&n.to_be_bytes());
311    }
312}
313
314/// Recursive-descent nesting limit — see [`DecodeError::NestingTooDeep`]
315/// for why this exists. No real macula wire value nests remotely this
316/// deep; this only ever rejects an adversarial input.
317pub const MAX_NESTING_DEPTH: usize = 128;
318
319/// Decode a single deterministic-CBOR value from `bytes`. The whole
320/// buffer must be consumed by exactly one top-level value — trailing
321/// bytes are an error, matching the reference decoder's own contract.
322pub fn decode(bytes: &[u8]) -> Result<Value, DecodeError> {
323    // Nobody consumes the top-level value's canonical bytes — don't
324    // build them (see `decode_one`'s `need_canon` param).
325    let (value, _canonical_bytes, pos) = decode_one(bytes, 0, 0, false)?;
326    if pos != bytes.len() {
327        return Err(DecodeError::TrailingBytes);
328    }
329    Ok(value)
330}
331
332fn need(buf: &[u8], pos: usize, n: usize) -> Result<(), DecodeError> {
333    match pos.checked_add(n) {
334        Some(end) if end <= buf.len() => Ok(()),
335        _ => Err(DecodeError::Truncated),
336    }
337}
338
339/// Decodes one value, and — only when `need_canon` is true — its own
340/// canonical (deterministic-CBOR) bytes, built bottom-up as decoding
341/// proceeds rather than re-derived by a separate encode pass afterward.
342/// See `decode_map`'s doc for why the bytes are needed at all (a map
343/// using another map as a key needs its key's canonical bytes to
344/// dedupe/sort by, and re-encoding a key from scratch at every ancestor
345/// level is itself an unbounded-work trap on nested input) and why
346/// `need_canon` exists (computing them for every value regardless of
347/// whether anything ever reads them — the common case, since most
348/// decoded values are never used as a map key at any level — turned out
349/// to be its own real cost: a value nested `depth` levels inside a
350/// value that never touches a map key at all still doesn't need canon
351/// bytes, but always building them anyway meant a large nested
352/// non-map-keyed value paid full canon-construction cost with nothing
353/// to show for it, confirmed to regress both time and peak memory on
354/// large deep lists/values with no map keys anywhere in them).
355/// `decode_map` is the only caller that ever passes different values
356/// for its two child calls: always `true` for a key (dedup needs it
357/// unconditionally, regardless of whether the map's OWN canon bytes are
358/// wanted) and its own `need_canon` for a value (only needed if this
359/// whole map is itself nested inside some ancestor's key).
360fn decode_one(
361    buf: &[u8],
362    pos: usize,
363    depth: usize,
364    need_canon: bool,
365) -> Result<(Value, Vec<u8>, usize), DecodeError> {
366    if depth > MAX_NESTING_DEPTH {
367        return Err(DecodeError::NestingTooDeep);
368    }
369    need(buf, pos, 1)?;
370    let byte0 = buf[pos];
371    let major = byte0 >> 5;
372    let ai = byte0 & 0x1F;
373
374    if major == 7 {
375        let (value, next) = decode_major7(buf, pos, ai)?;
376        return Ok(scalar_canonical_bytes(value, next, need_canon));
377    }
378
379    let (n, next) = decode_count(buf, pos + 1, ai)?;
380    match major {
381        0 => Ok(scalar_canonical_bytes(
382            Value::Int(n as i128),
383            next,
384            need_canon,
385        )),
386        1 => Ok(scalar_canonical_bytes(
387            Value::Int(-1i128 - n as i128),
388            next,
389            need_canon,
390        )),
391        2 => {
392            let len = n as usize;
393            need(buf, next, len)?;
394            let value = Value::Bytes(buf[next..next + len].to_vec());
395            Ok(scalar_canonical_bytes(value, next + len, need_canon))
396        }
397        3 => {
398            let len = n as usize;
399            need(buf, next, len)?;
400            let text = String::from_utf8(buf[next..next + len].to_vec())
401                .map_err(|_| DecodeError::InvalidUtf8)?;
402            Ok(scalar_canonical_bytes(
403                Value::Text(text),
404                next + len,
405                need_canon,
406            ))
407        }
408        4 => decode_list(buf, next, n, depth + 1, need_canon),
409        5 => decode_map(buf, next, n, depth + 1, need_canon),
410        _ => Err(DecodeError::UnsupportedMajorType(major)),
411    }
412}
413
414/// `with_canonical_bytes`, but skipped (an empty `Vec` instead) when
415/// nothing will ever read it — see `decode_one`'s `need_canon` doc.
416fn scalar_canonical_bytes(value: Value, next: usize, need_canon: bool) -> (Value, Vec<u8>, usize) {
417    if need_canon {
418        with_canonical_bytes(value, next)
419    } else {
420        (value, Vec::new(), next)
421    }
422}
423
424/// Computes a scalar (non-list/map) value's own canonical bytes via a
425/// plain, non-recursive `encode_value` call — cheap regardless of where
426/// in a nested structure it's called from, unlike `List`/`Map`, which
427/// build their canonical bytes by concatenating their CHILDREN's
428/// already-computed bytes (see `decode_list`/`decode_map`) instead of
429/// calling `encode_value` on themselves.
430fn with_canonical_bytes(value: Value, next: usize) -> (Value, Vec<u8>, usize) {
431    let mut canon = Vec::new();
432    encode_value(&value, &mut canon).expect("a value produced by this decoder is always encodable");
433    (value, canon, next)
434}
435
436fn decode_count(buf: &[u8], pos: usize, ai: u8) -> Result<(u64, usize), DecodeError> {
437    match ai {
438        0..=23 => Ok((ai as u64, pos)),
439        24 => {
440            need(buf, pos, 1)?;
441            Ok((buf[pos] as u64, pos + 1))
442        }
443        25 => {
444            need(buf, pos, 2)?;
445            Ok((u16::from_be_bytes([buf[pos], buf[pos + 1]]) as u64, pos + 2))
446        }
447        26 => {
448            need(buf, pos, 4)?;
449            let b: [u8; 4] = buf[pos..pos + 4].try_into().expect("checked len");
450            Ok((u32::from_be_bytes(b) as u64, pos + 4))
451        }
452        27 => {
453            need(buf, pos, 8)?;
454            let b: [u8; 8] = buf[pos..pos + 8].try_into().expect("checked len");
455            Ok((u64::from_be_bytes(b), pos + 8))
456        }
457        28..=31 => Err(DecodeError::UnsupportedAdditionalInfoEncoding(ai)),
458        _ => unreachable!("additional info is a 5-bit field, 0..=31"),
459    }
460}
461
462fn decode_major7(buf: &[u8], pos: usize, ai: u8) -> Result<(Value, usize), DecodeError> {
463    match ai {
464        22 => Ok((Value::Null, pos + 1)),
465        25 => {
466            need(buf, pos + 1, 2)?;
467            let half = u16::from_be_bytes([buf[pos + 1], buf[pos + 2]]);
468            Ok((Value::Float(half_to_f64(half)?), pos + 3))
469        }
470        26 => {
471            need(buf, pos + 1, 4)?;
472            let b: [u8; 4] = buf[pos + 1..pos + 5].try_into().expect("checked len");
473            Ok((Value::Float(f32::from_be_bytes(b) as f64), pos + 5))
474        }
475        27 => {
476            need(buf, pos + 1, 8)?;
477            let b: [u8; 8] = buf[pos + 1..pos + 9].try_into().expect("checked len");
478            Ok((Value::Float(f64::from_be_bytes(b)), pos + 9))
479        }
480        _ => Err(DecodeError::UnsupportedAdditionalInfo(ai)),
481    }
482}
483
484fn decode_list(
485    buf: &[u8],
486    mut pos: usize,
487    count: u64,
488    depth: usize,
489    need_canon: bool,
490) -> Result<(Value, Vec<u8>, usize), DecodeError> {
491    let mut items = Vec::with_capacity(count.min(1024) as usize);
492    let mut canon = Vec::new();
493    if need_canon {
494        encode_head(4, count, &mut canon);
495    }
496    for _ in 0..count {
497        let (item, item_canon, next) = decode_one(buf, pos, depth, need_canon)?;
498        if need_canon {
499            canon.extend_from_slice(&item_canon);
500        }
501        items.push(item);
502        pos = next;
503    }
504    Ok((Value::List(items), canon, pos))
505}
506
507/// Duplicate keys overwrite (last write wins), matching the reference
508/// decoder exactly — not treated as an error.
509///
510/// Looks up each key's slot by its own canonical bytes (from
511/// `decode_one`'s bottom-up construction — see that function's doc) in
512/// a `HashMap`, rather than a `Value`-equality linear scan over
513/// everything decoded so far: the scan made this function O(n²) on a
514/// map with many distinct keys — a single ~350 KB crafted frame (well
515/// under `frame::MAX_FRAME_BYTES`) pegged a CPU core for 50+ seconds
516/// decoding it, and the cost scaled quadratically toward the
517/// frame-size cap, all of it running before any signature check on the
518/// frame.
519///
520/// An earlier version of this fix looked up each key by calling
521/// `encode(&k)` fresh, per entry, instead of reusing the bytes
522/// `decode_one` already built while decoding that same key — that's
523/// sound for a FLAT map (fixed the 350 KB/50 s case, confirmed
524/// empirically), but reintroduced unbounded work for a map whose KEY is
525/// itself a large nested structure: re-encoding a key from scratch at
526/// every ancestor level costs O(depth × key size), and a 128-level
527/// chain of single-entry maps (`MAX_NESTING_DEPTH`) each keyed by a
528/// large blob turned back into tens of seconds of pre-auth CPU on a
529/// frame still under the size cap — confirmed empirically. Building
530/// canonical bytes bottom-up (each value's bytes computed exactly once,
531/// when it's decoded, then only ever concatenated/sorted by its
532/// ancestors — never re-derived) fixed that: the same 128-deep/15 MB
533/// case dropped from ~31 s to ~1 s. This is O(depth × size), the same
534/// bound `MAX_NESTING_DEPTH` already exists to enforce — NOT O(total
535/// input size) regardless of nesting shape, since a key containing a
536/// key still gets its bytes copied once per level it's nested under.
537/// It just can no longer exceed the depth cap's own bound, the same
538/// guarantee `NestingTooDeep` already gives the rest of this decoder.
539///
540/// Computing canon bytes unconditionally for every value (not just
541/// values that end up under a map key somewhere) was ALSO measured to
542/// be a real, separate cost — a large nested value that never touches
543/// a map key still paid full canon-construction cost for nothing;
544/// `need_canon` (threaded through `decode_one`/`decode_list`/this
545/// function) skips it. A key's canon is always needed, unconditionally
546/// (dedup requires it); a value's is only needed if this whole map is
547/// itself nested inside some ancestor's key, i.e. this map's OWN
548/// `need_canon`.
549///
550/// The still-remaining, deliberate, narrow divergences from a literal
551/// `Value`-equality scan, fuzzed against 500k adversarial inputs
552/// against both this and the pre-fix decoder: nested-map keys that
553/// differ only in wire insertion order now merge (the old scan kept
554/// both — wrong, since Erlang maps/this format's own key-sort are both
555/// unordered); `+0.0`/`-0.0` keys no longer merge (the old scan merged
556/// them via `PartialEq` — wrong, since neither Erlang's `=:=` nor the
557/// reference NIF's own byte-dedup merge them); bit-identical `NaN` keys
558/// now merge (the old scan never did, since `NaN != NaN` under
559/// `PartialEq` — matches the reference). All three move this decoder
560/// TOWARD the reference decoder's actual behavior, not away from it,
561/// and none of the three is reachable in practice: no real macula map
562/// key is ever a float or a nested map.
563fn decode_map(
564    buf: &[u8],
565    mut pos: usize,
566    count: u64,
567    depth: usize,
568    need_canon: bool,
569) -> Result<(Value, Vec<u8>, usize), DecodeError> {
570    let capacity = count.min(1024) as usize;
571    let mut pairs: Vec<(Value, Value)> = Vec::with_capacity(capacity);
572    // Owns each distinct key's canonical bytes (moved in on first sight,
573    // never cloned) -> slot index into `pairs`/`vals_canon`.
574    let mut index_of_key: std::collections::HashMap<Vec<u8>, usize> =
575        std::collections::HashMap::with_capacity(capacity);
576    // Per-slot VALUE canon, kept in step with `pairs` (same index, same
577    // last-write-wins updates) -- only populated when `need_canon`, since
578    // a key's canon (owned by `index_of_key` above) is the only one ever
579    // needed just to make dedup itself work.
580    let mut vals_canon: Vec<Vec<u8>> = Vec::with_capacity(if need_canon { capacity } else { 0 });
581    for _ in 0..count {
582        // A key ALWAYS needs its canon bytes -- that's the dedup
583        // identity itself, independent of whether this map's OWN canon
584        // bytes (built below) are ever going to be read by anything.
585        let (k, key_canon, next1) = decode_one(buf, pos, depth, true)?;
586        let (v, val_canon, next2) = decode_one(buf, next1, depth, need_canon)?;
587        pos = next2;
588        use std::collections::hash_map::Entry;
589        match index_of_key.entry(key_canon) {
590            Entry::Occupied(e) => {
591                let i = *e.get();
592                pairs[i].1 = v;
593                if need_canon {
594                    vals_canon[i] = val_canon;
595                }
596            }
597            Entry::Vacant(e) => {
598                e.insert(pairs.len());
599                pairs.push((k, v));
600                if need_canon {
601                    vals_canon.push(val_canon);
602                }
603            }
604        }
605    }
606    if !need_canon {
607        return Ok((Value::Map(pairs), Vec::new(), pos));
608    }
609    // Matches `encode_map`'s own rule exactly: sort entries by the
610    // key's encoded bytes, plain lexicographic `Ord` on `Vec<u8>`.
611    let mut order: Vec<(&Vec<u8>, usize)> = index_of_key.iter().map(|(k, &i)| (k, i)).collect();
612    order.sort_by(|a, b| a.0.cmp(b.0));
613    let mut canon = Vec::new();
614    encode_head(5, order.len() as u64, &mut canon);
615    for (k, i) in order {
616        canon.extend_from_slice(k);
617        canon.extend_from_slice(&vals_canon[i]);
618    }
619    Ok((Value::Map(pairs), canon, pos))
620}
621
622/// IEEE 754 binary16 → f64. Subnormals (exp=0) and normals (1..=30) use
623/// the standard formula; exp=31 (NaN/infinity) has no representation here
624/// — matches the reference decoder, which has no clause for it either.
625fn half_to_f64(half: u16) -> Result<f64, DecodeError> {
626    let sign: f64 = if (half >> 15) & 1 == 1 { -1.0 } else { 1.0 };
627    let exp = (half >> 10) & 0x1F;
628    let frac = (half & 0x3FF) as f64;
629    match exp {
630        0 => Ok(sign * 2f64.powi(-14) * (frac / 1024.0)),
631        1..=30 => Ok(sign * 2f64.powi(exp as i32 - 15) * (1.0 + frac / 1024.0)),
632        _ => Err(DecodeError::UnrepresentableFloat),
633    }
634}
635
636#[cfg(test)]
637mod tests {
638    use super::*;
639
640    /// Decode a hex string into bytes — test-only helper, not exposed
641    /// from the crate.
642    fn hex(s: &str) -> Vec<u8> {
643        ::hex::decode(s).expect("valid hex fixture")
644    }
645
646    /// Every fixture below was captured directly from the real
647    /// `macula_cbor_nif:pack_deterministic/1` in `macula-io/macula`
648    /// (v10.10.0) via `rebar3 shell`, not hand-derived — see this
649    /// module's doc comment. If one of these ever fails, the Rust port
650    /// has diverged from what a real station actually accepts, not the
651    /// test itself.
652    fn assert_matches_reference(value: Value, expected_hex: &str) {
653        let bytes = encode(&value).expect("encodable fixture");
654        assert_eq!(
655            bytes,
656            hex(expected_hex),
657            "encoding of {value:?} did not match the real macula_cbor_nif output"
658        );
659        // Round-trip: decoding what we just encoded must reproduce an
660        // equivalent value (structural equality, not necessarily the
661        // exact same Map key order — decode doesn't re-sort).
662        let decoded = decode(&bytes).expect("our own output must decode");
663        let re_encoded = encode(&decoded).expect("decoded value must re-encode");
664        assert_eq!(re_encoded, bytes, "encode(decode(bytes)) != bytes");
665    }
666
667    #[test]
668    fn empty_map() {
669        assert_matches_reference(Value::Map(vec![]), "A0");
670    }
671
672    #[test]
673    fn integers_non_negative_minimal_length() {
674        assert_matches_reference(Value::Int(0), "00");
675        assert_matches_reference(Value::Int(23), "17");
676        assert_matches_reference(Value::Int(24), "1818");
677        assert_matches_reference(Value::Int(255), "18FF");
678        assert_matches_reference(Value::Int(256), "190100");
679        assert_matches_reference(Value::Int(65535), "19FFFF");
680        assert_matches_reference(Value::Int(65536), "1A00010000");
681    }
682
683    #[test]
684    fn integers_negative_minimal_length() {
685        assert_matches_reference(Value::Int(-1), "20");
686        assert_matches_reference(Value::Int(-24), "37");
687        assert_matches_reference(Value::Int(-25), "3818");
688        assert_matches_reference(Value::Int(-256), "38FF");
689    }
690
691    #[test]
692    fn integer_out_of_range_is_rejected() {
693        // One past the documented positive bound.
694        assert_eq!(
695            encode(&Value::Int(u64::MAX as i128 + 1)),
696            Err(IntOutOfRange(u64::MAX as i128 + 1))
697        );
698        // One past the documented negative bound (-(2^64)).
699        let floor = -(1i128 << 64);
700        assert!(encode(&Value::Int(floor)).is_ok());
701        assert!(encode(&Value::Int(floor - 1)).is_err());
702    }
703
704    #[test]
705    fn byte_strings() {
706        assert_matches_reference(Value::Bytes(vec![]), "40");
707        assert_matches_reference(Value::Bytes(b"hello".to_vec()), "4568656C6C6F");
708    }
709
710    #[test]
711    fn text_and_atom_equivalent_encoding() {
712        // "hello" as text, and the Erlang atom `true` (which the
713        // reference encodes identically to a text value of the same
714        // name) — both are just major-3 text on this wire format.
715        assert_matches_reference(Value::text("hello"), "6568656C6C6F");
716        assert_matches_reference(Value::text("true"), "6474727565");
717    }
718
719    #[test]
720    fn lists() {
721        assert_matches_reference(Value::List(vec![]), "80");
722        assert_matches_reference(
723            Value::List(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
724            "83010203",
725        );
726    }
727
728    #[test]
729    fn floats_always_binary64() {
730        // Even exactly-representable, "shortenable" values still emit
731        // the full 8-byte form — the deliberate divergence from RFC
732        // 8949's canonical-form recommendation. This is the fixture most
733        // likely to catch a generic "canonical CBOR" crate substituted
734        // in by mistake.
735        assert_matches_reference(Value::Float(0.0), "FB0000000000000000");
736        assert_matches_reference(Value::Float(12345.6789), "FB40C81CD6E631F8A1");
737    }
738
739    #[test]
740    fn map_keys_sorted_by_encoded_bytes_not_input_order() {
741        // Input order is b, a — output must be a, b (bytewise key sort).
742        assert_matches_reference(
743            Value::Map(vec![
744                (Value::text("b"), Value::Int(2)),
745                (Value::text("a"), Value::Int(1)),
746            ]),
747            "A2616101616202",
748        );
749    }
750
751    #[test]
752    fn map_keys_sorted_lexicographically_same_length() {
753        assert_matches_reference(
754            Value::Map(vec![
755                (Value::text("zebra"), Value::Int(1)),
756                (Value::text("apple"), Value::Int(2)),
757            ]),
758            "A2656170706C6502657A6562726101",
759        );
760    }
761
762    #[test]
763    fn map_keys_shorter_sorts_first_when_prefix() {
764        // "a" < "aa" < "aaa" — the rule most likely to be implemented
765        // wrong if a naive implementation sorts by raw value instead of
766        // encoded bytes.
767        assert_matches_reference(
768            Value::Map(vec![
769                (Value::text("aa"), Value::Int(1)),
770                (Value::text("a"), Value::Int(2)),
771                (Value::text("aaa"), Value::Int(3)),
772            ]),
773            "A3616102626161016361616103",
774        );
775    }
776
777    #[test]
778    fn null_alone() {
779        // The Erlang side special-cases exactly the atom named `null`
780        // (0xF6) — a DIFFERENT atom like `undefined` is not recognized at
781        // this layer and encodes as ordinary text instead (see
782        // `text_and_atom_equivalent_encoding` and this module's doc: the
783        // `undefined` -> `null` conversion happens one layer up, in
784        // `macula_frame.erl`'s `to_wire/1`, not inside the codec itself).
785        assert_matches_reference(Value::Null, "F6");
786    }
787
788    #[test]
789    fn nested_structure_with_null() {
790        assert_matches_reference(
791            Value::Map(vec![
792                (Value::text("name"), Value::text("macula")),
793                (
794                    Value::text("nums"),
795                    Value::List(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
796                ),
797                (Value::text("nil"), Value::Null),
798            ]),
799            "A3636E696CF6646E616D65666D6163756C61646E756D7383010203",
800        );
801    }
802
803    #[test]
804    fn frame_shaped_map() {
805        let node_id: Vec<u8> = (1u8..=32).collect();
806        assert_matches_reference(
807            Value::Map(vec![
808                (Value::text("node_id"), Value::Bytes(node_id)),
809                (Value::text("version"), Value::Int(2)),
810                (Value::text("frame_type"), Value::text("connect")),
811                (Value::text("capabilities"), Value::Int(0)),
812            ]),
813            "A4676E6F64655F696458200102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F206776657273696F6E026A6672616D655F7479706567636F6E6E6563746C6361706162696C697469657300",
814        );
815    }
816
817    #[test]
818    fn decode_rejects_tags() {
819        // Major type 6, additional info 0 — a tag, not part of this wire
820        // format.
821        assert_eq!(decode(&[0xC0]), Err(DecodeError::UnsupportedMajorType(6)));
822    }
823
824    #[test]
825    fn decode_rejects_trailing_bytes() {
826        // A valid `0` (0x00) followed by a stray byte.
827        assert_eq!(decode(&[0x00, 0xFF]), Err(DecodeError::TrailingBytes));
828    }
829
830    #[test]
831    fn decode_rejects_truncated_input() {
832        // Major 0, AI 24 (one more byte expected) but the buffer ends.
833        assert_eq!(decode(&[0x18]), Err(DecodeError::Truncated));
834    }
835
836    /// Builds a payload of `depth` one-element-list wrappers (major 4,
837    /// AI 1 — a single byte, `0x81`, per level) around one terminal
838    /// scalar (`0x00`, the integer 0). Before `MAX_NESTING_DEPTH` existed,
839    /// decoding this crashed the whole process with a real stack
840    /// overflow (verified against this exact decoder pre-fix, on a
841    /// realistic 2 MiB worker-thread stack, at a nesting depth of only
842    /// 100_000 -- well under 1% of what a single 16 MiB wire frame could
843    /// carry) rather than returning a decode error. A stack overflow
844    /// aborts the process; it is not a `panic!` `#[should_panic]` can
845    /// catch, so the tests below only exercise the now-clean error path.
846    fn nested_list_payload(depth: usize) -> Vec<u8> {
847        let mut buf = vec![0x81u8; depth];
848        buf.push(0x00);
849        buf
850    }
851
852    #[test]
853    fn decode_accepts_nesting_at_the_depth_limit() {
854        let bytes = nested_list_payload(MAX_NESTING_DEPTH);
855        assert!(decode(&bytes).is_ok());
856    }
857
858    #[test]
859    fn decode_rejects_nesting_one_past_the_depth_limit() {
860        let bytes = nested_list_payload(MAX_NESTING_DEPTH + 1);
861        assert_eq!(decode(&bytes), Err(DecodeError::NestingTooDeep));
862    }
863
864    #[test]
865    fn decode_rejects_extreme_nesting_without_crashing() {
866        // Far beyond the limit, and far beyond what actually crashed the
867        // pre-fix decoder -- this is the direct regression test for the
868        // stack-overflow finding. If this test process crashes instead of
869        // completing, the depth guard has regressed.
870        let bytes = nested_list_payload(100_000);
871        assert_eq!(decode(&bytes), Err(DecodeError::NestingTooDeep));
872    }
873
874    #[test]
875    fn decode_duplicate_map_keys_last_write_wins() {
876        // Two entries both keyed "a" (0x61 0x61), values 1 then 2.
877        let bytes = hex("A2616101616102");
878        let decoded = decode(&bytes).expect("valid map");
879        match decoded {
880            Value::Map(pairs) => {
881                assert_eq!(pairs.len(), 1);
882                assert_eq!(pairs[0], (Value::text("a"), Value::Int(2)));
883            }
884            other => panic!("expected a map, got {other:?}"),
885        }
886    }
887
888    /// A duplicate key in the middle of several distinct ones overwrites
889    /// in place — the duplicate's ORIGINAL insertion slot, not a new one
890    /// appended at the end — and every other key's position is
891    /// undisturbed. Guards `decode_map`'s HashMap-indexed dedup: it would
892    /// be easy for a faster implementation to accidentally reorder
893    /// entries or dedupe the wrong slot.
894    #[test]
895    fn decode_duplicate_map_key_overwrites_its_original_slot_not_the_end() {
896        let map = Value::Map(vec![
897            (Value::text("a"), Value::Int(1)),
898            (Value::text("b"), Value::Int(2)),
899            (Value::text("c"), Value::Int(3)),
900        ]);
901        let mut bytes = encode(&map).expect("encodable");
902        // Append one more entry, "b" -> 99, so the wire form has 4
903        // entries with "b" duplicated -- can't build this through
904        // `encode` directly since it only ever emits already-deduped
905        // maps; construct the extra entry's bytes by hand and bump the
906        // map's own entry count (the map header's low nibble, byte 0).
907        assert_eq!(bytes[0] & 0x1F, 3, "expected a 3-entry map header");
908        bytes[0] = (bytes[0] & 0xE0) | 4;
909        bytes.extend_from_slice(&encode(&Value::text("b")).unwrap());
910        bytes.extend_from_slice(&encode(&Value::Int(99)).unwrap());
911
912        let decoded = decode(&bytes).expect("valid map");
913        match decoded {
914            Value::Map(pairs) => {
915                assert_eq!(
916                    pairs,
917                    vec![
918                        (Value::text("a"), Value::Int(1)),
919                        (Value::text("b"), Value::Int(99)),
920                        (Value::text("c"), Value::Int(3)),
921                    ]
922                );
923            }
924            other => panic!("expected a map, got {other:?}"),
925        }
926    }
927
928    /// Regression guard for a real bug: `decode_map`'s duplicate-key
929    /// check used to be a `Value`-equality linear scan over every entry
930    /// decoded so far, making decode O(n^2) in entry count. A single
931    /// ~350 KB crafted map (well under `frame::MAX_FRAME_BYTES`) took
932    /// 50+ seconds to decode as a result -- confirmed empirically against
933    /// the pre-fix code, not just reasoned about. This decodes twice as
934    /// many entries in a fraction of a second; if the map's dedup
935    /// regresses to linear-scan behavior, this test will time out long
936    /// before it fails its assertions.
937    #[test]
938    fn decode_map_with_many_distinct_keys_is_not_quadratic() {
939        let n: i128 = 20_000;
940        let pairs: Vec<(Value, Value)> = (0..n).map(|i| (Value::Int(i), Value::Int(0))).collect();
941        let bytes = encode(&Value::Map(pairs)).expect("encodable");
942
943        let start = std::time::Instant::now();
944        let decoded = decode(&bytes).expect("valid map");
945        let elapsed = start.elapsed();
946
947        match decoded {
948            Value::Map(decoded_pairs) => assert_eq!(decoded_pairs.len(), n as usize),
949            other => panic!("expected a map, got {other:?}"),
950        }
951        // The fixed decoder does this in low single-digit milliseconds;
952        // the old O(n^2) scan took whole seconds at this size. A wide
953        // margin avoids CI flakiness while still failing fast on a
954        // real complexity regression.
955        assert!(
956            elapsed < std::time::Duration::from_secs(2),
957            "decoding {n} distinct-keyed entries took {elapsed:?} -- \
958             looks like decode_map regressed to O(n^2)"
959        );
960    }
961
962    /// A second, narrower regression this same bug had once already:
963    /// the first attempt at fixing the flat-map O(n^2) case above
964    /// re-encoded each key fresh (`encode(&k)`) to find its slot, which
965    /// fixed the flat case but reintroduced unbounded work for a map
966    /// whose KEY is itself a large nested structure -- re-encoding a
967    /// key from scratch at every ancestor level costs O(depth × key
968    /// size), and a `MAX_NESTING_DEPTH`-deep chain of single-entry maps
969    /// keyed by a large blob took real, measured tens of seconds even
970    /// though it's well under `frame::MAX_FRAME_BYTES`. This decodes a
971    /// nesting-depth-limit-deep chain wrapping a multi-megabyte blob key
972    /// in well under a second; if key canonicalization regresses to
973    /// re-deriving a key's bytes at every ancestor level instead of
974    /// reusing what decoding that key already computed, this test will
975    /// time out long before it fails its assertions.
976    #[test]
977    fn decode_map_with_a_large_deeply_nested_key_is_not_quadratic_in_depth() {
978        // `MAX_NESTING_DEPTH` copies of "a 1-entry map wrapping...",
979        // around one 4 MiB byte-string key, each level's own map then
980        // valued at `Int(0)` (innermost first).
981        let blob_len = 512 * 1024;
982        let mut bytes = vec![0xA1u8; MAX_NESTING_DEPTH];
983        bytes.push(0x5A); // major 2 (bytes), AI 26 -> 4-byte length follows
984        bytes.extend_from_slice(&(blob_len as u32).to_be_bytes());
985        bytes.extend(std::iter::repeat_n(0x41u8, blob_len));
986        bytes.extend(std::iter::repeat_n(0x00u8, MAX_NESTING_DEPTH));
987
988        let start = std::time::Instant::now();
989        let decoded = decode(&bytes).expect("valid, maximally-nested map-key chain");
990        let elapsed = start.elapsed();
991
992        // Sanity: really did decode the full nested-map chain down to
993        // the 4 MiB blob at its center, not bail out early on a
994        // malformed payload. `0xA1` nests a 1-entry map as each level's
995        // KEY, so the blob is `MAX_NESTING_DEPTH` levels of `Map` down.
996        let mut cursor = &decoded;
997        for _ in 0..MAX_NESTING_DEPTH {
998            match cursor {
999                Value::Map(pairs) if pairs.len() == 1 => cursor = &pairs[0].0,
1000                other => panic!("expected a 1-entry map at this nesting level, got {other:?}"),
1001            }
1002        }
1003        match cursor {
1004            Value::Bytes(b) => assert_eq!(b.len(), blob_len),
1005            other => panic!("expected the innermost key to be Bytes, got {other:?}"),
1006        }
1007        assert!(
1008            elapsed < std::time::Duration::from_secs(5),
1009            "decoding a {MAX_NESTING_DEPTH}-deep map-key chain around a {blob_len}-byte blob \
1010             took {elapsed:?} -- looks like key canonicalization regressed to re-deriving a \
1011             key's bytes at every ancestor level instead of reusing decode_one's own"
1012        );
1013    }
1014
1015    #[test]
1016    fn get_finds_a_field_by_text_key() {
1017        let map = Value::Map(vec![(Value::text("a"), Value::Int(1))]);
1018        assert_eq!(map.get("a"), Some(&Value::Int(1)));
1019        assert_eq!(map.get("missing"), None);
1020    }
1021
1022    #[test]
1023    fn get_on_a_non_map_is_none() {
1024        assert_eq!(Value::Int(1).get("a"), None);
1025    }
1026
1027    #[test]
1028    fn without_removes_only_the_named_keys() {
1029        let map = Value::Map(vec![
1030            (Value::text("a"), Value::Int(1)),
1031            (Value::text("b"), Value::Int(2)),
1032            (Value::text("c"), Value::Int(3)),
1033        ]);
1034        let stripped = map.without(&["b"]);
1035        assert_eq!(stripped.get("a"), Some(&Value::Int(1)));
1036        assert_eq!(stripped.get("b"), None);
1037        assert_eq!(stripped.get("c"), Some(&Value::Int(3)));
1038    }
1039
1040    #[test]
1041    fn with_field_replaces_an_existing_key_in_place() {
1042        let map =
1043            Value::Map(vec![(Value::text("a"), Value::Int(1))]).with_field("a", Value::Int(2));
1044        assert_eq!(map.get("a"), Some(&Value::Int(2)));
1045        // Replacing, not appending — still exactly one pair.
1046        match map {
1047            Value::Map(pairs) => assert_eq!(pairs.len(), 1),
1048            _ => panic!("expected a map"),
1049        }
1050    }
1051
1052    #[test]
1053    fn with_field_appends_a_new_key() {
1054        let map = Value::Map(vec![]).with_field("a", Value::Int(1));
1055        assert_eq!(map.get("a"), Some(&Value::Int(1)));
1056    }
1057}