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}